题目:
- POJ1463
- POJ3342
- Codeforces1084D
- Codeforces1118F1
- POJ1655
- POJ1463
题意:原题意是让你选择尽可能少的点,选择的这些点和它最临近点加起来可以涵盖所有点。换一种离散数学上的说法,就是给你一棵树(树是一种连通无回路的图),让你把这棵树变成一个二部图,然后求两部分哪个部分顶点。
思路:树(记忆)形(化)d(搜)p(索),从任一个点出发(我写的 任一个点出发的代码过不了 按题解改成从根节点出发就能过 咱也不知道为啥 咱也不敢问TAT),开始深搜,到叶节点为终止点,记录下是否选择开始回溯(注意叶节点是否选择在刚开始就初始化了),dp[i][0]表示i这个点不选,所需要选的最少数量,dp[i][1]表示这个点选,所需要选的最少数量。
注意,如果父节点被选,那子节点可以被选也可以不被选;如果父节点没被选,那子节点一定要选。可以和这篇博客的第二道题POJ3342对比一下,第二道也是父子节点有约束情况。
两道题具体不同:本题是要求要覆盖所有的结点,并且要尽可能少;而下面party那道题是要求尽可能多的人去,但是父节点去了子节点就不能去。
所以说:前者是儿子来约束父亲,如果父亲没选(照顾不到儿子),那孩子必须选,而父亲选了,孩子可以选也可以不选,即:
dp0+=dp[to][1];
dp1+=min(dp[to][1],dp[to][0]);
后者是父亲来约束儿子,如果父亲去了,儿子一定不去,而父亲不去,儿子可以去也可以不去,即:
dp1+=dp[to][0];
dp0+=max(dp[to][0],dp[to][1]);
两题看似差不多,但是有这样细微的差别,一定要搞明白!!!
AC代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define maxn 1600
using namespace std;
int n,cnt;
int dp[maxn][2],con[maxn],out[maxn],root[maxn]; //dp[x][0]代表
struct Side{
int y,next;
}side[maxn<<2];
int head[maxn];//记录第一个点连接的边的编号
void init(){
cnt=0;
memset(dp,0,sizeof(dp));
memset(con,0,sizeof(con));
memset(side,0,sizeof(side));
memset(head,-1,sizeof(head));//初始化为-1!!!
memset(out,0,sizeof(out));
memset(root,0,sizeof(root));
}
void add(int x,int y){
cnt++;
side[cnt].y=y;
side[cnt].next=head[x];
head[x]=cnt;
}
void tree_dp(int rt){
int dp0=0,dp1=0;
con[rt]=1;
if(!out[rt]) return;//若是叶节点 就返回
for(int i=head[rt];i!=-1;i=side[i].next){
int to=side[i].y;
if(!con[to]){//若这个点没来过 防止倒搜
tree_dp(to);
dp0+=dp[to][1];
dp1+=min(dp[to][1],dp[to][0]);
}
}
dp[rt][0]=dp0;
dp[rt][1]=dp1+1;
}
int main() {
//freopen("stdin.txt","r",stdin);
while(scanf("%d",&n)!=EOF) {
init();
char c;
for(int i=0; i<n; i++) {//从0开始!!!
int sta,num;//起始点编号和边数
scanf("%d%c%c%d%c",&sta,&c,&c,&num,&c);
int end;//终点编号
for(int j=1; j<=num; j++) {
scanf("%d",&end);
add(sta,end);
add(end,sta);
out[sta]++;
root[end]=1;
}
}
int rt;
for(int i=0;i<n;i++){
if(!root[i]) rt=i;
if(!out[i]){//叶子节点
dp[i][1]=1;
dp[i][0]=0;
}
}
tree_dp(rt);
int minn=min(dp[rt][0],dp[rt][1]);
printf("%d\n",minn);//求个最小值!!!至少嘛!
}
return 0;
}
- POJ3342
题意:上一题题解有提到,即公司组织参加舞会,公司成员之间有隶属关系,隶属关系之间有约束,即相邻的两级,上级去,他的下级就不能去。即爹去,儿不能去,爹不去,儿可去可不去。
dp[i][0]代表不选i这个点时候子树所能去的最大人数,dp[i][1]表示选i这个点的时候子树所能去的最大人数。
这题多了一点小思维点就是判断方案是否重复。这样就需要多开一个unique数组。 开什么数组!都说了是个小思维。当然这个小思维我是没有想出来的,毕竟是个菜鸡儿。
判重复思路借鉴:https://blog.csdn.net/weixin_43179892/article/details/84076430
注意先别急着点链接!!!别点链接!!!看看下面我讲的,应该能看懂的!!!
方法很巧妙!下面说一下大体思路,(我猜那人的)大题思路是根据这个核心代码片段来的:
dp1+=dp[to][0];
dp0+=max(dp[to][0],dp[to][1]);
首先,爹去,一定选择儿子不去,其次,爹若不去,用u代表儿子,一定选max(dp[u][0],dp[u][1])里面较大的。
所以我们遍历所有点,找到一个点u使得dp[u][0]>=dp[u][1],这样不论v的爹去不去,dp[u][0]都是被选的,也就是说一定是不选u的。在这种情况下(u不选,即u的孩子都是可选可不选),那么再看看u的子节点v,若出现dp[v][1]和dp[v][0]相等的情况,那就说明有重复的情况啦!可以根据下面的核心代码想一下。
最后因为没有判断过大boss,所以这里需要特判一下!
关键判重代码:
int flag=0;//0无重复
for(int i=1;i<=n;i++){
if(dp[i][0]>=dp[i][1]){//不论i的父节点选不选 都会选dp[i][0]
for(int j=head[i];j!=-1;j=side[j].next){//又写成i了!!!
int to=side[j].y;//写成i了zz。。。
if(dp[to][0]==dp[to][1]){
flag=1;break;//有重复的
}
}
}
if(flag) break;
}
if(dp[1][0]==dp[1][1]) flag=1; //boss这里需要特判!
AC代码:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<map>
#include<string>
#define ll long long
#define maxn 220
using namespace std;
int n,num,cnt;//num人数,cnt边数
int dp[maxn][2];
map<string,int> mp;
struct Side{
int y,next;
}side[maxn<<1];
int head[maxn];
void init(){
num=0,cnt=0;
mp.clear();
memset(dp,0,sizeof(dp));
memset(side,0,sizeof(side));
memset(head,-1,sizeof(head));
}
void add(int x,int y){
cnt++;
side[cnt].y=y;
side[cnt].next=head[x];
head[x]=cnt;
}
void tree_dp(int rt){
if(head[rt]==-1) return;
int dp0=0,dp1=0;
for(int i=head[rt];i!=-1;i=side[i].next){
int to=side[i].y;
tree_dp(to);
dp1+=dp[to][0];
dp0+=max(dp[to][0],dp[to][1]);
}
dp[rt][0]=dp0;
dp[rt][1]=dp1+1;
}
int main(){
while(scanf("%d",&n)!=EOF){
if(n==0) break;
init();
string boss,s,f;//这里尝试过用字符串而不是string 但是WA了。。。按题解来了就。。。
cin>>boss;
mp[boss]=++num;
for(int i=2;i<=n;i++){
cin>>s>>f;
if(mp.find(s)==mp.end()) mp[s]=++num;//记住用法
if(mp.find(f)==mp.end()) mp[f]=++num;
add(mp[f],mp[s]);
}
for(int i=1;i<=n;i++){
if(head[i]==-1){//若是叶节点
dp[i][0]=0;
dp[i][1]=1;
}
}
tree_dp(1);//搜一次 这里不用找root了。。。
int maxx=max(dp[1][0],dp[1][1]);
printf("%d ",maxx);
int flag=0;//0无重复
for(int i=1;i<=n;i++){
if(dp[i][0]>=dp[i][1]){//不论i的父节点选不选 都会选i
for(int j=head[i];j!=-1;j=side[j].next){//又写成i了!!!
int to=side[j].y;//写成i了zz。。。
if(dp[to][0]==dp[to][1]){
flag=1;break;//有重复的
}
}
}
if(flag) break;
}
if(dp[1][0]==dp[1][1]) flag=1;
if(flag) printf("No\n");
else printf("Yes\n");
}
return 0;
}
- Codeforces1084D
思路及核心思想转载自:https://blog.csdn.net/mmk27_word/article/details/85001714
题意:给你一棵树,每个点都有权值,每条边也有权值,问你求出一条路,使得:路中所有点的权值减去边的权值获得最大。
思路:树状dp,dp[i] 保存从i 节点开始走向子节点得到的最大价值
核心代码:这个核心代码tql!!!膜!
dfs(v,u);
ans=max(ans,dp[u]+dp[v]-val);
dp[u]=max(dp[u],dp[v]+a[u]-val);
现在来解释一下这段核心代码什么意思:
首先v是u的子节点,搜索了v之后,就知道了v的信息(往叶节点方向走(注意只是叶节点方向并不一定走到叶节点)得到的最大的那条路dp[v],以及ans:由v的子树们组成的最长的路)。
这样就可以用dp[v]来更新ans(此时ans为由u的子树们组成的最长的路 ans=max(ans,dp[u]+dp[v]-val);)与dp[u](即u往叶节点走的最大路dp[u]=max(dp[u],dp[v]+a[u]-val);)。
这种非常巧妙的代码是非常棒的!再次膜大佬。
下面是AC代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define cl(a,b) memset(a,b,sizeof(a))
#define maxn 300100
using namespace std;
ll n,cnt,ans;//cnt总边数(双向
//color记录结点颜色 num1 num2 记录以该节点为根节点
ll a[maxn],head[maxn];
ll dp[maxn];
struct Side{
ll y,val,next;
}side[maxn<<1];
void init(){
cnt=0;
cl(a,0);
cl(head,-1);
}
void add(ll x,ll y,ll val){
cnt++;
ans=0;
side[cnt].y=y;
side[cnt].val=val;
side[cnt].next=head[x];
head[x]=cnt;
}
void dfs(ll u,ll pre){
dp[u]=a[u];
ans=max(ans,dp[u]);
for(ll i=head[u];i!=-1;i=side[i].next){
ll v=side[i].y,val=side[i].val;
if(v==pre) continue;
dfs(v,u);//这里搜完所以dp[v]最大值已经求出来了
ans=max(ans,dp[u]+dp[v]-val);//核心代码
dp[u]=max(dp[u],dp[v]+a[u]-val);
}
}
int main(){
cin>>n;
init();
for(ll i=1;i<=n;i++){
scanf("%lld",&a[i]);
}
ll u,v,val;
for(ll i=1;i<n;i++){
scanf("%lld%lld%lld",&u,&v,&val);
add(u,v,val);
add(v,u,val);
}
dfs(1,-1);//搜第一个结点 默认第一个结点是根树
printf("%lld",ans);
return 0;
}
- CodeForces 1118F1
题意:有一棵树,上面各个节点是红色(标号1)或者蓝色(标号2)或者无色(标号0)。若去掉某一条边,得到的两个子树都不同时拥有红、蓝两种颜色,那这条边就叫“好边”。问你一共有多少条“好边”。
思路:把任意一个节点当做根,深搜遍历所有的子树,统计所有子树红蓝各多少个点。然后深搜结束。
下面是判断“好边”的核心代码:
num1、num2代表各个节点对应子树红色蓝色节点数量。
int ans=0;
for(int i=2;i<=n;i++){//搜每个结点 每个结点都代表这个子树
if(num1[i]&&num2[i]) continue;//若当前这个子树 红蓝都有就不行
int x=num1[1]-num1[i];
int y=num2[1]-num2[i];
if(x==0||y==0) ans++;
}
有人可能问,我们不是找符合要求的边吗?为什么要遍历点呢?
试想,若有n个点,则边数一定是n-1,若除了根节点,那正好每个节点对应一条边(树状图上是它上面那条边),所以遍历节点就是遍历边啦!
所以,我们可以得到,若某个节点对应的子树红蓝色都有,那这个节点对应的上面那条边就不行咯,不然生成的下面这个子树是两种颜色都有的。另外,若根节点对应子树(就是最大的树)的某种颜色数量和你遍历的当前子树相等(当然是在当前子树只有一种颜色的前提下),那说明当前子树涵盖了这种颜色的所有节点,所以当前子树的根节点对应的边可以,ans++。
下面是AC代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define cl(a,b) memset(a,b,sizeof(a))
#define maxn 300100
using namespace std;
int n,cnt;//cnt总边数(双向
//color记录结点颜色 num1 num2 记录以该节点为根节点
int color[maxn],head[maxn],num1[maxn],num2[maxn];
struct Side{
int y,next;
}side[maxn<<1];
void init(){
cnt=0;
cl(color,0);
cl(head,-1);
cl(num1,0);
cl(num2,0);
}
void add(int x,int y){
cnt++;
side[cnt].y=y;
side[cnt].next=head[x];
head[x]=cnt;
}
void dfs(int u,int pre){
if(color[u]==1) num1[u]++;
if(color[u]==2) num2[u]++;
for(int i=head[u];i!=-1;i=side[i].next){
int v=side[i].y;
if(v==pre) continue;
dfs(v,u);
num1[u]+=num1[v];
num2[u]+=num2[v];
}
}
int main(){
cin>>n;
init();
for(int i=1;i<=n;i++){
scanf("%d",&color[i]);
}
int u,v;
for(int i=1;i<n;i++){
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
dfs(1,-1);//搜第一个结点 默认第一个结点是根树
int ans=0;
for(int i=2;i<=n;i++){//搜每个结点 每个结点都代表这个子树
if(num1[i]&&num2[i]) continue;//若当前这个子树 红蓝都有就不行
int x=num1[1]-num1[i];
int y=num2[1]-num2[i];
if(x==0||y==0) ans++;
}
printf("%d",ans);
return 0;
}
- POJ1635
题意:树的重心模板。
思路:树形dp,这里就直接贴一下用了大神核心代码的代码。具体思路在注释里面。
注意这里我们需要维护每个节点的两个值:dpnum和dpmax。前者指的是该节点所对应的子树的所有节点的数量,后者指的是若该点被删除,剩下的最大子树节点的数量。这很重要!
首先,深搜遍历当前点u的所有终点的v时候,每搜完一个v就代表已经知道了v的dpnum和dpmax。然后累加dpnum[v]求当前节点的dpnum。然后每搜一个v就判断一下dpmax[u]能否被dpnum[v]更新。然后搜完这个点的所有边对应的终点,再用dpnum[u]来看看父亲那边的子树能否把自己的max更新,(n-dpnum[u])就是父亲那边的子树大小,可以用来更新dpmax[u]。
AC代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#define ll long long
#define inf 0x3f3f3f3f
#define cl(a,b) memset(a,b,sizeof(a))
#define maxn 20004
using namespace std;
int n,cnt,head[maxn],dpnum[maxn],dpmax[maxn];//dpnum表示该点子树结点之和 dpmax表示该点若被删除后 剩下的最大子树
struct Side{
int y,next;
}side[maxn<<1];
void init(){
cnt=0;
for(int i=1;i<=n;i++){
dpnum[i]=1;
dpmax[i]=-1;
head[i]=-1;
}
}
void add(int x,int y){
cnt++;
side[cnt].y=y;
side[cnt].next=head[x];
head[x]=cnt;
}
int dfs(int u,int pre){
for(int i=head[u];i!=-1;i=side[i].next){
int v=side[i].y;
if(v==pre) continue;
dpnum[u]+=dfs(v,u);//这一步是为了求一下节点u对应的子树节点数量
dpmax[u]=max(dpmax[u],dpnum[v]);//看看u的子树最大支能不能被新探索出的子树更新
}
dpmax[u]=max(n-dpnum[u],dpmax[u]);//再看看u的父结点部分会不会被更新(意思是若dpnum[u]小,可能把dpmax更新(比如叶结点))
return dpnum[u];//注意 返回的是结点数量 而不是子树最大值!!!
}
int main(){
int T;
cin>>T;
while(T--){
cin>>n;
init();
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
dfs(1,-1);
int minn=inf,i0;
for(int i=1;i<=n;i++){
if(minn>dpmax[i]){
minn=dpmax[i];
i0=i;
}
}
printf("%d %d\n",i0,minn);
}
return 0;
}