• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
jacklee404
Never Stop!
博客园    首页    新随笔    联系   管理    订阅  订阅
树形DP

树形DP

例题

树的直径

题目🔗

一棵树中,最大的\(dist[x][y]\), \(x, y \in V\)

求解方法:

  1. 任取一个点作为起点\(r\), 找到离\(r\)最远的一个点\(a\)
  2. 找到一个离\(a\)最远的一个点\(b\)

\(dis[a][b]\)为树的直径

任取一点\(a\)为起点, 找到离他最远的点\(u\), \(u\)一定是直径的某个端点

证明:

设\(dist[b][c]是一个树的直径\), 如下图:

假设我们根据上述条件找到的\(<a, u>\)和 \(<b, c>\) 这两个路径不相交。

image-20230706213216417

显然这种情况下, \(u\)在树的路径中

若两个路径相交:

image-20230706213834821

综上,通过该选法找到的\(u\)一定为树上直径上的一点。

如何进行动态规划:

我们考虑当前点为树的直径上高度最低的一个点\(u\)(最上面的一个点):

令\(dist[v]\)表示从\(v\)到叶子节点的一条路径中的最大值

  1. 当该点为直径上的终点时,树的直径为\(w(u,v) + dist[v]\), \(v\) 为\(max(dist[v_i]), v_i \in S_u\)
  2. 当该点位直径上的一个中间点时,树的直径为\(w(u, v) + dist[v_1] + dist[v_2]\) 其中 \(v_i \in S_u\), 且\(v_1\)为\(dist[v_i]\)中最大值,\(v_2\)为次大值

状态转移方程:

\[f[u] = dist[v_1] + dist[v_2], v_i \in S_u \\ d_{tree} = max(f[u]),u \in G \]

#include <bits/stdc++.h>

using i64 = long long;

const int N = 1e4 + 10, M = 2e4 + 10;

int n;

int h[N], ne[M], e[M], w[M], idx;

int ans;

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

int dfs(int u, int fa) {
	int d1 = 0, d2 = 0;

	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];

		if (v == fa) continue;

		int d = dfs(v, u) + w[i];

		if (d >= d1) d2 = d1, d1 = d;
		else if (d > d2) d2 = d;
	}

	ans = std::max(ans, d1 + d2);

	return d1;
}

int main() {
	memset(h, -1, sizeof h);

	std::cin >> n;

	for (int i = 1; i < n; i ++) {
		int a, b, c;	

		std::cin >> a >> b >> c;

		add(a, b, c), add(b, a, c);
	}

	dfs(1, -1);

	std::cout << ans;
}

树的中心

题目🔗

对于树\(G<V, E>\), 求树中的任意一点到其他点的最大距离的最小值。

我们先任选一点为起点,对于任一点的路径可以分为向上走的最远距离和向下走的最远距离,我们设\(d1[x], d2[x]\)分别为\(x\)结点向下走的最大值和次大值, \(p1[x]\)为最大值对应的点。

显然我们可以先\(O(V + E)\)的预处理任意点的\(d1[x], d2[x], p1[x]\)值, 对于向上走的路径我们设\(up[x]\)为\(x\)结点向上走的最远距离, 那么我们可以更新:

其中\(fa\)为\(x\)的父节点

\[up[x] = max(d1[fa], up[fa]) + w[i], x \ne p1[x] \\ up[x] = max(d2[fa], up[fa]) + w[i], x = p1[x] \]

#include <bits/stdc++.h>

using i64 = long long;

const int N = 1e4 + 10, M = N * 2, INF = 0x3f3f3f3f;

int h[N], e[M], ne[M], w[M], idx;

int d1[N], d2[N], p1[N], p2[N], up[N];

bool leaf[N];

int n;

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

int dfs1(int u, int fa) {
	if (h[u] == -1) {
		return leaf[u] = true, 0;
	}

	d1[u] = d2[u] = -INF;

	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];

		if (v == fa) continue;

		int d = dfs1(v, u) + w[i];

		if (d >= d1[u]) {
			d2[u] = d1[u], d1[u] = d;
			p1[u] = v;	
		}
		else if (d > d2[u]) d2[u] = d;
	}
	
	return d1[u];
}

void dfs2(int u, int fa) {
	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];

		if (v == fa) continue;

		if (p1[u] == v) up[v] = std::max(up[u], d2[u]) + w[i];
		else up[v] = std::max(up[u], d1[u]) + w[i];

		dfs2(v, u);
	}
}

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

	for (int i = 1; i <= n - 1; i ++) {
		int a, b, c;	

		std::cin >> a >> b >> c;

		add(a, b, c), add(b, a, c);
	}

	dfs1(1, -1);
	dfs2(1, -1);
	

	int ans = INF;

	for (int i = 1; i <= n; i ++) {
		if (leaf[i]) {
			ans = std::min(ans, up[i]);
		} else ans = std::min(ans, std::max(up[i], d1[i]));
	}

	std::cout << ans;
}

数字转换

题目🔗

可以\(O(\ln(n))\)的预处理后,在进行建树

\(O(V + E)\)的求树的直径

#include <bits/stdc++.h>

using i64 = long long;

const int N = 1e5 + 10, M = N * 2;

int n, a[N];

int h[N], ne[M], e[M], idx;

bool st[N];

int ans;

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

void init() {
	for (int i = 1; i <= n; i ++) {
		for (int j = i + i; j <= n; j += i) {
			a[j] += i;
		}
	}

	for (int i = 2; i <= n; i ++) {
		if (i > a[i])
			add(i, a[i]), add(a[i], i);
	}
}

int dfs(int u, int fa) {
	int d1 = 0, d2 = 0;

	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];

		// std::cout << v << "\n";

		if (v == fa) continue;

		st[v] = true;

		int d = dfs(v, u) + 1;

		if (d >= d1) d2 = d1, d1 = d;
		else if (d > d2) d2 = d;
	}

	ans = std::max(ans, d1 + d2);

	return d1;
}

int main() {
	memset(h, -1, sizeof h);

	std::cin >> n;	

	init();

	for (int i = 2; i <= n; i ++) {
		if (!st[i]) {
			dfs(i, -1), st[i] = true;
			// std::cout << ans << "\n";
		}
	}

	std::cout << ans;
}

二叉苹果树

题目🔗

有依赖的背包问题

我们把可以保留的边数看作体积,然后按父节点可以给子节点留下的体积最多是多少,进行分组背包

设\(f[u][j]\)为以\(u\)为根节点,可以保留的边数为\(j\)的情况下的最大苹果数量

\[f[u][j] = max(f[u][j], f[u][j - k - 1] + f[v][k] + w[i]), 0 \le k < j, 0 \le j \le m \]

复杂度 \(O(N \times V \times V)\)

#include <bits/stdc++.h>

using i64 = long long;

const int N = 110, M = N * 2;

int h[N], e[M], ne[M], w[M], idx;

int n, m;

int f[N][N];

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

void dfs(int u, int fa) {
	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];

		if (v == fa) continue;

		dfs(v, u);

		for (int j = m; j >= 0; j --) {
			for (int k = 0; k < j; k ++) {
				f[u][j] = std::max(f[u][j], f[u][j - k - 1] + f[v][k] + w[i]);
			}
		}
	}
}

int main() {
	memset(h, -1, sizeof h);

	std::cin >> n >> m;

	for (int i = 1; i < n; i ++) {
		int a, b, c;
		
		std::cin >> a >> b >> c;

		add(a, b, c), add(b, a, c);	
	}

	dfs(1, -1);


	std::cout << f[1][m];
}

战略游戏

题目🔗

没有上司的舞会 是每条边上最多选择一个点。最大权值

战略游戏 是 每条边上最少选择一个点。最大权值

我们设

在以\(i\)为根的子树中:

\(f[i][0]\) 表示选定不选\(i\)点的最小代价

\(f[i][1]\) 表示选定\(i\)点的最小代价

按照题目的设定条件,每个边都需要被选定的点覆盖, 设\(u\)的儿子结点集合为\(S_u\)

则状态机的转移方程为:

\[f[u][0] = \sum \limits_{v \in S_u} f[v][1] \\ f[u][1] = \sum \limits_{v \in S_u} max(f[u][0], f[u][1]) \]

#include <bits/stdc++.h>

using i64 = long long;

const int N = 1510, M = N;

int h[N], e[M], ne[M], idx;
int f[N][2];

bool st[N];

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

void dfs(int u) {
	f[u][0] = 0;
	f[u][1] = 1;

	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];

		dfs(v);

		f[u][0] += f[v][1];
		f[u][1] += std::min(f[v][0], f[v][1]);
	}
}

int main() {

	int n;

	while (std::cin >> n) {
		memset(h, -1, sizeof h);
		memset(st, false, sizeof st);

		idx = 0;

		for (int i = 1; i <= n; i ++) {
			int id, cnt;
			
			scanf("%d:(%d)", &id, &cnt);

			while (cnt --) {
				int ver;

				scanf("%d", &ver);

				add(id, ver);

				st[ver] = true;
			}	
		}

		int root = 0;

		while (st[root]) root ++;

		dfs(root);

		printf("%d\n", std::min(f[root][0], f[root][1]));
	}
}

皇宫看守

题目🔗

与上一个题目不同的是,这里是点覆盖点。

在点覆盖边中,如果一个父节点没有被选择,那么他的所有子节点都要被选择去覆盖与父节点相连的边。

但是在点覆盖点当中,一个父节点没有被选择,那么他的子节点至少有一个被选择,是一个组合问题。

我们考虑状态转移,设定状态机:

属性都为\(Min\)代价

\(f[u][0]\) 表示该点被其父节点覆盖

\(f[u][1]\) 表示该点被其子节点覆盖

\(f[u][2]\) 表示该点被覆盖

那么我们可以得到正确的状态转移方程:

\[f[u][0] = \sum \limits_{v \in S_u} min(f[v][2], f[v][1]) \\ f[u][2] = \sum \limits_{v \in S_u} min(f[v][0], f[v][1], f[v][2]) \\ f[u][1] = \sum \limits_{v \in S_u, k \in S_u, v \ne k} f[k][2] + min(f[v][1], f[v][2]), \\ \]

#include <bits/stdc++.h>

using i64 = long long;

const int N = 1510;

int h[N], e[N], ne[N], w[N], idx;

int n;

bool st[N];

int f[N][3];

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

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

	int sum = 0;

	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];

		dfs(v);
		
		f[u][0] += std::min(f[v][2], f[v][1]);	
		f[u][2] += std::min(f[v][0], std::min(f[v][1], f[v][2]));

		sum += std::min(f[v][1], f[v][2]);
	}

	f[u][1] = 1e9;

	for (int i = h[u]; ~i; i = ne[i]) {
		int v = e[i];
		
		f[u][1] = std::min(f[u][1], sum - std::min(f[v][1], f[v][2]) + f[v][2]);
	}
}


int main() {
	std::cin >> n;

	memset(h, -1, sizeof h);

	for (int i = 1; i <= n; i ++) {
		int id, cost, cnt;
		
		std::cin >> id >> cost >> cnt;

		w[id] = cost;

		while (cnt --) {
			int ver;
			
			std::cin >> ver;

			add(id, ver);

			st[ver] = true;					
		}
	}

	int root = 1;

	while (st[root]) root ++;

	dfs(root);

	std::cout << std::min(f[root][1], f[root][2]);shuwei 
posted on 2023-07-09 21:42  Jack404  阅读(18)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3