树形DP

一、简单树形DP

树形 \(DP\),即在树上进行 \(DP\),一般以递归进行,以题为例:

模板题 没有上司的舞会

\(\Rightarrow\) 分析:对于一个结点 \(x\),其一个儿子结点为 \(y\),分两种情况:
(1)如果 \(x\) 不去,那么他的儿子结点可去可不去,即:
$ f_{x,0}=\sum max(f_{y,0},f_{y,1}) $
(2)如果 \(x\) 去,那么他的儿子结点只能不去,即:
$ f_{x,1}=\sum f_{y,0} $

\(\Rightarrow\) 代码实现:
(1)输入的时候输入的值相当于最开始的 \(f_{i,1}\)
(2)利用 \(dfs\) 递归记录每个结点的值即可。
(3)\(dfs\) 要从根节点开始,此时我们只要根据输入来判断,对于每一个 \(l_i\) 肯定不是根节点,所以最后看哪一个点没有在 \(l_i\) 中出现过即是根节点。

长城之子,归于长城。
#include <iostream>
#include <cstdio>
#define N 6003

using namespace std;

int n,f[N][2],jilu[N],vis[N];

struct edge{
	int next,to;
}edge[N];
int head[N],cnt;
void add(int from,int to){
	edge[++cnt].next = head[from];
	edge[cnt].to = to;
	head[from] = cnt;
}

void Input(){
	scanf("%d",&n);
	for(int i = 1;i <= n;i ++) scanf("%d",&f[i][1]);
	for(int i = 1,l,k;i < n;i ++){
		scanf("%d%d",&l,&k);
		add(k,l);jilu[l] = 1;
	}
}

void dfs(int x){
	vis[x] = 1;
	for(int i = head[x];i;i = edge[i].next){
		int y = edge[i].to;
		if(vis[y]) continue;
		dfs(y);
		f[x][1] += f[y][0];
		f[x][0] += max(f[y][1],f[y][0]);
	}
}

void work(){
	for(int i = 1;i <= n;i ++){
		if(!jilu[i]){
			dfs(i);
			printf("%d\n",max(f[i][0],f[i][1]));
			return ;
		}
	}
}

int main(){
	Input();
	work();
	return 0;
}

三色二叉树

\(\Rightarrow\) 分析:先考虑如何状态转移,有三种情况:
\(x\) 为当前根节点,分别令 \(fmax_{x,0},fmax_{x,1},fmax_{x,2}\) 表示如果结点 \(x\) 为绿色 \(or\) 红色 \(or\) 蓝色,那么其对应子树可以得到被染成绿色结点的最大值,\(fmin\) 为最小值。
根据题目输入性质,结点 \(x\) 的左儿子可定紧挨着这个结点,所以转移左儿子时直接使用 \(x + 1\) 就可以了。
那么如果当前结点为绿色,则:
$ fmax_{x,0} = max(fmax_{x + 1,1},fmax_{x + 1,2}) + 1 $

$ fmin_{x,0} = min(fmin_{x + 1,1},fmin_{x + 1,2}) + 1 $

如果是红色/蓝色,则:

$ fmax_{x,1} = max(fmax_{x + 1,0},fmax_{x + 1,2}) $

$ fmin_{x,1} = min(fmin_{x + 1,0},fmin_{x + 1,2}) $

$ fmax_{x,2} = max(fmax_{x + 1,0},fmax_{x + 1,1}) $

$ fmin_{x,2} = min(fmin_{x + 1,0},fmin_{x + 1,1}) $
那么右儿子如何处理呢?此时我们令 \(t\) 为右儿子所在位置。
那么如果当前结点为绿色,则:
$ fmax_{x,0} = max(fmax_{x + 1,1} + fmax_{t,2},fmax_{x + 1,2} + fmax_{t,1}) + 1 $

$ fmin_{x,0} = min(fmin_{x + 1,1} + fmin_{t,2},fmin_{x + 1,2} + fmin_{t,1}) + 1 $

如果是红色/蓝色,则:

$ fmax_{x,1} = max(fmax_{x + 1,0} + fmax_{t,2},fmax_{x + 1,2} + fmax_{t,0}) $

$ fmin_{x,1} = min(fmin_{x + 1,0} + fmin_{t,2},fmin_{x + 1,2} + fmin_{t,0}) $

$ fmax_{x,2} = max(fmax_{x + 1,0} + fmax_{t,1},fmax_{x + 1,1} + fmax_{t,0}) $

$ fmin_{x,2} = min(fmin_{x + 1,0} + fmin_{t,1},fmin_{x + 1,1} + fmin_{t,0}) $

\(\Rightarrow\) 代码实现:
现在只有一个问题,如何知道 \(t\) 是多少呢?
首先我们知道,左儿子肯定挨着该儿子的父亲节点,右儿子肯定挨着左儿子,所以我们可以遍历时随时记录遍历到的最右边的点的下标 \(cnt\),那么 \(t\) 其实就相当于是 \(cnt + 1\),这个可以自己想一下,也可以模拟一下样例。这样可以节约时间复杂度。

本猫守望的长城,屹立不倒。
#include <iostream>
#include <cstdio>
#define N 500005

using namespace std;

string s;
int cnt = 0,f_max[N][3],f_min[N][3];

void dfs(int x){
	if(s[x] == '0'){
		f_max[x][0] = f_min[x][0] = 1;
		return ;
	}
	dfs(++cnt);
	if(s[x] == '1'){
		f_max[x][0] = max(f_max[x + 1][1],f_max[x + 1][2]) + 1;
		f_max[x][1] = max(f_max[x + 1][0],f_max[x + 1][2]);
		f_max[x][2] = max(f_max[x + 1][0],f_max[x + 1][1]);
		f_min[x][0] = min(f_min[x + 1][1],f_min[x + 1][2]) + 1;
		f_min[x][1] = min(f_min[x + 1][0],f_min[x + 1][2]);
		f_min[x][2] = min(f_min[x + 1][0],f_min[x + 1][1]);
	}
	else{
		int t = ++ cnt;
		dfs(t);
		f_max[x][0] = max(f_max[x + 1][1] + f_max[t][2],f_max[x + 1][2] + f_max[t][1]) + 1;
		f_max[x][1] = max(f_max[x + 1][0] + f_max[t][2],f_max[x + 1][2] + f_max[t][0]);
		f_max[x][2] = max(f_max[x + 1][1] + f_max[t][0],f_max[x + 1][0] + f_max[t][1]);
		f_min[x][0] = min(f_min[x + 1][1] + f_min[t][2],f_min[x + 1][2] + f_min[t][1]) + 1;
		f_min[x][1] = min(f_min[x + 1][0] + f_min[t][2],f_min[x + 1][2] + f_min[t][0]);
		f_min[x][2] = min(f_min[x + 1][1] + f_min[t][0],f_min[x + 1][0] + f_min[t][1]);
	}
}

int main(){
	cin >> s;
	dfs(cnt);
	printf("%d %d\n",max(f_max[0][0],max(f_max[0][1],f_max[0][2])),min(f_min[0][0],min(f_min[0][1],f_min[0][2])));
	return 0;
}

二、树上背包

其实也就是树形 \(DP\) 和背包的结合啦。
还是以题为例:

选课

\(\Rightarrow\) 分析:每一门功课都最多有一门先修课,所以可以看成一个树形结构。
考虑如何实现状态转移,令 \(f_{x,i,j}\) 表示对于结点 \(x\),已经遍历了其前 \(i\) 个叶子结点,共学习 \(j\) 门功课可以获得的最大学分。
从三维的角度分析,我们令每个结点的叶子结点所有的编号依次递增,那么:
\(f_{x,i,j} = max(f_{x,i - 1,j},f_{x,i - 1,k} + f_{y,siz_y,j - k})\)
其中 \(y\) 表示 \(x\) 的第 \(i\) 个叶子结点,\(siz_y\) 表示 \(y\) 的子树的大小。

如果卡空间怎么办?——考虑可否去掉一维。
刚刚的问题其实就想到于对于一个结点的贡献进行 \(01\) 背包来计算,所以类似 \(01\) 背包一样,可以去掉第二维,即:
\(f_{x,j} = max(f_{x,j},f_{x,k} + f_{y,j - k})\)

\(\Rightarrow\) 代码实现:
(1)输入的时候相当于输入最开始时候的 \(f_{i,1}\)
(2)可以根据题意,把 \(0\) 作为根节点,但是处理的时候要注意:一共要取 \(m + 1\) 门功课,因为 \(0\) 必须的选。
(3)注意循环的顺序以及循环的端点。

即便三分钟热度,也足以穿透次元壁。
#include <iostream>
#include <cstdio>
#define N 305

using namespace std;

int n,m,f[N][N],siz[N],vis[N];

struct edge{
	int next,to;
}edge[N];
int head[N],cnt;
void add(int from,int to){
	edge[++cnt].next = head[from];
	edge[cnt].to = to;
	head[from] = cnt;
}

void Input(){
	scanf("%d%d",&n,&m);
	for(int i = 1,k;i <= n;i ++) {
		scanf("%d%d",&k,&f[i][1]);
		add(k,i);
	}
}

void dfs(int x){
	vis[x] = 1;
	for(int i = head[x];i;i = edge[i].next){
		int y = edge[i].to;
		if(vis[y]) continue;
		dfs(y);
		for(int j = m + 1;j >= 1;j --){
			for(int k = 1;k <= j;k ++){
				f[x][j] = max(f[x][j],f[x][k] + f[y][j - k]);
			}
		}
	}
}

void work(){
	dfs(0);
	printf("%d",f[0][m + 1]);
}

int main(){
	Input();
	work();
	return 0;
}

二叉苹果树

\(\Rightarrow\) 分析:与上题基本类似,只不过有一个地方不太一样:上题相当于选结点,但是本题相当于选路径,所以在状态转移上有小的差别。
仍然,令 \(f_{x,i,j}\) 表示对于结点 \(x\),已经遍历了其前 \(i\) 个叶子结点,共选 \(j\) 个树枝可以获得的最多的苹果。
从三维的角度分析,我们令每个结点的叶子结点所有的编号依次递增,那么:
\(f_{x,i,j} = max(f_{x,i - 1,j},f_{x,i - 1,k} + f_{y,siz_y,j - k - 1} + w_{x,y})\)
还是可以像 \(01\) 背包一样,去掉第二维:
\(f_{x,j} = max(f_{x,j},f_{x,k} + f_{y,j - k - 1} + w_{x,y})\)
\(\Rightarrow\) 代码实现:注意循环顺序已经循环的端点即可。

崩塌的不止防御塔,还有你最后的倔强。
#include <iostream>
#include <cstdio>
#define N 105

using namespace std;

int n,Q,vis[N],f[N][N];

struct edge{
	int next,to,dis;
}edge[N << 1];
int head[N],cnt;
void add(int from,int to,int dis){
	edge[++cnt].next = head[from];
	edge[cnt].to = to;
	edge[cnt].dis = dis;
	head[from] = cnt;
}

void Input(){
	scanf("%d%d",&n,&Q);
	for(int i = 1,u,v,w;i < n;i ++){
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);add(v,u,w);
	}
}

void dfs(int x){
	vis[x] = 1;
	for(int i = head[x];i;i = edge[i].next){
		int y = edge[i].to;
		if(vis[y]) continue;
		dfs(y);
		for(int j = Q;j >= 1;j --){
			for(int k = 0;k < j;k ++){
				f[x][j] = max(f[x][j],f[x][k] + f[y][j - k - 1] + edge[i].dis);
			}
		}
	}
}

void work(){
	dfs(1);
	printf("%d",f[1][Q]);
}

int main(){
	Input();
	work();
	return 0;
}

换根DP

换根 \(DP\),就是根结点不确定的时候,有些属性可能会随着根结点的变化而变化(比如深度),这个时候就要引入换根 \(DP\) 来求解。还是以题为例:

模板题:STA-Station

\(\Rightarrow\) 分析:如果单纯暴力的话,就是将每一个结点作为根结点进行答案求解,最后记录答案,时间复杂度 \(O(n^2)\),肯定不行,考虑优化。
我们肯定是想知道两个结点分别作为根结点之间有什么关系。我们令 \(x\) 作为根结点的时候深度和为 \(f_x\),其一个儿子结点为 \(y\),深度和为 \(f_y\),子树大小为 \(siz_y\),那么如果根结点由 \(x\) 转变成 \(y\) 时,所有 \(y\) 的子树的结点的深度全部加 \(1\),其他结点的深度都减 \(1\),由此,状态转移就很容易出来了:
$ f_y = f_x + n - 2 \times siz_y $

\(\Rightarrow\) 代码实现:
可以先把 \(1\) 作为根结点预处理出来 \(siz_i\)\(f_1\),然后进行状态转移即可。
注意在极端情况下 \(f\) 数组会爆 \(int\),注意开 \(long\) \(long\)

一硫二硝三木炭,解构你轰轰烈烈的人生。
#include <iostream>
#include <cstdio>
#define N 1000006

using namespace std;

int n,res;
long long ans,dep[N],siz[N],f[N];

struct edge{
	int next,to;
}edge[N << 1];
int head[N << 1],cnt;
void add(int from,int to){
	edge[++cnt].next = head[from];
	edge[cnt].to = to;
	head[from] = cnt;
}

void dfs1(int x,int fa,int deep){
	siz[x] = 1,dep[x] = deep,f[1] += dep[x];
	for(int i = head[x];i;i = edge[i].next){
		int y = edge[i].to;
		if(y == fa) continue;
		dfs1(y,x,deep + 1);
		siz[x] += siz[y];
	}
}

void Input(){
	scanf("%d",&n);
	for(int i = 1,u,v;i < n;i ++){
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	dfs1(1,0,1);
}

void dfs2(int x,int fa){
	for(int i = head[x];i;i = edge[i].next){
		int y = edge[i].to;
		if(y == fa) continue;
		f[y] = f[x] + n - 2 * siz[y];
		dfs2(y,x);
	}
	
}

void work(){
	dfs2(1,0);
	for(int i = 1;i <= n;i ++) if(f[i] > ans) ans = f[i],res = i;
	printf("%d",res);
}

int main(){
	Input();
	work();
	return 0;
}

Great Cow Gathering G

\(\Rightarrow\) 分析:此题和上题基本类似,只不过有了路径长度以及结点的权值。大体的思路不变,仍然令 \(x\) 作为根结点的时候不方便和为 \(f_x\),其一个儿子结点为 \(y\),不方便和为 \(f_y\),只不过这个时候 \(siz_y\) 代表这个子树下有多少头牛。那么当根节点由 \(x\) 转为 \(y\) 时,所有 \(y\) 的子树的结点的不方便值减去了 \(w_{x,y}\),其他的都加上了 \(w_{x,y}\),那么状态转移方程又出来了(注:\(res\) 为牛头的总数):
$ f_y = f_x + w_{x,y} \times (res - siz_y \times 2)$

\(\Rightarrow\) 代码实现:仍然预处理出 \(siz_i\)\(f_1\),以及注意开 \(long\) \(long\)

让本猫示范下,动口又动手的输出。
#include <iostream>
#include <cstdio>
#define N 100005
#define int long long

using namespace std;

int n,siz[N],c[N],dis[N],ans,f[N],res;

struct Edge{
	int next,to,dis;
}edge[N << 1];
int head[N],cnt;
void add(int from,int to,int dis){
	edge[++cnt] = (Edge){head[from],to,dis};
	head[from] = cnt;
}

void dfs1(int x,int fa){
	siz[x] = c[x];f[1] += dis[x] * c[x];
	for(int i = head[x];i;i = edge[i].next){
		int y = edge[i].to;
		if(y == fa) continue;
		dis[y] = dis[x] + edge[i].dis;
		dfs1(y,x);
		siz[x] += siz[y];
	}
}

void Input(){
	scanf("%lld",&n);
	for(int i = 1;i <= n;i ++) scanf("%lld",&c[i]),res += c[i];
	for(int i = 1,u,v,w;i < n;i ++){
		scanf("%lld%lld%lld",&u,&v,&w);
		add(u,v,w);add(v,u,w);
	}
	dfs1(1,0);
}

void dfs2(int x,int fa){
	for(int i = head[x];i;i = edge[i].next){
		int y = edge[i].to;
		if(y == fa) continue;
		f[y] = f[x] + edge[i].dis * (res - 2 * siz[y]);
		ans = min(ans,f[y]);
		dfs2(y,x);
	}
}

void work(){
	ans = f[1];
	dfs2(1,0);
	printf("%lld",ans);
}

signed main(){
	Input();
	work();
	return 0;
}
posted @ 2023-03-14 17:14  Joy_Dream_Glory  阅读(41)  评论(1编辑  收藏  举报