树形DP详细解析

1.基本定义

树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。

2.模板题

Acwing 285. 没有上司的舞会

思路

我们设 f(i,0/1) 代表以 i 为根的子树的最优解(第二维的值为 0 代表 i 不参加舞会的情况,1 代表 i 参加舞会的情况)。

对于每个状态,都存在两种决策(其中下面的 x 都是 i 的儿子):

  • 1.上司不参加舞会时,下属可以参加,也可以不参加,此时有 f(i,0)=max{f(x,1),f(x,0)}

  • 2.上司参加舞会时,下属都不会参加,此时有 f(i,1)=f(x,0)+ai

于是我们可以通过 DFS,在返回上一层时更新当前结点的最优解。

代码

#include <iostream>
#include <algorithm>
#include <cstring>
//#define int long long

using namespace std;

#define N 6010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()

int n, m, idx = 0, ok[N], vis[N], dp[N][2];
int h[N], e[N], ne[N];

void add (int a, int b) {
	e[++ idx] = b, ne[idx] = h[a], h[a] = idx;
} //建边

void dfs (int dep) {
	vis[dep] = 1; //将这个点打上标记

	for (int i = h[dep]; i; i = ne[i]) {
		if (vis[e[i]]) continue; //如果用过,跳出
		dfs (e[i]); //递归枚举子树
		dp[dep][1] += dp[e[i]][0]; //使用这个点的价值首先包含不使用这个点的价值
		dp[dep][0] += max (dp[e[i]][0], dp[e[i]][1]); //不取这个点的价值包含选或不选子节点的最大价值
	}
}

int main () {
	IOS;
	cin >> n;
	For (i, 1, n) {
		cin >> dp[i][1];
	} //输入值可视为使用第 i 个点的最大价值

	For (i, 1, n - 1) {
		int x, y;
		cin >> x >> y; //建边,同时标记 x 点有父亲
		ok[x] = 1, add (y, x);
	}

	For (i, 1, n) {
		if (!ok[i]) { 
			dfs (i); //如果发现根节点,就递归查找
			cout << max (dp[i][1], dp[i][0]) << endl;
			//输出选或不选根节点的最大值
			return 0; //可以结束了
		}
	}

	return 0;
}

3.例题1:树上最长距离

题目

Acwing 1072. 树的最长路径

思路

树的直径模板题,经典思路如下:

  • 1.在树上任意取一点,找出距离该点最远的点 u

  • 2.找出距离 u 点最远的点 v

  • 3.uv 的距离即为树上最长路径。

证明方法也很简单,用反证法加分类讨论即可快速证明,这里不再多说。。。

但这种做法只适用于不带边权的树的直径,但显然本题不是这样的,而且边权可能为负。所以我们应采用树形 DP 法:

首先我们知道,一棵有根树的直径其实就是这棵树的若干棵子树中的最大树和次大树的价值之和,

所以我们可以从 1 号节点开始,以每个节点为根,找到他的最大子树和次大子树之和,再在所有答案中取最大值即可。

代码

#include <iostream>
#include <algorithm>
#include <cstring>
//#define int long long

using namespace std;

#define N 20010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()

int n, ans = 0;
int h[N], e[N], w[N], ne[N], idx = 1;

void add (int a, int b, int c) {
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
} //邻接表建边

int dfs (int u, int fa) {
	int dist = 0; //表示从当前点往下走的最大长度
	int d_fir = 0, d_sec = 0; //存最大价值与次大价值

	for (int i = h[u]; i != -1; i = ne[i]) {
		if (e[i] == fa) continue; 
		//注意,因为是无向边,会出现自循环的情况。所以要判断如果找到了父亲节点,就直接跳过。
		int j = e[i], t = dfs (j, u) + w[i]; //j是子节点,t是从这棵子树能得到的最大价值
		dist = max (dist, t); //更新dist

		if (t >= d_fir) d_sec = d_fir, d_fir = t;
		//如果这个价值比当前最大值还要大,就将之前次大值赋为之前最大值,之前最大值赋为当前最大值
		else if (t > d_sec) d_sec = t;
		//如果这个价值之比次大值大,就只更新次大值
	}

	ans = max (ans, d_fir + d_sec);
	//用最大值+次大值更新答案
	return dist;
}

int main () {
	IOS;
	memset (h, -1, sizeof h);
	cin >> n;

	For (i, 1, n - 1) {
		int a, b, c;
		cin >> a >> b >> c;
		add (a, b, c), add (b, a, c);
		//建边
	}

	dfs (1, -1); //从1开始枚举,它的父亲点不存在,所以定为-1
	cout << ans << endl;
	return 0;
}

4.例题2:树的中心

题目

Acwing 1073. 树的中心

思路

先来看一组样例;

image

现在如果我们对 2 分析,发现有 4 条边与之相连,而这四条边又可以分成两大类:往子节点走和往父亲节点走。

往子节点走很好计算,用上一题所讲的方法即可;现在要解决的就是往父亲节点走的最大距离。

我们发现,往上无论怎么走,第一步肯定要先走到 1 号点。然后此时又分为两种情况:

  • 1.从父节点出发的最长路径不经过 2 号点:很简单,直接使用这个最大值作为最远距离即可;

  • 2.从父节点出发的最长路径要经过 2 号点:因为不能走重复路线,所以只能使用次大值作为最远距离。

所以,我们要做两遍树形 DP,分别向下和向上走,最后的最远距离就是向下和向上走距离的最大值。

代码

#include <iostream>
#include <algorithm>
#include <cstring>
//#define int long long

using namespace std;

#define N 20010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()

int n, d1[N], d2[N], up[N], p1[N], p2[N];
int h[N], e[N], w[N], ne[N], idx = 1;
//p1[],p2[] 用于存储每一个节点的最长路径和次长路径是从哪个点经过的

void add (int a, int b, int c) {
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
} //建边

int dfs_d (int u, int fa) { //大部分是上一题代码
	d1[u] = d2[u] = -0x3f3f3f3f;

	for (int i = h[u]; i; i = ne[i]) {
		int j = e[i];
		if (j == fa) continue;

		int t = dfs_d (j, u) + w[i];
		if (t >= d1[u]) {
			d2[u] = d1[u], d1[u] = t;
			p2[u] = p1[u], p1[u] = j;
			//原次长距离更新为原最长距离,原最长距离更新为现最长距离
		} else if (t > d2[u]) d2[u] = t, p2[u] = j; //只更新次长距离
	}

	if (d1[u] == -0x3f3f3f3f) d1[u] = d2[u] = 0;
	//如果之前的循环并没有更新d1和d2,即u是叶子结点,就将其最长距离和次长距离设为0
	return d1[u]; //返回最长距离
}

void dfs_u (int u, int fa) {
	for (int i = h[u]; i; i = ne[i]) {
		int j = e[i];
		if (j == fa) continue;
		//如果从u出发最长距离经过了这个儿子,就用次长距离更新
		if (p1[u] == j) up[j] = max (up[u], d2[u]) + w[i];
		else up[j] = max (up[u], d1[u]) + w[i];
		//否则用最长距离更新
		dfs_u (j, u);
	}
}

int main () {
	IOS;
	memset (h, -1, sizeof h);
	cin >> n;

	For (i, 1, n - 1) {
		int a, b, c;
		cin >> a >> b >> c;
		add (a, b, c), add (b, a, c);
	}

	dfs_d (1, -1);
	dfs_u (1, -1);
	
	int res = 0x3f3f3f3f;
	For (i, 1, n) {
		res = min (res, max (d1[i], up[i]));
	}

	cout << res << endl;
	return 0;
}

5.例题3:二叉苹果树

题目

Acwing 1074. 二叉苹果树

思路

我们用 dp(i,j) 表示以 i 为根,选择 j 条树枝时的最大价值。不难发现这其实就是一个分组背包问题的变式。

然后我们就可以轻松加愉快的在树形 dp 里再套一个分组背包就可以啦!

代码

#include <iostream>
#include <algorithm>
#include <cstring>
//#define int long long

using namespace std;

#define N 510
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()

int n, m, dp[N][N];
int h[N], e[N], w[N], ne[N], idx = 1;

void add (int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
} //建边

void dfs (int u, int fa) {
    for (int i = h[u]; i != -1; i = ne[i]) { //枚举每一组
		if (e[i] == fa) continue;
		dfs (e[i], u);
		for (int j = m; j >= 0; j --) //倒序枚举体积
			for (int k = 0; k < j; k ++) //枚举决策,即选择第 k 组时的情况
				dp[u][j] = max (dp[u][j], dp[u][j - k - 1] + dp[e[i]][k] + w[i]);
				//状态转移方程,也很好推。。。
	}
}

int main () {
    IOS;
    memset (h, -1, sizeof h);
    cin >> n >> m;

	For (i, 1, n - 1) {
		int a, b, c;
		cin >> a >> b >> c;
		add (a, b, c), add (b, a, c);
	}

	dfs (1, -1);
	cout << dp[1][m] << endl;
	//答案即为以 1 为根,选择 m 条树枝的最大价值
    return 0;
}

6.例题4:战略游戏

题目

Acwing 323. 战略游戏

思路

这道题的实质就是 在每条边上最少选择一个点,求最小权值

那么我们很容易联想到我们的模板题。它的实质就是 在每条边上最多选择一个点,求最大权值

于是我们采用 闫式DP分析法

状态表示 dp(i,j)(j=0/1)

  • 1.集合:在所有以 i 为根的子树中选,且点 i 的状态为 j 的所有选法;

  • 2.属性:Min

状态计算:

  • dp(i,0):因为每条边上最少选择一个点,而第 i 个节点没有选,所以它的子节点必须全选,即dp(i,0)=min(dp(i,0),j=1kdp(sj,1))(k)

  • dp(i,1):因为选择了第 i 个点,所以它的子节点可选可不选。所以要在选与不选间取较小值再来更新 dp(i,1),即:dp(i,1)=min(j=1kmin(dp(sj,0),dp(sj,1)))

代码

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
//#define int long long

using namespace std;

#define N 100010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()

int n, st[N], dp[N][2];
int h[N], e[N], ne[N], idx = 0;

void add (int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void dfs (int u) {
    dp[u][0] = 0;
    dp[u][1] = 1; //清空一下
    
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        dfs (j);
        
        dp[u][0] += dp[j][1]; //状态转移公式
        dp[u][1] += min (dp[j][0], dp[j][1]);
    }
}

int main () {
    IOS;
    while (~scanf ("%d", &n)) {
        memset (h, -1, sizeof h);
        memset (st, 0, sizeof st);
        idx = 0; //清空一下,有多组数据
        
        For (i, 1, n) {
            int id, ver, cnt;
            scanf ("%d:(%d) ", &id, &cnt);
        	//由于这题的输入实在太恶心了,所以建议使用scanf格式化读入
            while (cnt --) {
                scanf ("%d", &ver);
                add (id, ver);
                st[ver] = 1; //标记子节点
            }
        }
        
        int root = 0;
        while (st[root]) {
            root ++;
        } 
        
        dfs (root); 
        cout << min (dp[root][0], dp[root][1]) << endl;
    }
    return 0;
}

7.例题5:皇宫看守

题目

Acwing 1077. 皇宫看守

思路

不难发现,本题与上一题相差不大。只是由原来的观察边变成了观察点。

那么我们可以用 dp(i,j)(j=0/1/2) 表示集合:dp(i,0) 表示点 i 被父亲节点观察到的最小花费,dp(i,1) 表示点 i 被子节点观察到的最小花费,dp(i,2) 表示点 i 被自己观察到的最小花费(即在 i 上安排警卫的最小花费)。

然后我们来推一下公式:

  • 1.dp(i,0)=min{dp(j,1),dp(j,2)}

  • 2.dp(i,2)=min{dp(j,0),dp(j,1),dp(j,2)}

  • 3.dp(i,0)=mink{dp(k,2)+jkmin{dp(j,1),dp(j,2)}}

代码

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
//#define int long long

using namespace std;

#define N 1510
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()

int n, st[N], w[N], dp[N][3];
int h[N], e[N], ne[N], idx = 1;

void add (int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void dfs (int u) {
	dp[u][2] = w[u];

	for (int i = h[u]; i; i = ne[i]) {
		int j = e[i];
		dfs (j);

		dp[u][0] += min (dp[j][1], dp[j][2]);
		dp[u][2] += min (min (dp[j][0], dp[j][1]), dp[j][2]);
	}

	dp[u][1] = 1e9;
	for (int i = h[u]; i; i = ne[i]) {
		int j = e[i];
		dp[u][1] = min (dp[u][1], dp[j][2] + dp[u][0] - min (dp[j][1], dp[j][2]));
	}
}

int main () {
    IOS;
    cin >> n;

	For (i, 1, n) {
		int id, cost, cnt;
		cin >> id >> cost >> cnt;
		w[id] = cost;

		while (cnt --) {
			int ver;
			cin >> ver;
			add (id, ver);
			st[ver] = 1;
		}
	}

	int root = 1;
	while (st[root]) root ++;

	dfs (root);
	cout << min (dp[root][1], dp[root][2]) << endl;
    return 0;
}

posted @   linbaicheng2022  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示