详解树形DP
作者:zhxmdefj
前言
给定一颗有N个节点的树(一般是无根树,就有N-1条无向边),可以任选一个节点作为根节点
一般以节点从深到浅(子树从小到大)的顺序作为dp阶段顺序
dp的状态表示中,第一维通常是节点编号(节点编号代表了以该节点为根的子树)
对于每个节点x,先递归在它的每个子节点上进行dp,回溯时,从子节点向x进行状态转移
A - Anniversary part
N个员工,编号为1~N
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直属上司。现在有个宴会,宴会每邀请来一个员工 i 都会增加一定的快乐指数 Ri,但如果某个员工的直属上司来了,那么这个员工就不会来。计算邀请哪些员工可以使快乐指数最大,输出最大的快乐指数
设 dp [x] [0] 表示在 x 为根的子树中邀请部分员工,并且 x 不参加时,快乐指数总和的最大值,此时 x 子节点(直接下属)可以参加也可以不参加 (s表示x子节点)
设 dp [x] [1] 表示在 x 为根的子树中邀请部分员工,并且 x 参加时,快乐指数总和的最大值,此时 x 子节点(直接下属)都不能参加,H[x] 表示当前节点(x)的快乐指数 (s表示x子节点)
这个题给的是有根树,就可以从根节点开始,dp目标就是 max(F[root,0] , F[root,1]) 时间复杂度ON
#include <cstdio> #include <iostream> #include <cstring> #include <algorithm> #include <vector> using namespace std; vector<int> son[10010]; int dp[10010][2];//0不参加,1参加 int v[10010];//记录有没有父节点 int h[10010];//记录快乐指数 int n; void DFS(int x){ dp[x][0] = 0; dp[x][1] = h[x]; for (int i = 0; i < son[x].size(); i++) { int y = son[x][i]; DP(y); dp[x][0] += max(dp[y][0],dp[y][1]); dp[x][1] += dp[y][0]; } } int main(){ cin>>n; for (int i = 1; i <=n ; i++) scanf("%d",&h[i]); for (int i = 1; i <n ; i++) { int x,y; cin>>x>>y; v[x] = 1;//x有爸爸 son[y].push_back(x);//x是y的子节点 } int root; for (int i = 1; i <= n; i++) if(!v[i]){ //i没有爸爸 root = i; break; } DFS(root); cout << max(dp[root][0],dp[root][1]) << endl; return 0; }
B - Strategic game
有n个点,在某些点上放置哨兵,每个哨兵可以监控和它有边相连的点,问监视所有的点需要的最少的哨兵数
也就是说一颗n个结点的树,要求选出其中的一些顶点,使得对于树中的每条边(u, v),u和v至少有一个被选中,要求给出选中顶点数最少的方案
设 dp [x] [0] 表示在不选择节点 x 的情况下,以 x 为根节点的子树,最少需要选择的节点数
当i为叶子节点时
当i不为叶子节点时 (s表示x子节点)
设 dp [x] [1] 表示在选择节点 x 的情况下,以 x 为根节点的子树,最少需要选择的节点数
当i为叶子节点时
当i不为叶子节点时 (s表示x子节点)
#include <iostream> #include <cstring> #include <cstdio> #include <cmath> #define maxn 1508 using namespace std; int dp[maxn][2]; int soncnt[maxn]; int parent[maxn]; int n; void DFS(int x) { int i, d1=0, d0=0; if (soncnt[x] == 0) { dp[x][0] = 0; dp[x][1] = 1; return; } for (i=0; i < n; i++) { if (parent[i] == x) { DFS(i); d1 += min(dp[i][0], dp[i][1]); d0 += dp[i][1]; } } dp[x][1] = d1 + 1; dp[x][0] = d0; } int main() { int dad, son, m; while (cin >> n) { memset(soncnt, 0, sizeof(soncnt)); memset(parent, -1, sizeof(parent)); int root = -1; for (int i = 0; i < n; i++) { scanf("%d:(%d)", &dad, &m); soncnt[dad] = m; if (root == -1) { root = dad; } while (m--) { scanf("%d", &son); parent[son] = dad; } } DFS(root); cout << min(dp[root][0], dp[root][1]) << endl; } return 0; }
C - Tree Cutting
给一颗n个结点的树,节点编号为1~n
问:删除哪些结点后,剩余各个子树的大小均小于原总结点数的一半
拆除一个节点后,剩余部分为其若干儿子的子树以及该节点上层所连其余部分(n-size[i]),只要这些连接块大小都不超过n/2,该节点就满足条件。因而我们可以先求出每个节点所管辖的那棵树的大小,自下而上地为每个节点求出其子树规模(该点规模=其儿子的规模和+1)。
在建树的时候可以直接用连接表(vector)储存无向边,这时由于无法区分与每个点相连的是其父节点还是子节点,会引发问题:在dfs的时候把父节点误认作儿子节点,解决方法就是在递归的时候传入父节点编号,然后在递归,计算规模,比较大小的时候避开它就可以了
#include<iostream> #include<cstring> #include<vector> #include<cstdio> #include<algorithm> using namespace std; int n; int root; vector<int>G[10000+400]; vector<int>ans; int sz[10000+400]; void dfs(int par,int u){ sz[u]=1; for(int i=0;i<(int)G[u].size();i++){ if(G[u][i]!=par) dfs(u,G[u][i]); } int piece=0; for(int i=0;i<(int)G[u].size();i++) if(par!=G[u][i]){ sz[u]+=sz[G[u][i]]; piece=max(piece,sz[G[u][i]]); } piece=max(piece,n-sz[u]); if(piece<=n/2) ans.push_back(u); } int main(){ while(cin>>n){ memset(sz,0,sizeof(sz)); int x,y; for(int i=0;i<n-1;i++){ scanf("%d%d",&x,&y); G[x].push_back(y); G[y].push_back(x); } dfs(0,1); sort(ans.begin(),ans.end()); for(int i=0;i<(int)ans.size();i++) cout<<ans[i]<<endl; } return 0; }
LCA 最近公共祖先
倍增(基于二分的方法)
假如树上的两个点,处于同一深度时,对于往上跳mid步,显然有单调性:
如果它们往上跳mid步是同一个点,则这个点是它们的共同祖先,但不一定是最近公共祖先
那么如果我们可以快速得到每个点往上跳若干步是谁,显然我们可以利用二分来求LCA
这个可以用倍增来做。
倍增的思想其实非常简单,就是利用二进制的思想。一个显然的结论有:
节点u往上跳2^(k+1)的布的祖先 = (节点u往上跳2k布的祖先)往上跳2k布的祖先
用代码表示就是:
fa[u][k + 1] = fa[ fa[u][k] ][k];
这个的预处理也非常简单,我们只需要初始化每个fa[u][0]就可以了就可以了,因为接下来的部分都可以根据上面的公式递推。
然后预处理好这个之后,我们就可以根据这个进行二分了,代码如下:
int fa[maxn][20], dep[maxn]; void dfs(int u, int f, int d) { dep[u] = d; fa[u][0] = f; for(int i = 1; i < 20; ++i) { fa[u][i] = fa[fa[u][i - 1]][i] - 1; } for(int i = head[u]; ~i; i = nxt[i]) { int t = to[i]; if(t != f) { dfs(t, u, d + 1); } } } int lca(int u, int v) { if(dep[u] < dep[v]) swap(u, v); int k = dep[u] - dep[v]; for(int i = 0; i < k; ++i) { if((1 << i) & k) u = fa[u][i]; } if(u == v) return u; for(int i = 19; i >= 0; --i) { if(fa[u][i] != fa[v][i]) { u = fa[u][i]; v = fa[v][i]; } } return fa[u][0]; }
以上就是详解树形DP的详细内容,更多关于树形DP的资料请关注脚本之家其它相关文章!