树形(换根) dp
树形 \(dp\)
定义:
其实就是在树上递归进行的 \(dp\)
过程:
利用一道题进行举例子:P1352 没有上司的舞会
就是如果选择一个节点,一个节点的父亲一定不要选择,求权值之和最大值。
我们定义 \(f[i][1/0]\) 表示以 \(i\) 为根的子树的最优解(参加/不参加).
推出两个状态转移方程( \(x\) 为 \(i\) 的儿子):
邻接表跑,在返回上一层时更新当前节点的最优解。
树上背包:
就是背包问题和树形 \(dp\) 的结合。
还是举例子:P2014 [CTSC1997]选课
跟上题相反,选择一个节点,这个节点的父亲一定要选,求选 \(m\) 个点的权值最大。
新建点 \(0\) 为根节点,来连接这个森林。
设 \(f[x][i][j]\) 表示在 \(x\) 根节点的子树中,已经遍历了 \(x\) 号点的前 \(i\) 棵子树,选了 \(j\) 门课程。
我们枚举 \(x\) 节点的每个子节点 \(y\) ,同时枚举以 \(y\) 为根的子树选了几门课程,将子树的结果合并到 \(x\) 上。
记 \(x\) 的儿子个数为 \(S_x\) ,以 \(x\) 为根的子树大小为 \(sizes_x\),则有转移方程:
进行背包 \(dp\) 优化:倒序枚举 \(j\) 的值,即可把 \(i\) 这一维省去。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int n,a[N],dp[N][N],m;
int nxt[N],ver[N],head[N],tot;
int sizes[N];
void add(int x,int y){
ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot;
}
void dfs(int x){
dp[x][1]=a[x];
int p=1;
sizes[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
dfs(y);
for(int i=min(sizes[x],m+1);i;i--)
for(int j=1;(j<=sizes[y])&&(i+j<=m+1);j++)
dp[x][i+j]=max(dp[x][i+j],dp[x][i]+dp[y][j]);
sizes[x]+=sizes[y];
}
}
int main(){
cin>>n>>m;
for(int i=1,x,y;i<=n;i++){
scanf("%d%d",&x,&y);
add(x,i); a[i]=y;
}
dfs(0);
printf("%d\n",dp[0][m+1]);
system("pause");
return 0;
}
换根 \(dp\)
换根 \(dp\) 又被称为二次扫描,通常不会指定根节点,而且根节点的变化会对一些值,例如子节点深度和,点权和产生影响。
一句话总结重点:第一次 \(dfs\) 搜索所有点,得出所有点状态的值,第二次 \(dfs\) 对于各种状态进行计算,从而得出所需要的答案。
\(dfs\) 设计:
总结:
第一次扫描时,任选一个点为根,在“有根树”上执行一次树形 \(dp\),在回溯时,自底向上的状态转移。
第二次扫描时,从第一次选的根出发,对整根树执行一个 \(dfs\),在每次递归前进行自顶向下的转移,计算出换根后的解。
- 一般来说,都需要存储树的大小,所以我们定义 \(sizes\) 作为这个节点子树的大小。因此有一下代码:
sizes[x]=1;
...
dfs(y,x);
sizes[x]+=sizes[y];
-
其他状态需要自己设置,一般来说,树形 \(dp\) 的题目都是跟树的节点数目有关的。
-
进入到第二遍 \(dfs\) 中,我们就需要运用 容斥原理 去求出答案:
例如P3047 [USACO12FEB]Nearby Cows G 中,我们去计算距离不超过 \(k\) 的点的点权之和。因为在第一遍 \(dfs\) 中,我们运用了:
for(int j=1;j<=k;j++)
f[x][j]+=f[y][j-1];
如果是这样的话,我们对于距离为 \(x\) 的边,都多算了 \((k-x)\) 次,因此在第二次 \(dfs\) 中有以下代码:
for(int j=k;j>=2;j--) f[y][j]-=f[y][j-2];
for(int j=1;j<=k;j++) f[y][j]+=f[x][j-1];
换根dp公式:
void dfs(int x,int fa){
sizes[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa) continue;
dfs(y,x);
sizes[x]+=size[y];
....
....
}
}
void dfs2(int x,int fa)[
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa) continue;
...
...
...
ans[x]=...
dfs(y,x);
}
]
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>x>>y;
add(x,y);add(y,x);
}
dfs(1,0);dfs2(1,0);
cout<<ans<<endl;
return 0;
}
例题:
解题思路:
\(dp[i][1]\) 表示选择该节点所获得的最大值,\(dp[i][0]\) 表示不选择该节点获得的最大值。
因此我们就可以轻松写出来代码:
#include<bits/stdc++.h>
using namespace std;
vector<int> son[10010];
int f[10010][2],v[10010],h[10010],n;
void dp(int x){
f[x][0]=0;//0表示其本身不参加
f[x][1]=h[x]; //本身参加
for(int i=0;i<son[x].size();i++){
int y=son[x][i];//儿子的地址
dp(y);
f[x][0]+=max(f[y][0],f[y][1]);//儿子节点参加或不参加的最大值
f[x][1]+=f[y][0];//每个儿子节点不参加的最大值
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>h[i];
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
v[x]=1;//有爹
son[y].push_back(x);
}
int root;
for(int i=1;i<=n;i++)
if(!v[i]){//没爹,即根节点
root=i;
break;
}
dp(root);
cout<<max(f[root][1],f[root][0])<<endl;
return 0;
}
我们知道,一个节点不能作为重心,有且仅有一个子树大小大于 \(\lfloor\dfrac{n}{2}\rfloor\),我们一定是从这个子树里面选一个子树接在当前的根上。
那么就找一个这样的子树就可以啦
考虑 \(dp[u]\) 的最佳转移点,如果 \(dp[u]\) 最佳转移点就是\(v\) ,那么我们就需要得到一个点第二大的能够去除的子树大小
所以必须考虑维护两个 \(dp\) 值,\(dp[u][0]\) 表示第一大的值,\(dp[u][1]\) 表示第二大的值。
这样我们就可以解出这道题了。
#include<bits/stdc++.h>
using namespace std;
const int N=8e5+5;
int n;
int nxt[N],ver[N],head[N],tot;
int dp[N][2],pos[N];
int sizes[N],maxsizes[N];
int ans[N],[N];
void add(int x,int y){
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void dfs(int x,int fa){
sizes[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa) continue;
dfs(y,x); int v;
sizes[x]+=sizes[y];
if(sizes[y]>sizes[maxsizes[x]]) maxsizes[x]=y;// 最多子节点的树
if(sizes[y]<=n/2) v=sizes[y];//子节点不足
else v=dp[y][0];
if(dp[x][0]<v){
dp[x][1]=dp[x][0];
dp[x][0]=v;pos[x]=y;
}
else if(dp[x][1]<v) dp[x][1]=v;
}
}
void dfs2(int x,int fa){
ans[x]=1;
if(sizes[maxsizes[x]]>n/2) ans[x]=(sizes[maxsizes[x]]-dp[maxsizes[x]][0]<=n/2);
else if(n-sizes[x]>n/2) ans[x]=(n-sizes[x]-f[x]<=n/2);
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa) continue; int v;
if(n-sizes[x]>n/2) v=f[x];
else v=n-sizes[x];
f[y]=max(f[y],v);
if(pos[x]==y) f[y]=max(f[y],dp[x][1]);
else f[y]=max(f[y],dp[x][0]);
dfs2(y,x);
}
}
int main()
{
cin>>n;
for(int i=1;i<n;i++){
int x,y;scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
dfs(1,0);dfs2(1,0);
for(int i=1;i<=n;i++)
printf("%d ",ans[i]);
//system("pause");
return 0;
}