学了树形DP的一点心得。。。虽然可能没啥意义。。。
树形dp大概就是在树上做dp吧。
遍历树上节点的同时,将一些数值通过转移方程传递给下(上)一个状态,大概分成两种状态:
1.由儿子向父亲(叶——》)根;
2.由父亲到儿子(根——》叶);
一般都是儿子到父亲(笔者是个蒟蒻,还没见过2。。。)
因为我们要处理一个节点时,要从父亲(或所有儿子)得到值,又要把值给所有儿子或父亲,
线性不太好实现,然而DFS的顺序刚好符合要求,于是
DFS顺序遍历是解决树上DP的常见方法
1 void tdp(int x,int fa){ 2 for(int i=head[x];i;i=q[i].nex){ 3 int j=q[i].to; 4 if(j!=fa){ 5 tdp(j,x); 6 //(转移) 7 } 8 } 9 }
链表的DFS
当然,这不是绝对的,线性也有符合情况的,因为拓扑序也代表了先后顺序,可以用拓扑序倒序来代替后续便利。
因为懒,所以没有代码。。。
对于树形DP我们一般先确定状态,大概是问什么设什么:
问题 A: 二叉苹果树
题目描述
#include<bits/stdc++.h> using namespace std; int head[150],val[150],n,m,tt=0,f[150][150],mp[105][105],l[105],r[105]; void biuldtree(int x){ for(int i=1;i<=n;i++){ if(mp[x][i]>=0){ l[x]=i;val[i]=mp[x][i]; mp[x][i]=mp[i][x]=-1; biuldtree(i); break; } } for(int i=1;i<=n;i++){ if(mp[x][i]>=0){ r[x]=i;val[i]=mp[x][i]; mp[x][i]=mp[i][x]=-1; biuldtree(i); break; } } return; } int tdp(int i,int j){ if(j==0)return 0; if(!l[i]&&!r[i])return val[i]; if(f[i][j])return f[i][j]; for(int k=0;k<=j-1;k++) f[i][j]=max(f[i][j],tdp(l[i],k)+tdp(r[i],j-k-1)+val[i]); return f[i][j]; } int main(){ cin>>n>>m; int a,b,k; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ mp[i][j]=-1; } } for(int i=1;i<n;i++){ scanf("%d%d%d",&a,&b,&k); mp[a][b]=mp[b][a]=k; } biuldtree(1); cout<<tdp(1,m+1); return 0; }
同样,有差不多一题:选课
问题 B: 选课
题目描述
大学实行学分制。每门课程都有一定的学分,学生只要选修了这门课并通过考核就能获得相应学分。学生最后的学分是他选修各门课的学分总和。 每个学生都要选择规定数量的课程。有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其他的一些课程基础上才能选修。例如《数据结构》必须在选修了《高级语言程序设计》后才能选修。我们称《高级语言程序设计》是《数据结构》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。为便于表述,每门课都有一个课号,课号依次为 1,2,3,……n。
#include<bits/stdc++.h> using namespace std; int head[5050],nex[5050],to[5050],val[5050],cnt=0,n,m; int dp[5050][5050]; void add(int a,int b){ nex[++cnt]=head[a]; to[cnt]=b; head[a]=cnt; } void tdp(int x){ for(int i=head[x];i;i=nex[i]){ tdp(to[i]); for(int t=m;t>=0;t--){ for(int j=t;j>=0;j--){ dp[x][t]=max(dp[x][t],dp[x][t-j]+dp[to[i]][j]); } } } if(x!=0){ for(int t=m;t>0;t--){ dp[x][t]=dp[x][t-1]+val[x]; } } } int main(){ cin>>n>>m; int a,b,c; for(int i=1;i<=n;i++){ cin>>a>>c; add(a,i); val[i]=c; } tdp(0); cout<<dp[0][m]<<" "; }
与二叉苹果树是一样的吧。。。不过根节点是个假的,没有权且选了没有意义,需要特判一下。
还有战略游戏和没有上司的舞会
这俩是几乎一毛一样的的。。。
一个max,一个min,相信大家都看得出来。。。
战略游戏
Bob 喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的方法。现在他有个问题。
现在他有座古城堡,古城堡的路形成一棵树。他要在这棵树的节点上放置最少数目的士兵,使得这些士兵能够瞭望到所有的路。
注意:某个士兵在一个节点上时,与该节点相连的所有边都将能被瞭望到。
请你编一个程序,给定一棵树,帮 Bob 计算出他最少要放置的士兵数。
没有上司的舞会
Ural 大学有 N 个职员,编号为 1~N。他们有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。每个职员有一个快乐指数。现在有个周年庆宴会,要求与会职员的快乐指数最大。但是,没有职员愿和直接上司一起与会。
#include<bits/stdc++.h> using namespace std; struct dd{ int nex,to,sum; }q[10050]; int cnt=0,val[6050],n,m,tt=0,head[6050],dp[6050][2]; bool pp[50500]; void add(int a,int b){ q[++cnt].nex=head[a]; q[cnt].to=b; head[a]=cnt; } void tdp(int x,int fa){ dp[x][1]=val[x]; //zlyx dp[x][1]=1; dp[x][0]=0; for(int i=head[x];i;i=q[i].nex){ int j=q[i].to; if(j!=fa){ tdp(j,x); dp[x][0]+=max(dp[j][1],dp[j][0]); dp[x][1]+=dp[j][0]; //战略游戏 //dp[x][0]+=dp[j][1]; // dp[x][1]+=min(dp[j][1],dp[j][0]); } } } int main(){ memset(dp,0,sizeof(dp)); cin>>n; int a,b,c; for(int i=1;i<=n;i++){ cin>>val[i]; } cin>>a>>b; while(a){ add(a,b); add(b,a); cin>>a>>b; } tdp(1,0); cout<<max(dp[1][1],dp[1][0]); //cout<<min(dp[1][1],dp[1][0]); return 0; }
因为没有上司的舞会所有点满足且只能不存在相邻的点被选,不存在先后,互不影响,所以不需要求根节点,因为
所有节点此时都是等价的。
而战略游戏则是所有路被看,也不存在先后,故把1做根就行。
太平王OYWB事件后,LA成了皇上特聘的御前一品侍卫。 皇宫以午门为起点,直到后宫嫔妃们的寝宫,呈一棵树的形状;有边直接相连的宫殿可以互相望见。大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。 可是LA手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。
编程任务:帮助LA布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。
这一题比较有趣,因为他是只有点被看到就行,
所以一个点就有三种情况,
f1:不选,且已经被儿子看到
f2:不选,还没有确定是否被看到
f3:选
易知,f2[i]的情况只需要Σmin(f2[j],f3[j]),因为此时不一定选,所以只需要求最小即可
而f1[i]则是取Σ f2[ j ] -min(f2[j],f3[j]),意为,在所有可能不选的最小方案中,找到一个代价最小的选择,保证满足每个点都有人看。
f3就喜闻乐见咯毕竟选了他,其他子节点选不选都ok咯f3[i]=Σmin{f1[j],f2[j],f3[j]};
代码
#include<bits/stdc++.h> using namespace std; typedef long long ll; struct dd{ int nex,to,sum; }q[13050]; int cnt=0,val[6050],n,m,tt=0,head[6050],dp[6050][3]; void add(int a,int b){ q[++cnt].nex=head[a]; q[cnt].to=b; head[a]=cnt; } void tdp(int x,int fa){ dp[x][1]=0;//不选X,且没人看X dp[x][2]=0;//不选x,且有人看X int tag;int ddp=0x3fffffff; for(int i=head[x];i;i=q[i].nex){ int j=q[i].to; if(j!=fa){ tdp(j,x); dp[x][0]+=min(dp[j][1],min(dp[j][2],dp[j][0])); dp[x][1]+=min(dp[j][0],dp[j][2]); dp[x][2]+=min(dp[j][0],dp[j][2]); ddp=min(ddp,dp[j][0]-min(dp[j][0],dp[j][2])); } } dp[x][2]+=ddp; dp[x][0]+=val[x];//选X } int main(){ memset(dp,0,sizeof(dp)); cin>>n; int a,b,c; for(int i=1;i<=n;i++){ cin>>a>>b>>c; val[a]=b; for(int j=1;j<=c;j++){ cin>>b; add(a,b); add(b,a); } } int i=1; tdp(1,0); cout<<min(dp[1][2],dp[1][0]); return 0; }
然后就这样了,
还有一些例题,大家可以自己思考一下。
偷天换日
神偷对艺术馆内的名画垂涎欲滴准备大捞一把。艺术馆由若干个展览厅和若干条走廊组成。每一条走廊的尽头不是通向一个展览厅,就是分为两个走廊。每个展览厅内都有若干幅画,每副画都有一个价值。经过走廊和偷画都是要耗费时间的。警察会在第n秒到达进口,在不被逮捕的情况下你最多能得到的价值。
加分二茬树(好像用区间。。)
设一个 n 个节点的二叉树 tree 的中序遍历为 (1,2,3,……,n),其中数字 1,2,3,……,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 d_i,tree及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree本身)的加分计算方法如下:
记 subtree的左子树加分为 l,右子树加分为 r,subtree 的根的分数为 a,则 subtree的加分为: l*r+a
若某个子树为空,规定其加分为 1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为 (1,2,3,……,n) 且加分最高的二叉树 tree。
要求输出:
-
- tree 的最高加分;
- tree的前序遍历。