树形dp 入门
Posted snowy2002
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树形dp 入门相关的知识,希望对你有一定的参考价值。
今天学了树形dp,发现树形dp就是入门难一些,于是好心的我便立志要发一篇树形dp入门的博客了。
树形dp的概念什么的,相信大家都已经明白,这里就不再多说。直接上例题。
一、常规树形DP
题目描述
某大学有N个职员,编号为1~N。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数Ri,但是呢,如果某个职员的上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
这是一道非常经典的树形dp,首先分析题目,如果上司去了,那么他的所有下司都不能去;如果上司不去,那么他的所有下司去不去无所谓,但要取最大值。接下来就是状态转移方程:
我们设f[i][0]为i不去的情况,f[i][1]为i去的情况,那么
f[i][0]+=max(f[i的下司][1],f[i的下司][0]);
f[i][1]+=f[i的下司]][0];
初始化,如果i去,那么显然f[i][1]=i的快乐指数,如果i不去那么f[i][1]=0;
最后贴上代码
1 #include<iostream> 2 #include<cstring> 3 #include<cmath> 4 #include<string> 5 #include<cstdio> 6 using namespace std; 7 struct edge 8 { 9 int num,child[6001]; 10 }g[6001]; 11 int n; 12 int a[10000]; 13 int tree[10000]; 14 int f[6001][2]; 15 int aa,bb,root; 16 void dp(int t) 17 { 18 f[t][1]=a[t];//初始化 19 f[t][0]=0;//初始化 20 for(int i=1;i<=g[t].num;i++) 21 { 22 dp(g[t].child[i]); 23 f[t][0]+=max(f[g[t].child[i]][1],f[g[t].child[i]][0]);//状态转移 24 f[t][1]+=f[g[t].child[i]][0];//状态转移 25 } 26 } 27 int main() 28 { 29 cin>>n; 30 for(int i=1;i<=n;i++) 31 { 32 scanf("%d",&a[i]); 33 } 34 while(scanf("%d%d",&aa,&bb)) 35 { 36 if(aa==0&&bb==0) break; 37 g[bb].num++;//记录孩子结点的个数 38 g[bb].child[g[bb].num]=aa;//储存孩子结点 39 tree[aa]=bb;//记录父亲结点 40 } 41 root=1; 42 while(tree[root]) root++;//找出树根 43 dp(root);//从树根开始dp 44 int ans=max(f[root][1],f[root][0]); 45 cout<<ans; 46 return 0; 47 }
题目描述
Bob喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。
他要建立一个古城堡,城堡中的路形成一棵树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能了望到所有的路。
注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被了望到。
请你编一程序,给定一树,帮Bob计算出他需要放置最少的士兵.
和上个题一模一样,就是初始化不同,上个题每个人都有不同的权值,而这个题每个人的权值都为1;
状态转移一模一样 ;初始化,f[i][1]=1,f[i][1]=0;
最后上代码
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<string> 5 #include<cstring> 6 using namespace std; 7 struct edge 8 { 9 int num,child[1501]; 10 }g[1501]; 11 int a[1501]; 12 int f[1501][2]; 13 int x,y; 14 int n; 15 void dp(int t) 16 { 17 f[t][1]=1;//初始化为1 18 f[t][0]=0; 19 for(int i=1;i<=g[t].num;i++) 20 { 21 dp(g[t].child[i]); 22 f[t][0]+=f[g[t].child[i]][1];//状态转移 23 f[t][1]+=min(f[g[t].child[i]][1],f[g[t].child[i]][0]);//状态转移 24 } 25 } 26 int main() 27 { 28 cin>>n; 29 for(int i=1;i<=n;i++) 30 { 31 scanf("%d",&x); 32 scanf("%d",&g[x].num);//记录该结点的孩子个数 33 for(int j=1;j<=g[x].num;j++) 34 { 35 scanf("%d",&g[x].child[j]);//存储该结点的孩子 36 a[j]=i;//记录父亲 37 } 38 } 39 int root=0; 40 while(a[root]) root++;//找出根节点 41 dp(root); 42 int ans=min(f[root][1],f[root][0]); 43 cout<<ans; 44 return 0; 45 }
洛谷P2458 [SDOI2006]保安站岗
题目描述
五一来临,某地下超市为了便于疏通和指挥密集的人员和车辆,以免造成超市内的混乱和拥挤,准备临时从外单位调用部分保安来维持交通秩序。
已知整个地下超市的所有通道呈一棵树的形状;某些通道之间可以互相望见。总经理要求所有通道的每个端点(树的顶点)都要有人全天候看守,在不同的通道端点安排保安所需的费用不同。
一个保安一旦站在某个通道的其中一个端点,那么他除了能看守住他所站的那个端点,也能看到这个通道的另一个端点,所以一个保安可能同时能看守住多个端点(树的结点),因此没有必要在每个通道的端点都安排保安。
编程任务:
请你帮助超市经理策划安排,在能看守全部通道端点的前提下,使得花费的经费最少。
难度提升,如果看不懂可以先放一放
分析:
我们发现,要使所有点最终全部被覆盖,那么无非有3种状态:
(以下全部都是对于要覆盖任意一个以x为根的子树来说的)
(其中我们设y节点为y的儿子,fa为x的父亲)
1.x节点被自己覆盖,即选择x点来覆盖x点
2.x节点被儿子y覆盖,即选择y点来覆盖x点
3.x节点被父亲fa覆盖,即选择fa点来覆盖x点
借此三种状态,我们可以设f[x][0/1/2]为让以x为根的子树中的节点全部被覆盖,且x点的被覆盖情况为1/2/3时的最小代价
为了方便,我们不妨设这三种情况分别为:
1.f[x][0]---对应上面的1
2.f[x][1]---对应上面的2
3.f[x][2]---对应上面的3
既然是DP,总是有转移方程的,我们想一下dp方程要如何设计
设计状态转移方程:
(1):对应上面的1.
f[x][0]=∑ min(f[y][0],f[y][1],f[y][2]) + val[x]
其中val[x]是选择x点的代价
我们很容易想到,在节点x被选择之后,我们就可以无拘无束了(蛤?),也就是说对于x儿子节点y的状态可以不去考虑,因为x节点被选择之后y节点无论如何也会被覆盖到,所以我们在儿子y的所有状态里取min,累加起来就行了
(2):对应上面的3(先讲3,因为2比较难以理解,放到了后面)
f[x][2]=∑ min(f[y][0],f[y][1])
为什么3情况对应的转移方程要这样写呢?
我们不妨这样理解,对于x节点我们让它的父亲节点fa覆盖它,那么根据我们的状态设计,此时必须要满足以x的儿子y为根的子树之中所有点已经被覆盖
那么这时就转化为一个子问题,要让y子树满足条件,只有两种决策:要么y被y的儿子覆盖,要么被y自己覆盖(即选择y节点),只需要在y的这两种状态取min累加就可以了
(3):对应上面的2(DuangDuangDuang 敲黑板划重点啦)
f[x][1]=∑ min(f[y][0],f[y][1]),如果选择的全部都是f[y][1],要再加上min(f[y][0]-f[y][1])
到了这里,我们就要回顾一下我们设计的dp状态了:
设f[x][0/1/2]为让以x为根的子树中的节点全部被覆盖,且x点的被覆盖情况为1/2/3时的最小代价
先提示一下,如果你理解了下面,那么本题是很简单的。。如果你没理解,就返回到这里再看一遍吧,我就在这里等着你
对于此时的状态,f[x][1]代表对于节点x让x被自己的儿子覆盖,那么和分析(2)一样,都要先满足此时以y的子树已经满足了条件,才能进行转移,这就是前面那部分:∑ min(f[y][0],f[y][1])的来历,那么后面那一长串又是怎么回事呢?
我们可以这样理解,此时既然要保证x点是被自己的儿子覆盖的,那么如果此时y子树已经满足了全部被覆盖,但是y此时被覆盖的状态却是通过y节点自己的儿子达到的,那么x就没有被儿子y覆盖到,那么我们不妨推广一下,如果x所有的儿子y所做的决策都不是通过选择y点来满足条件,那么我们就必须要选择x的一个子节点y,其中y满足f[y][0]-f[y][1]最小,并把这个最小的差值累加到f[x][1]中去,这样才能使得x点被自己的儿子覆盖,状态f[x][1]也才能合理地得到转移
好了,如果你还是没有太懂这个(3)的设计过程,请你回到之前再仔细看几遍
如果你已经理解了上面,那么恭喜你这个题,你已经A掉了
因为转移方程既然有了,那么我们就只需要最后的答案了
由于题目中没有说这棵树的根节点是哪个,所以你可以默认1就是根,或者开一个数组在加边的时候记录一下每个点的入度,最后没有入度的点就是根(但好像没有区别,毕竟我A掉了)
最后答案即为min(f[root][0],f[root][1])
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<string> 5 #include<cstring> 6 using namespace std; 7 struct edge 8 { 9 int num,child[6001]; 10 }g[6001]; 11 int a[6001]; 12 int dp[6001][3]; 13 int root[6001]; 14 int n,aa; 15 void dfs(int x) 16 { 17 dp[x][0]=a[x]; 18 int cc=1000000000; 19 int num=0; 20 for(int i=1;i<=g[x].num;i++) 21 { 22 dfs(g[x].child[i]); 23 int t=min(dp[g[x].child[i]][0],dp[g[x].child[i]][1]); 24 dp[x][0]+=min(t,dp[g[x].child[i]][2]); 25 dp[x][1]+=t; 26 dp[x][2]+=t; 27 if(dp[g[x].child[i]][0]<dp[g[x].child[i]][1]) num++; 28 else cc=min(cc,dp[g[x].child[i]][0]-dp[g[x].child[i]][1]); 29 } 30 if(num==0) dp[x][1]+=cc; 31 } 32 int main() 33 { 34 scanf("%d",&n); 35 for(int i=1;i<=n;i++) 36 { 37 scanf("%d",&aa); 38 scanf("%d",&a[aa]); 39 scanf("%d",&g[aa].num); 40 for(int j=1;j<=g[aa].num;j++) 41 { 42 scanf("%d",&g[aa].child[j]); 43 root[g[aa].child[j]]=aa; 44 } 45 } 46 aa=1; 47 while(root[aa]) aa++; 48 dfs(aa); 49 cout<<min(dp[aa][0],dp[aa][1]); 50 return 0; 51 }
二、树形背包问题(在树上进行分组背包处理)
未完待续~~~
以上是关于树形dp 入门的主要内容,如果未能解决你的问题,请参考以下文章