自己做了动态规划的题目已经有了一个月,但是成效甚微,所以来总结一下动态规划,希望自己能够温故知新。这个博客是关于树形dp的,动态规划的一类题目。
首先从最简单的树形DP入手,树形DP顾名思义就是一棵树和动态规划结合起来,我做了7,8题树形DP,目前为止发现树形DP的代码样式都是差不多,都在dfs树的过程中进行DP。
首先看一道简单的入门题目
题意就是在一棵树中,选取一些结点每个结点都可以监管者连接自己的一条边,问最少选取多少个结点可以让所有边都被监管起来。
思路:1:结点状态可以分为取和不取,所以用二维数组表示结点的状态。2:如果当前结点选取了,子结点可以选取也可以不选取,但是如果当前结点没有选取,那么子节点必须选取。
状态转移方程见代码里
#include <iostream>
#include <algorithm>
#include <string.h>
#include <math.h>
#include <stdlib.h>
using namespace std;
#define MAX 1500
int n;
int root;
int tot;
struct Node
{
int value;
int next;
}edge[MAX*2+5];
int head[MAX+5];
int dp[MAX+5][2];
int vis[MAX+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
dp[root][0]=0;
dp[root][1]=1;
vis[root]=1;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
if(!vis[k])
{
dfs(k);
//状态转移方程
dp[root][0]+=dp[k][1];
dp[root][1]+=min(dp[k][0],dp[k][1]);
}
}
}
int main()
{
int a,b;
int m;
while(scanf("%d",&n)!=EOF)
{
tot=0;
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
for(int i=0;i<n;i++)
{
scanf("%d:(%d)",&a,&m);
if(i==0) root=a;
for(int j=0;j<m;j++)
{
scanf("%d",&b);
add(a,b);
add(b,a);
}
}
dfs(root);
printf("%d\n",min(dp[root][0],dp[root][1]));
}
return 0;
}
这是一道入门题目,那么再看一到和它非常类似的题目,只是稍微变了一下
状态转移方程见代码里
(http://acm.hdu.edu.cn/showproblem.php?pid=1520)
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h>
using namespace std;
#define MAX 6000
struct Node
{
int value;
int next;
}edge[MAX*2+5];
int tag[MAX+5];
int dp[MAX+5][2];
int n;
int root;
int tot;
int rat[MAX+5];
int head[MAX+5];
int vis[MAX+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
dp[root][1]=rat[root];
dp[root][0]=0;
vis[root]=1;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
if(!vis[k])
{
dfs(k);
//状态转移方程
dp[root][1]+=dp[k][0];
dp[root][0]+=max(dp[k][0],dp[k][1]);
}
}
}
int main()
{
int a,b;
while(scanf("%d",&n)!=EOF)
{
tot=0;
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
memset(tag,0,sizeof(tag));
for(int i=1;i<=n;i++)
{
scanf("%d",&rat[i]);
}
scanf("%d%d",&a,&b);
while(a!=0&&b!=0)
{
tag[a]=1;
add(b,a);
add(a,b);
scanf("%d%d",&a,&b);
}
for(int i=1;i<=n;i++)
{
if(tag[i]==0)
{
root=i;
break;
}
}
dfs(1);
printf("%d\n",max(dp[1][0],dp[1][1]));
}
return 0;
}
这两道题目可以说一个类型的,都是在树上选取一些结点,以获得最优值,限制条件就是子节点和父节点的关系,二者不可以都选取,或者不可以都不选取,所以代码几乎都一样。
接下来看一道比前面难一点的题目
(http://acm.hdu.edu.cn/showproblem.php?pid=1561)
题意:有n个城堡形成一棵树,你只能攻打m个城堡,每个城堡里都有相应的宝物,要攻克这些城堡必须先攻克其他某一个特定的城堡,就是先攻克根,才可以往下攻击子树,问可以获得最大的宝物。
思路:这道题目和前面不同的是,有两个限制条件,一个是必学攻克父节点才能去攻打子节点,另一个是只能攻克m个城堡。这里可以看出树形dp和依赖背包是不是有一点差不多。其实解决依赖背包问题,就是用树形dp的思路,先解决子树的最优解,然后才能得到根的最优解。过程是从下往上的,而树的DFS就是先搜索到叶子节点,然后逐层往上,不断更新结点的最优值,最终保证根节点的值是最优的。状态转移方程见代码里
#include <iostream>
#include <string.h>
#include <algorithm>
#include <stdlib.h>
#include <math.h>
using namespace std;
#define MAX 200
struct Node
{
int value;
int next;
}edge[MAX+5];
int tot;
int head[MAX+5];
int n,m;
int num[MAX+5];
int dp[MAX+5][MAX+5];
int vis[MAX+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root,int tag)
{
vis[root]=1;
dp[root][1]=num[root];
for(int i=head[root];i!=-1;i=edge[i].next)
{
int t=edge[i].value;
if(!vis[t])
{
dfs(t,tag-1);
}
for(int j=tag;j>=1;j--)
{
for(int k=1;k<j;k++ )
{
//状态转移方程
dp[root][j]=max(dp[root][j],dp[root][j-k]+dp[t][k]);
}
}
}
}
int main()
{
int a;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0)
break;
tot=0;
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a,&num[i]);
add(a,i);
}
dfs(0,m+1);
printf("%d\n",dp[0][m+1]);
}
return 0;
}
代码看懂了也很好理解,接下来看这个问题的升级版(http://acm.hdu.edu.cn/showproblem.php?pid=1011)
题意:和上面一提差不多,变的是每个结点被攻克要求不同士兵数量
//
// main.cpp
// 树形DP2
//
// Created by 陈永康 on 16/1/3.
// Copyright (c) 2016年 陈永康. All rights reserved.
//
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h>
using namespace std;
int dp[105][105];//当前节点为i,小兵数为j得到的最大能量值
int n,m;
int bug[105];
int brain[105];
int head[105];
int tot;
int vis[105];
struct Node
{
int value;
int next;
}edge[105*2];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root,int tag)
{
vis[root]=1;
int term=(bug[root]+19)/20;
dp[root][term]=brain[root];
for(int i=head[root];i!=-1;i=edge[i].next)
{
int u=edge[i].value;
if(!vis[u])
{
dfs(u,tag-term);
for(int j=tag;j>=term;j--)
{
for(int k=1;j+k<=m;k++)
{
if(dp[u][k])
dp[root][j+k]=max(dp[root][j+k],dp[root][j]+dp[u][k]);
}
}
}
}
}
int main()
{
int x,y;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==-1&&m==-1)
break;
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
tot=0;
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&bug[i],&brain[i]);
}
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs(1,m);
if(m==0)
printf("0\n");
else
printf("%d\n",dp[1][m]);
}
}
值得注意的是还有另外一个ac代码,它和上面有一点不同
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h>
using namespace std;
int dp[105][105];//当前节点为i,小兵数为j得到的最大能量值
int n,m;
int bug[105];
int brain[105];
int head[105];
int tot;
int vis[105];
struct Node
{
int value;
int next;
}edge[105*2];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
vis[root]=1;
int term=(bug[root]+19)/20;
for(int i=term;i<=m;i++)
dp[root][i]=brain[root];
for(int i=head[root];i!=-1;i=edge[i].next)
{
int u=edge[i].value;
if(!vis[u])
{
dfs(u);
for(int j=m;j>=term;j--)
{
for(int k=1;j+k<=m;k++)
{
if(dp[u][k])
dp[root][j+k]=max(dp[root][j+k],dp[root][j]+dp[u][k]);
}
}
}
}
}
int main()
{
int x,y;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==-1&&m==-1)
break;
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
tot=0;
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&bug[i],&brain[i]);
}
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs(1);
if(m==0)
printf("0\n");
else
printf("%d\n",dp[1][m]);
}
}
这段代码并没有像前面那样,dfs的时候加上当前结点可以使用的小兵数量,似乎这样写下去并没有体现要攻打子节点必须攻克父节点这个限制条件啊,但这的确是对的。我的猜想是,这是两个不同的规划方式,第一个规划方式,你可以打一个dp二维数组的表,发现它是一个严格的从根节点往下,必须满足父节点被攻克,子节点才可以有最优值。而第二种方式,你可以发现,它的dp二维数组表,是以每一个结点为根节点得到这个结点的最优值。也就是说当我规划到这个节点,不用管它的父节点他就是根。所以回朔到根节点的时候,依然可以保证要攻打子节点必须攻克父节点这个限制条件,最终的值也是正确的。如果问题变成这样,你可以事先挑选某个结点作为根节点,往下去攻打。那么第二种解法就满足了
接下来,还是这个类型的题目但是又要更难一些,
(http://acm.hdu.edu.cn/showproblem.php?pid=4169)
题意:这个题目和前面的题目的区别就是,把要攻打子节点必须攻克父节点这个限制条件去掉了。加上一个,子节点和父节点不能共存。而且结点有1500000,所以你用dp数组表示结点的状态是肯定炸的。关键在于怎么设定dp数组,我感觉要不看博客accept这一题,是要对动态规划十分熟悉的。状态转移方程见代码,我把我的理解都写在注释里了
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h>
using namespace std;
#define MAX 1500000
int v[MAX+5];
int head[MAX+5];
int dp[305];
int n;
int tot;
int m;
struct Node
{
int value;
int next;
}edge[MAX+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
int dfs(int root)
{
int t=0;
int tag=0;
int cur[305];//这个数组是不断替换的
for(int i=0;i<=m;i++)
dp[i]=cur[i]=-1;
dp[0]=cur[0]=0;
for(int i=head[root];i!=-1;i=edge[i].next)
{
tag=1;
int k=edge[i].value;
//得到子树的状态数
int state=dfs(k);
for(int j=t;j>=0;j--)
{
for(int p=1;p<=state;p++)
{
if(p+j>m)
break;
cur[p+j]=max(cur[p+j],dp[p]+cur[j]);
}
}
t+=state;
}
if(!tag) t++;
cur[1]=max(cur[1],v[root]);
//把当前子树的状态保存到dp数组里,这里的dp数组表示当前这颗树的最优值,cur数组也是
for(int i=0;i<=m;i++)
dp[i]=cur[i];
return t;
}
int main()
{
int a;
int root;
while(scanf("%d%d",&n,&m)!=EOF)
{
memset(head,-1,sizeof(head));
tot=0;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a,&v[i]);
if(a==0)
root=i;
add(a,i);
}
dfs(root);
if(dp[m]==-1)
printf("impossible\n");
else
printf("%d\n",dp[m]);
}
return 0;
}
这道题目定义了两个状态数组,cur[],dp[]。dp[]里面保存的是最终的答案,cur[]是一个临时的数组表示子树的状态。
看另一道不同风格的题目吧
(http://acm.hdu.edu.cn/showproblem.php?pid=2412)
这道题目和前面一道非常相似,但是有一点不同,它要询问这个最优值是不是只有一种组合方式。这道题目的解决方法是,再开一个状态数组,表示当前的最优值是不是唯一,如果能想到这里,问题就好解决了
#include <iostream>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <algorithm>
#include <map>
#include <string>
using namespace std;
#define MAX 200
map<string,int>m;
int n;
string a,b;
int tot;
int head[MAX+5];
int dp[MAX+5][2];
int dup[MAX+5][2];
bool tag;
struct Node
{
int value;
int next;
}edge[MAX*2+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
dp[root][1]=1;
dp[root][0]=0;
dup[root][1]=1;
dup[root][0]=1;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
dfs(k);
dp[root][1]+=dp[k][0];
dp[root][0]+=max(dp[k][0],dp[k][1]);
if(dp[k][1]<dp[k][0]&&dup[k][0]==0)
dup[root][0]=0;
else if(dp[k][1]>dp[k][0]&&dup[k][1]==0)
dup[root][0]=0;
else if(dp[k][1]==dp[k][0])
dup[root][0]=0;
if(dup[k][0]==0)
dup[root][1]=0;
}
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
if(n==0)
break;
memset(head,-1,sizeof(head));
m.clear();
cin>>a;
m[a]=1;
int cnt=1;
tot=0;
for(int i=1;i<n;i++)
{
cin>>a>>b;
if(!m.count(a))
m[a]=++cnt;
if(!m.count(b))
m[b]=++cnt;
add(m[b],m[a]);
}
memset(dp,0,sizeof(dp));
memset(dup,0,sizeof(dup));
dfs(1);
if(dp[1][1]>dp[1][0]&&dup[1][1]==1)
printf("%d Yes\n",dp[1][1]);
else if(dp[1][0]>dp[1][1]&&dup[1][0]==1)
printf("%d Yes\n",dp[1][0]);
else
printf("%d No\n",max(dp[1][1],dp[1][0]));
}
return 0;
}
前面说的都是在节点的值上面进行动态规划,下面看如果要在边上吗进行规划呢?
HDU-3452 Bonsai
题意就是给你一棵树,每个边都有权值,问你剪去那些边,可以让叶子节点不能达到根节点,且剪去边的权值和最小
思路:为了达到让叶子节点不能达到根节点的目的,你有两种选择要么剪去根节点和下一个子节点的边,要么不剪这条边,直接到子树里面取剪。状态转移方程也就清晰了,见代码里
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h>
using namespace std;
#define MAX 1000
struct Node
{
int value;
int next;
int weight;
}edge[MAX*2+5];
int tot;
int n,r;
int a,b,c;
int head[MAX+5];
int vis[MAX+5];
int dp[MAX+5];
void add(int x,int y,int w)
{
edge[tot].weight=w;
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
vis[root]=1;
int tag=0;
int sum;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
sum=edge[i].weight;
if(!vis[k])
{
tag=1;
dfs(k);
dp[root]+=min(sum,dp[k]);
}
}
if(tag==0)
dp[root]=sum;
}
int main()
{
while(scanf("%d%d",&n,&r)!=EOF)
{
if(n==0&&r==0)
break;
tot=0;
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
for(int i=0;i<n-1;i++)
{
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,c);
}
if(n==1)
printf("0\n");
else
{
dfs(r);
printf("%d\n",dp[r]);
}
}
return 0;
}
再看一道这道题目的升级版,题意一样,不同的就是问你的是最小限制,就是你选取剪去的边里面权值最大的。这道题目的思路就是树形dp加二分,状态转移方程稍微变一下,加上一个限制。二分其实就是枚举这个限制,二分更快一点而已。
题目要问的是最小的最大限制,必然二分答案。
HDU-3586 Information Disturbing
#include <iostream>
#include <string.h>
#include <algorithm>
#include <stdlib.h>
#include <math.h>
using namespace std;
#define MAX 1000
int dp[MAX+5];
int head[MAX+5];
int vis[MAX+5];
int n,m;
int tot;
int maxin;
struct Node
{
int value;
int weight;
int next;
}edge[MAX*2+5];
void add(int x,int y,int weight)
{
edge[tot].weight=weight;
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root,int limit)
{
vis[root]=1;
bool tag=false;
int sum=0;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
sum=edge[i].weight;
if(!vis[k])
{
tag=true;
dfs(k,limit);
if(sum>limit)
dp[root]+=dp[k];
else
dp[root]+=min(dp[k],sum);
}
}
if(!tag)
dp[root]=1000010;
}
int main()
{
int x,y,z;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0)
break;
memset(head,-1,sizeof(head));
tot=0;maxin=0;
for(int i=1;i<n;i++)
{
scanf("%d%d%d",&x,&y,&z);
maxin=max(maxin,z);
add(x,y,z);
add(y,x,z);
}
int l=1;int r=maxin;int ans=-1;
while(l<=r)
{
int mid=(l+r)/2;
memset(vis,0,sizeof(vis));
memset(dp,0,sizeof(dp));
dfs(1,mid);
if(dp[1]<=m)
{
ans=mid;
r=mid-1;
}
else
l=mid+1;
}
printf("%d\n",ans);
}
return 0;
}