树形 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],

posted @ 2020-07-16 16:23  INFP  阅读(272)  评论(1编辑  收藏  举报