树形DP是WHAT?
就是树上的DP呗 :-p
HOW TO DP?
给定一棵有N个节点的树(通常是无根树,也就是有N-1条无向边),我们可以任选一个节点为根节点,从而定义出每个节点的深度和每棵子树的根。
在树上设计动态规划算法时,一般就以节点从深到浅(子树从小到大)的顺序作为DP的“阶段”。DP的状态表示中,第一维通常是节点编号(代表以该节点为根的子树)。大多数时候,我们采用递归的方式实现树形动态规划。对于每个节点x,先递归在它的每个子节点上进行DP,在回溯时,从子节点向节点x进行状态转移。
经典例题
1、最大独立子集:
最大独立子集的定义是,对于一个树形结构,所有的孩子和他们的父亲存在排斥,也就是如果选取了某个节点,那么会导致不能选取这个节点的所有孩子节点。一般询问是要求给出当前这颗树的最大独立子集的大小(被选择的节点个数)。
例题:没有上司的舞会
我们设
d
p
[
i
]
[
1
]
dp[i][1]
dp[i][1]表示
i
i
i要来的情况下以
i
i
i为根节点的子树的最大快乐值
d
p
[
i
]
[
0
]
dp[i][0]
dp[i][0]表示
i
i
i不来的情况下以
i
i
i为根节点的子树的最大快乐值
若 i i i要来,那么他的每一个子节点都只能不来, d p [ i ] [ 1 ] = ∑ d p [ j ] [ 0 ] , j 为 i 的 子 节 点 dp[i][1]=\sum dp[j][0],j为i的子节点 dp[i][1]=∑dp[j][0],j为i的子节点
如果ta不来,那么他的每个子节点就可来可不来, d p [ i ] [ 0 ] = ∑ m a x ( d p [ j ] [ 0 ] , d p [ j ] [ 1 ] ) , j 为 i 的 子 节 点 dp[i][0]=\sum max(dp[j][0],dp[j][1]),j为i的子节点 dp[i][0]=∑max(dp[j][0],dp[j][1]),j为i的子节点
我们需要从每个叶子节点向上不断跟新dp,直到求到根节点,这一步我们可以用递归的方式,对于每一个节点,先递归他的每一个子节点,再在回溯时执行更新操作
最终的答案就是看根节点是去还是不去, m a x ( d p [ r o o t ] [ 1 ] , d p [ r o o t ] [ 0 ] ) max(dp[root][1],dp[root][0]) max(dp[root][1],dp[root][0])’
Code:
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int MAXN=6005;
int h[MAXN];
vector<int> son[MAXN];
bool flag[MAXN];
int dp[MAXN][5];
int Max(int a,int b){
return a<b?b:a;
}
int Min(int a,int b){
return a<b?a:b;
}
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];
dfs(y);
dp[x][0]+=Max(dp[y][0],dp[y][1]);
dp[x][1]+=dp[y][0];
}
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&h[i]);
}
for(int i=1;i<=n-1;i++){
int l,k;
scanf("%d %d",&l,&k);
son[k].push_back(l);
flag[l]=1;
}
int root;
for(int i=1;i<=n;i++){
if(!flag[i]){
root=i;
break;
}
}
dfs(root);
printf("%d",Max(dp[root][1],dp[root][0]));
return 0;
}
2、树的重心
this
树的重心定义为,当把节点x去掉后,其最大子树的节点个数最少(或者说成最大连通块的节点数最少),那么节点x就是树的重心。
通俗的理解:这个点去掉后,剩下的联通块尽量平均
算法:
树上任选一结点 u u u 开始 DFS,沿路统计其所有子树的大小和以它为根的子树的大小,这样也就可以得到一个点所有子树的大小及的大小
还有一个问题,如果这个点不是根节点,那么删掉后剩下的连通块不止有他的子树,还有他上面的部分,这部分可以由总结点减去他自己的子树大小得到
我们知道一个性质:删除重心后所得的所有子树,节点数不超过原树的1/2,那么我们使用这个性质来判断一个点是否为重心即可
代码:
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int MAXN=105;
vector<int> g[MAXN];
int dp[MAXN];
bool vis[MAXN];
int ans[MAXN];
int len=0;
int n;
void dfs(int x,int to){
dp[x]=1;
bool flag=true;
for(int i=0;i<g[x].size();i++){
int y=g[x][i];
if(!vis[y]){
vis[y]=true;
dfs(y,x);
dp[x]+=dp[y];
if(dp[y]>n/2){
flag=false;
}
}
}
if(n-dp[x]>n/2){
flag=false;
}
if(flag){
ans[++len]=x;
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n-1;i++){
int u,v;
scanf("%d %d",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,-1);
printf("%d\n",len);//有多少个重心
for(int i=1;i<=len;i++){
printf("%d\n",ans[i]);//输出每一个重心
}
return 0;
}
3、树的直径
this
给定一棵树,树中每条边都有一个权值,树中两点之间的距离定义为连接两点的路径边权之和。树中最远的两个节点(两个节点肯定都是叶子节点)之间的距离被称为树的直径,连接这两点的路径被称为树的最长链。后者通常也可称为直径。
算法:
不妨设1号节点为根,“N个节点,N-1条边的无向图”就可以看作“有根树”。设d[x]表示从节点x出发走向以x为根的子树,能够到达最远节点的距离。设x的子节点为y1,y2,……,yt,edge(x, y)表示边权,显然有:
接下来,我们可以考虑对每个节点x求出“经过节点x的最长链的长度”ans[x],整棵树的直径就是:
那么,如何求ans[x]呢?我们用链式前向星存图,依次遍历以x为起点的所有连边,用已经更新过的d[x]与没有枚举到的x的连边之和来更新ans[x]:
然后再不断更新
注意ans[i]存的是经过i节点的最长链,最后还要便利一遍ans才能求到真正的直径
而我们的ans数组其实可以用一个整型数ans代替,就不需要后面再更新一遍了
关键代码:
void dp(int x){
vis[x]=1;
for(int i=head[x];i;i=edge[i].next){
int y=edge[i].to;
if(vis[y]) continue;
dp(y);
ans=max(ans,d[x]+d[y]+edge[i].dis);
d[x]=max(d[x],d[y]+edge[i].dis);
}
}
拓展:两次DFS求直径
在树上任选一节点 u u u ,通过搜索求得距离它最远的点 x x x ,再从点 x x x 出发通过搜索得到距离它最远的点 y y y , x x x 到 y y y 的路径即为这棵树的直径。
#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int MAXN=100005;
vector<int> g[MAXN];
int point,maxn;
void dfs(int now,int fa,int step){
if(step>maxn){
maxn=step;
point=now;
}
for(int i=0;i<g[now].size();i++){
int v=g[now][i];
if(v!=fa){
dfs(v,now,step+1);
}
}
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n-1;i++){
int u,v;
scanf("%d %d",&u,&v);
g[u].push_back(v);
g[v].push_back(u);
}
maxn=-1;
dfs(1,0,0);
maxn=-1;
dfs(point,0,0);
printf("%d",maxn);
return 0;
}