树形DP整理小结

来源:互联网 发布:linux socket调试工具 编辑:程序博客网 时间:2024/05/18 00:49

树形DP:

在树上进行dp,树是递归的定义的,所以树形dp也是递归的求解,一般而言,dp[node]表示的是以node为根的子树能得到的最优解。

一般而言,dp[node]需要从node的子结点进行状态转移,

树形dp又常常和背包结合起来,因为dp[node]的状态是由它的儿子转移而来。

我们常常可以将node的n个儿子看做n个物品,要如何对这n个物品抉择得到最优的dp[node]就常用到背包的思想。

当然,常常dp不止node这一维,至于第二维保存什么 则根据题目来,

一般都是题目给出的限制条件作为dp数组的第二维,然后根据题意保存好状态,写出状态转移方程,

然后递归的求解dp[root]

还有,树形DP还涉及到建图的问题,如果题目能很清晰的输入一个树最好

用vector<int>s[N],s[i]保存节点i的所有儿子

而如果没有的话,保存之后的s会出现j是i的儿子同时i也是j的儿子的情况,这样的情况只需要在递归的过程中

对于当前节点的父亲做标记,枚举其儿子的时候跳过父亲即可(例如下面的第1题)

Problems:

前五题都是二维的dp,需要想好状态转移方程,且大多是树上的背包问题后五题都是一维的dp,在树上删点或者删边的问题,相对而言后五题更简单不过第10题给出的图不是树,需要tarjan缩点处理成一棵树

1.POJ 2486 Apple Tree

dp[i][j][0]表示从i节点出发最后回到i节点花费最多j步能获得的最大值, 
dp[i][j][1]表示从j节点出发最后不回到i节点花费最多j步能获得的最大值 
dp[root][j][0] = max(dp[root][j-k][0] + dp[son][k-2][0])
//root->son 和 son->root 共花费2步,j-k是在其他儿子花费的步数
dp[root][j][1] = max(dp[root][j-k][0] + dp[son][k-1][1])
dp[root][j][1] = max(dp[root][j-k][1] + dp[son][k-2][0])
//只需要回到root一次,一种情况是先走其他儿子回到root再走son,
//另一种情况是先走son这个儿子并回到root再去走其他儿子 

#define mem(a,x) memset(a,x,sizeof(a))#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<queue>#include<set>#include<stack>#include<cmath>#include<map>#include<stdlib.h>#include<cctype>#include<string>#define Sint(n) scanf("%d",&n)#define Sll(n) scanf("%I64d",&n)#define Schar(n) scanf("%c",&n)#define Sint2(x,y) scanf("%d %d",&x,&y)#define Sll2(x,y) scanf("%I64d %I64d",&x,&y)#define Pint(x) printf("%d",x)#define Pllc(x,c) printf("%I64d%c",x,c)#define Pintc(x,c) printf("%d%c",x,c)using namespace std;typedef long long ll;int dp[111][222][2];/*dp[i][j][0]表示从i节点出发最后回到i节点花费最多j步能获得的最大值, dp[i][j][1]表示从j节点出发最后不回到i节点花费最多j步能获得的最大值 dp[root][j][0] = max(dp[root][j-k][0] + dp[son][k-2][0])//root->son 和 son->root 共花费2步,j-k是在其他儿子花费的步数dp[root][j][1] = max(dp[root][j-k][0] + dp[son][k-1][1])dp[root][j][1] = max(dp[root][j-k][1] + dp[son][k-2][0])//只需要回到root一次,一种情况是先走其他儿子回到root再走son,//另一种情况是先走son这个儿子并回到root再去走其他儿子 */vector<int>s[111];int k;void treedp(int node,int fa){for (int i = 0;i < s[node].size();++i)//从节点node出发 {int &son = s[node][i];if (son == fa) continue;treedp(son,node);for (int j = k;j >= 0;--j)//枚举node节点所有可能花费的步数 {for (int ot = 0;ot <= j;++ot)//枚举在son这个儿子上花费的步数{if (ot-2>=0) dp[node][j][0] = max(dp[node][j][0],dp[node][j-ot][0]+dp[son][ot-2][0]);if (ot-1>=0) dp[node][j][1] = max(dp[node][j][1],dp[node][j-ot][0]+dp[son][ot-1][1]);if (ot-2>=0) dp[node][j][1] = max(dp[node][j][1],dp[node][j-ot][1]+dp[son][ot-2][0]);} } }} int main(){    int n;    while (Sint2(n,k) == 2)    {    mem(dp,0);mem(s,0);    for (int i = 1,v;i <= n;++i)    {    Sint(v);for (int j = 0;j <= k;++j){dp[i][j][0] = dp[i][j][1] = v;} }for (int i = 1,x,y;i < n;++i){Sint2(x,y);s[x].push_back(y);s[y].push_back(x);}treedp(1,0);//根节点1木有爸爸Pintc(max(dp[1][k][0],dp[1][k][1]),'\n'); }    return 0;}


2.POJ 1047 Rebuilding Roads

dp[i][j]表示对于节点i,要得到一颗j个节点的子树需要删除的最少边数

若保留儿子v dp[node][i] = dp[node][j] + dp[v][i-j]


#define mem(a,x) memset(a,x,sizeof(a))#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<queue>#include<set>#include<stack>#include<cmath>#include<map>#include<stdlib.h>#include<cctype>#include<string>#define Sint(n) scanf("%d",&n)#define Sll(n) scanf("%I64d",&n)#define Schar(n) scanf("%c",&n)#define Sint2(x,y) scanf("%d %d",&x,&y)#define Sll2(x,y) scanf("%I64d %I64d",&x,&y)#define Pint(x) printf("%d",x)#define Pllc(x,c) printf("%I64d%c",x,c)#define Pintc(x,c) printf("%d%c",x,c)using namespace std;typedef long long ll;const int inf = 0x3f3f3f3f;ll dp[170][170];//dp[i][j]表示对于节点i,要得到一颗j个节点的子树需要删除的最少边数/*若保留儿子v dp[node][i] = dp[node][j] + dp[v][i-j] */int f[170];vector<int>s[170]; int n,p,root;void treedp(int node){mem(dp[node],inf);dp[node][1] = 0;for (int k = 0;k < s[node].size();++k){int &v = s[node][k];treedp(v);for (int i = p;i >= 1;--i){dp[node][i]++;for (int j = 1;j < i;++j){dp[node][i] = min(dp[node][i],dp[node][j]+dp[v][i-j]);}}}}int main(){    while (Sint2(n,p) == 2)    {    mem(s,0);for (int i = 0;i <= n;++i) f[i] = i;    for (int i = 1,x,y;i < n;++i)    {    Sint2(x,y);    s[x].push_back(y);    f[y] = x;}for (int i = 1;i <= n;++i){if (f[i] == i){root = i;break;}}//cout<<root<<endl;treedp(root);ll ans = dp[root][p];for (int i = 1;i <= n;++i){ans = min(ans,dp[i][p] + 1);//选除根节点外的点还需要删除和父亲关联的边 }Pllc(ans,'\n');}    return 0;}


3.HDU 3586 Information Disturbing

题意:
给出N个节点,N-1条边的树,给出每条边权值w,现在要求切断其中一些边,使得任意一个叶子没有走到祖先(1)的路
给出m,要求切断的边的总权值小于等于m,求所有的方案中切断的最大的权最小的值
分析:
限制条件有3个:
1.每个叶子不能回到祖先
2.总花费不能超过m
3.求最大权的最小值
求最小值的话果断二分,枚举各个最大权,检查是否满足费用不超过m
其他就是切边,对于任意边,切or不切,决策果断用DP,又是在树上的DP,显然树形DP
dp[node]表示节点i切断与所有儿子的联系的最小花费,
切断与所有儿子的联系有2种情况:
1.直接切断 node 到 son的这条边 ,花费是 w(node,son)
2.儿子自身就与它自己的所有儿子切断了联系,花费是 dp[son]
状态转移:
if (w(node,son) <= limit) dp[node] += min(dp[son] , w(node,son)) //这条边可切断 else dp[node] += dp[son];//这条边不能切断
其中limit 是二分枚举的最大权,所以选择的w(node,son)不能超过这个limit

#define mem(a,x) memset(a,x,sizeof(a))#include<iostream>#include<cstdio>#include<cstring>#include<algorithm>#include<queue>#include<set>#include<stack>#include<cmath>#include<map>#include<stdlib.h>#include<cctype>#include<string>#define Sint(n) scanf("%d",&n)#define Sll(n) scanf("%I64d",&n)#define Schar(n) scanf("%c",&n)#define Sint2(x,y) scanf("%d %d",&x,&y)#define Sll2(x,y) scanf("%I64d %I64d",&x,&y)#define Pint(x) printf("%d",x)#define Pllc(x,c) printf("%I64d%c",x,c)#define Pintc(x,c) printf("%d%c",x,c)using namespace std;typedef long long ll;const int inf = 1000000 + 7;//开大了dp相加时会溢出 const int N = 1007;int dp[N];/*dp[i]表示对于节点i,切断与所有儿子的联系的最小费用 切断与儿子的联系有两种方法:1.直接切断 i - son 的边 花费 w(i,son)2.让儿子切断与儿子的儿子的联系,即儿子无法接收到信息,花费 dp[son]二分枚举 limit 找到满足费用小于等于m的最小limit if (w(i,son) <= limit) dp[i] += min(dp[son] , w(i,son)) //这条边可切断 else dp[i] += dp[son] */ int e[N][N];vector<int>s[N];int n,m;int limit;void treedp(int node,int fa){dp[node] = 0;bool leaf = 1;for (int i = 0;i < s[node].size();++i){int &son = s[node][i];if (son == fa) continue;leaf = 1;treedp(son,node);if (e[node][son] <= limit) dp[node] += min(dp[son],e[node][son]);else dp[node] += dp[son];}if (leaf) dp[node] = inf;//叶子}bool ok(){treedp(1,0);if (dp[1] > m) return 0;else return 1;}int finds(int l,int r){int ans = -1;while (l <= r) //二分求最小值 {int mid = (l+r)>>1;limit = mid;if (ok()){ans = limit;r = mid-1;}else l = mid + 1;}return ans;}int main(){    while (scanf("%d %d",&n,&m),m+n)    {    mem(s,0);mem(e,0);     int l = 1,r = 0;    for (int i = 1,u,v,w;i < n;++i)    {    scanf("%d %d %d",&u,&v,&w);    e[u][v] = e[v][u] = w;    s[u].push_back(v);    s[v].push_back(u);    r = max(r,w);}printf("%d\n",finds(l,r));}    return 0;}

4.POJ 3345 Bribing FIPA

是树形dp的感觉,但是不是一个完整的树啊,说不定还是个森林捏

其实可以假定一个根,将每个“森林”的根连到一个根上,然后为了保证选择的时候不选这个根,将这个虚拟的根的花费设为无穷

然后就可以用树形dp做了 
dp[node][i]表示以node为根的子树获得i张选票的最小花费 

也就是对于n个儿子,选择买到i张票的最小花费,也就是背包嘛
dp[node][i] =  min(dp[node][i-j] + dp[son][j])
对比:dp[node][i] = min(dp[node][i-c] + w) (花费j的体积得到dp[son][j]的价值)
不同之处在于这里花费的体积不是确定的,需要额外枚举 
背包: 

    for (k = 0; k < u[node].size(); k++)    {        son = u[node][k];        for (i = s[node]; i >= 0; i--) //当前状态的体积        {            for (j = 0; j <= i && j <= s[son]; j++) //枚举子状态的体积            {                dp[node][i] = min(dp[node][i],dp[node][i-j]+dp[son][j]);            }        }    }
其中  s[node]表示以node为根的树一共最多能买到的票数  也就是背包体积 

然后注意下输入就没问题了

#include<iostream>#include<cstdio>#include<cstring>#include<string>#include<map>#include<sstream>#include<vector>#include<algorithm>#define mem(a,x) memset(a,x,sizeof(a))using namespace std;typedef long long ll;/*dp[node][i]表示以node为根的子树获得i张选票的最小花费 也就是对于n个儿子,选择买到i张票的最小花费,也就是背包嘛dp[node][i] =  min(dp[node][i-j] + dp[son][j])对比:dp[node][i] = min(dp[node][i-c] + w) (花费j的体积得到dp[son][j]的价值)不同之处在于这里花费的体积不是确定的,需要额外枚举 背包: for (k=0;k<u[node].size();k++)    {        son=u[node][k];        for (i=s[node];i>=0;i--) //当前状态的体积         {        for (j=0;j<=i && j<=s[son];j++) //枚举子状态的体积         {        if (dp[node][i-j]+dp[son][j]<dp[node][i]) dp[node][i]=dp[node][i-j]+dp[son][j];} }     }     其中  s[node]表示以node为根的树一共最多能买到的票数  也就是背包体积 */const int inf = 0x3f3f3f3f;const int N = 204;int dp[N][N],s[N],cost[N],f[N]; char str[10000000];vector<int>u[N];//数组u[node]记录了节点 node 的所有儿子map<string,int>mp; int n,m;int dfs(int node){s[node] = 1;//它自身 for (int i = 0;i < u[node].size();++i){s[node] += dfs(u[node][i]);} for (int i = 0;i <= n;++i) dp[node][i] = inf;dp[node][0] = 0;dp[node][s[node]] = cost[node];for (int k = 0;k < u[node].size();++k){int son = u[node][k];for (int i = s[node];i >= 0;--i) //当前状态的体积 {for (int j = 0;j <= i&&j <= s[son];++j) //枚举子状态的体积 {dp[node][i] = min(dp[node][i],dp[node][i-j]+dp[son][j]);}}}return s[node];}int main(){while (gets(str)){if (str[0] == '#') break;sscanf(str,"%d%d",&n,&m);int id = 0;mem(u,0);mp.clear();for (int i = 1;i <= n;++i) f[i] = i;for (int i = 1;i <= n;++i){gets(str);stringstream ss(str);string name;ss>>name;if (mp[name]){ss>>cost[mp[name]];}else {id++;mp[name] = id;ss>>cost[id];}string sname;while (1){ss>>sname;if (ss.fail()) break;if (mp[sname]){u[mp[name]].push_back(mp[sname]);f[mp[sname]] = mp[name]; }else {++id;mp[sname] = id;u[mp[name]].push_back(id);f[mp[sname]] = mp[name]; }}}for (int i = 1;i <= n;++i){if (f[i] == i)//单个的点{u[0].push_back(i);} }cost[0] = inf;dfs(0);int ans = inf;for (int i = m;i <= n;++i){ans = min(ans,dp[0][i]);}printf("%d\n",ans);}return 0;}


5.HDU 1561 The more, The Better

和上面一样需要自己虚拟一个根,把节点0作为根节点,所以m要加一,根节点的价值设为0

然后构造一棵树,要打任意一个子结点必须先打下父节点

dp[node][i]表示以node为节点的子树攻克 i个城堡的最大收获 dp[node][i] = max { dp[node][i-j] + dp[son][j] } 

#include<iostream>#include<cstdio>#include<cstring>#include<string>#include<algorithm>#include<vector>#define mem(a,x) memset(a,x,sizeof(a))using namespace std;typedef long long ll;/*dp[node][i]表示以node为节点的子树攻克 i个城堡的最大收获 dp[node][i] = max { dp[node][i-j] + dp[son][j] } */const int N = 202;const int inf = 0x3f3f3f3f;int dp[N][N],w[N],f[N];vector<int>s[N];void dfs(int node,int m){if (m <= 0) return;dp[node][1] = w[node];//只打一个城堡就只能是父节点 for (int k = 0;k < s[node].size();++k){int son = s[node][k];dfs(son,m-1); for (int i = m+1;i > 1;--i){for (int j = 0;j < i;++j){dp[node][i] = max(dp[node][i],dp[node][i-j]+dp[son][j]);}}}}int main(){int n,m;while (scanf("%d%d",&n,&m) == 2&&(n||m)){m++;//节点0作为根 所以加一个节点0 w[0] = 0 mem(s,0);mem(dp,0);w[0] = 0;for (int i = 1;i <= n;++i) f[i] = i;for (int i = 1,a,b;i <= n;++i){scanf("%d%d",&a,&b);s[a].push_back(i);w[i] = b; }dfs(0,m);printf("%d\n",dp[0][m]);}return 0;}


6.POJ 1655 Balancing Act

dfs(node)找到以node为根的子树的节点个数
dp[node]记录每个节点的balance每个节点的balance在其儿子的树的节点个数和除dfs(node)以外的节点数中取最大值随便画个图画一画想清楚了就好了 

#include<iostream>#include<cstdio>#include<cstring>#include<string>#include<algorithm>#include<vector>#define mem(a,x) memset(a,x,sizeof(a))using namespace std;typedef long long ll;const int inf = 0x3f3f3f3f;const int  N = 20004;int dp[N];vector<int>s[N];int n;int dfs(int node,int fa){int ans = 1;//该节点 自身 int d = 0;for (int i = 0;i < s[node].size();++i){int son = s[node][i];if (son == fa) continue;int tmp = dfs(son,node);d = max(d,tmp);ans += tmp;  // 加上儿子的节点数 }dp[node] = max(d,n-ans);return ans;}int main(){int T;scanf("%d",&T);while (T--){scanf("%d",&n);mem(s,0);for (int i = 1,u,v;i < n;++i){scanf("%d%d",&u,&v);s[u].push_back(v);s[v].push_back(u);}int root = 1;dfs(root,-1);//随便找个根节点跑一遍即可 int ans = inf,node;for (int i = 1;i <= n;++i){if (ans > dp[i]){ans = dp[i];node = i;}}printf("%d %d\n",node,ans);} return 0;}

7.POJ 3107 Godfather

这题分明和上面一题是一样的意思嘛~~~

but 这题用vector不给过 TLE 了

but 这题时限2000ms vector TLE 改自己写的邻接表 532ms AC

所以 STL 是有多慢 ?!!!

另外 输入输出最好不要用 cin cout 神马的 。

#include<iostream>#include<cstdio>#include<cstring>#include<string>#include<vector>#include<algorithm>#define mem(a,x) memset(a,x,sizeof(a))using namespace std;typedef long long ll;const int inf = 0x3f3f3f3f;const int N =  50004;struct Node{int from,to,nx;}e[N<<1];int head[N];int tot;void AddEdge(int a,int b){e[tot].from = a;e[tot].to  = b;e[tot].nx = head[a];head[a] = tot++;}int dp[N];int n;int ans;int dfs(int node,int fa){int sum = 1, d = 0;for (int i = head[node];~i;i = e[i].nx){int son = e[i].to;if (son == fa) continue;int tmp = dfs(son,node);sum += tmp;d = max(d,tmp); }dp[node] = max(d,n-sum);ans = min(ans,dp[node]);return sum;}int main(){while (~scanf("%d",&n)){tot = 0;mem(head,-1);for (int i = 1,a,b;i < n;++i){scanf("%d %d",&a,&b); AddEdge(a,b);AddEdge(b,a);}ans = inf;dfs(1,-1);//随便找个点跑一遍 bool first = true;for (int i = 1;i <= n;++i){//cout<<i<<" : "<<dp[i]<<endl;if (dp[i] == ans){if (first) {printf("%d",i);first = false;}else printf(" %d",i);}}puts("");}return 0;}

8.POJ 2378 Tree Cutting

和上面一题一样的意思 一样的做法

#include<iostream>#include<cstdio>#include<cstring>#include<string>#include<vector>#include<algorithm>#define mem(a,x) memset(a,x,sizeof(a))using namespace std;typedef long long ll;const int inf = 0x3f3f3f3f;const int N = 10004;int dp[N];vector<int>s[N];int n;int dfs(int node,int fa){int ans = 1,d = 0;for (int i = 0;i < s[node].size();++i){int son = s[node][i];if (son == fa) continue;int tmp = dfs(son,node);ans += tmp;d = max(d,tmp);}dp[node] = max(d,n-ans);return ans;} int main(){while (~scanf("%d",&n)){mem(s,0);for (int i = 1,a,b;i < n;++i){scanf("%d %d",&a,&b);s[a].push_back(b);s[b].push_back(a);}dfs(1,-1);int half = n/2;for (int i = 1;i <= n;++i){if (dp[i] <= half){printf("%d\n",i);}}} return 0;}


9.POJ 3140 Contestants Division

和上面一题差不多的意思,不过上面的题都是删点,这个是删边,
差不多的做法,数据大要开 long long

#include<iostream>#include<cstdio>#include<cstring>#include<string>#include<vector>#include<algorithm>#include<stdlib.h>#define mem(a,x) memset(a,x,sizeof(a))using namespace std;typedef long long ll;const ll inf = 1LL<<60;const int N = 100004;vector<int>s[N];ll w[N];ll sum;int n,m;ll res;ll dfs(int node,int fa){ll ans = w[node];for (int i = 0;i < s[node].size();++i){int son = s[node][i];if (son == fa) continue;ll tmp = dfs(son,node);ans += tmp;}ll tmp = sum-2LL*ans;if (tmp < 0) tmp = - tmp;res = min(res,tmp);return ans;}int main(){int kas = 0;while (scanf("%d %d",&n,&m)&&(n||m)){sum = 0;for(int i = 1;i <= n;sum += w[i],++i) scanf("%lld",w+i);mem(s,0);for (int a,b;m--;){scanf("%d %d",&a,&b);s[a].push_back(b);s[b].push_back(a);}res = inf;dfs(1,-1);printf("Case %d: %lld\n",++kas,res);}return 0;}



10.HDU 2242 考研路茫茫——空调教室

和上面一题一样的意思,不同的是,这个题的输入不一定是一棵树,需要处理

虽说是树形DP的题,但实际上树形DP的部分很简单,难在 缩点 

因为没说输入的图是一棵树,所以会有各种乱七八糟的图,所以用 tarjan 缩点

作为一只连tarjan都不会的渣渣,花了大半天时间学习tarjan算法(似懂非懂不清不楚),

然后直接 借鉴(抄袭)了别人代码的 tarjan 算法写的 

#include<iostream>#include<cstdio>#include<cstring>#include<string>#include<stack>#include<vector>#include<algorithm>#define mem(a,x) memset(a,x,sizeof(a))using namespace std;typedef long long ll;const int inf = 0x3f3f3f3f;const int N = 10004;const int M = 20004;int n,m;int w[N], w1[N];int sum;int tot;struct Tarjan{struct node{int from,to,nx;}e[2*M];int head[N];int tot,cnt,ssc;//边 时间序号 双连通分量个数 int dfn[N],low[N],belong[N];//dfs序  最早dfs序 强连通分量的编号 bool vis[N];//访问标记 stack<int>s;// 用栈的好处在于使双联通分量的标号连续void init(){mem(head,-1);tot = cnt = ssc = 0;while (!s.empty()) s.pop();mem(vis,0);}void AddEdge(int u,int v){e[tot].from = u;e[tot].to = v;e[tot].nx = head[u];head[u] = tot++;}void tarjan(int u,int fa) //tarjan找双连通分量并进行缩点{dfn[u] = low[u] = cnt++;//时间vis[u] = 1;s.push(u);bool flag = 0;//考虑重边 int v;for (int j = head[u];~j;j = e[j].nx){v = e[j].to;if (v==fa&&!flag){flag = 1;continue;}if (!vis[v]) tarjan(v,u);low[u] = min(low[u],low[v]);} if (dfn[u] == low[u]){ssc++;do{v = s.top();s.pop();belong[v] = ssc;w[ssc] += w1[v];//权值在缩点时加到一个点上 }while (v != u);} } }tar;vector<int>s[N];void DAG()//get一个无环图{tot = 0;mem(s,0);for (int i = 0,a,b;i < tar.tot;++i){a = tar.e[i].from;b = tar.e[i].to;if (tar.belong[a] != tar.belong[b]) //两个点不是强连通的{s[tar.belong[a]].push_back(tar.belong[b]);} }} int ans;int dfs(int node,int fa){int num = w[node];for (int i = 0;i < s[node].size();++i){int son = s[node][i];if (son == fa) continue;num += dfs(son,node);}ans = min(ans,abs(num-(sum-num)));//分成两部分  一部分是num  另一部分是sum - num ans 是最小差值return num; }int main(){while (~scanf("%d %d",&n,&m)){tar.init();sum = 0;for (int i = 0;i < n;sum+=w1[i],++i) scanf("%d",w1+i);for (int a,b;m--;){scanf("%d %d",&a,&b);tar.AddEdge(a,b);tar.AddEdge(b,a);}mem(w,0);tar.tarjan(0,0);if (tar.ssc == 1) //只有一个双连通分量表示整个图都是双连通的 {puts("impossible");continue;}DAG();ans = inf;dfs(1,0);printf("%d\n",ans);}return 0;}
0 0
原创粉丝点击