树形 dp/换根 dp
树形dp
树形动归一般是依赖于dfs的,根据动归的后效性,父节点的状态一般都依赖子节点的状态以某种方式转移而来
换根的p2015
\[设f[i][j]表示i的子树上保留j条边最多苹果数\\
f[i][j]=max(f[i][j],f[left][j]+e[left].apple+f[right][k-j]+e[right].apple)\\
e[i].apple表示i条树枝的苹果数
\]
p2279
\[状态表示f[x][0]:覆盖到x的爷爷和x整棵子树(向上2层),最少个数\\
f[x][1]:覆盖到x的父亲和x子树(向上一层)\\
f[x][2]:覆盖到x整颗子树(向上0层)\\
f[x][3]:覆盖x的儿子及其子树(向上-1层)\\
f[x][4]:覆盖所有x的孙子及其子树(向上-2层)\\
显然f[x][i]一定包含f[x][i+1],y.z是x的儿子\\
f[x][0]=1+\sum f[y][4](因为i可以覆盖到向上2层,所以它自己必须是消防站~显然)\\
x的儿子中有一个一定覆盖的爷爷,同时覆盖到兄弟(因为y一定是选了),其他的儿子只需要覆盖的自己的儿子即可\\f[x][1]=min(f[y][0]+\sum f[z][3](y!=z))\\
f[x][2]=min(f[y][1]+\sum[z][2])有一个儿子覆盖到父亲,但无法覆盖到y的兄弟,所以其他儿子要覆盖到自己\\
f[x][3]=\sum f[y][2]让每个儿子覆盖到自己即可\\
f[x][4]=\sum f[y][3]让每个儿子覆盖到自己的儿子\\
\]
P1122 最大子树和
\[设f[i][0]为被当前这个点保安控制的点\\
显然f[i][0]=\sum min(f[son[i]][0],f[son[i]][1],f[son[i][2])+val[i]\\
f[i][1]为被当前这个点的儿子控制的点\\
显然f[i][1]=\sum min(f[son[i][0],f[son[i]][1])如果选择的全部都是f[son[i]][1],\\要再加上min(f[son[i]][0]-f[son[i]][1])\\
f[i][2]为被当前这个点的fa控制的点\\
这个有点麻烦f[i][2]=\sum min(f[son[i]][0],f[son[i]][1])我们不妨这样理解,对于i节点我们让它\\的父亲节点fa覆盖它,那么根据我们的状态设计,此时必须要满足以i的儿子son[i]为根的子树\\之中所有点已经被覆盖那么这时就转化为一个子问题,要让y子树满足条件,只有两种决策:要么son[i]\\被son[i]的儿子覆盖,要么被son[i]自己覆盖(即选择son[i]节点)\\,只需要在son[i]的这两种状态取min累加就可以了
\]
对于\(f[i][1]\)的转移,luogu大佬有详细解释:(这位大佬)\(\_\_\_new2zy\_\_\_\)
我们可以这样理解,此时既然要保证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]\)也才能合理地得到转移
就是这样1代表选了该点,0没选,假设问号为根节点,0为枝条,1为叶子,这样显然不行,所以取最小花费的点,加入到花费,明白了吧
?
0 0 0 0
1 1 1 1
//代码哥哥:
#include<cstdio>
#define maxn 1520
#define int long long
using namespace std;
inline int min(int a,int b){return a<b?a:b;}
inline int read(){
int p=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
return f*p;}
struct edge{
int to,next;
}e[maxn<<1];
int n,tot=0,head[maxn<<1],val[maxn];
int f[maxn][4];
inline void add(int x,int y)//加边
{
tot++;
e[tot].next=head[x];
head[x]=tot;
e[tot].to=y;
}
inline int treedp(int u,int fa){
f[u][0] = val[u];
int sum = 0,mincost = 0x777777f;
for(int i = head[u];i;i=e[i].next){
int y=e[i].to;
if(y==fa) continue;
treedp(y,u);
int az = min(f[y][0],f[y][1]);
f[u][0] += min(f[y][2],az);
f[u][2] += az;
if(f[y][0]<f[y][1]) sum++;//如果选择儿子节点更优,选上,计数器sum++,证明选过f[y][0]
else mincost=min(mincost,f[y][0]-f[y][1]);
f[u][1] += az;
}
if(!sum)f[u][1] += mincost;
}
signed main(){
n=read();
for(int i=1;i<=n;i++)
{
int x=read();
val[x]=read();
int num=read();
while(num>0)
{
int y=read();
add(x,y);
add(y,x);
num--;
}
}
treedp(1,0);
printf("%d",min(f[1][0],f[1][1]));
}
换根dp一般分为三个步骤
1、先指定一个根节点
2、一次dfs统计子树内的节点对当前节点的贡献
3、一次dfs统计父亲节点对当前节点的贡献并合并统计最终答案
二次扫描与换根法:
\(f[i]表示以u为根的树的深度和,size[i]表示以i为根子树的结点个数\)
\(f[v]=f[u]-size[x]+n-size[x]=f[u]+n-2*size[x]\)
本来是以u为根的树,变成以儿子v为根的树,
那么v的所有结点的深度都会减1,深度和就会减少size[v],
同样地,所有不在v的子树上的结点的深度都会+1,深度和就会加上n - size[v],