树形DP
例题
树的直径
一棵树中,最大的\(dist[x][y]\), \(x, y \in V\)
求解方法:
- 任取一个点作为起点\(r\), 找到离\(r\)最远的一个点\(a\)
- 找到一个离\(a\)最远的一个点\(b\)
\(dis[a][b]\)为树的直径
任取一点\(a\)为起点, 找到离他最远的点\(u\), \(u\)一定是直径的某个端点
证明:
设\(dist[b][c]是一个树的直径\), 如下图:
假设我们根据上述条件找到的\(<a, u>\)和 \(<b, c>\) 这两个路径不相交。
显然这种情况下, \(u\)在树的路径中
若两个路径相交:
综上,通过该选法找到的\(u\)一定为树上直径上的一点。
如何进行动态规划:
我们考虑当前点为树的直径上高度最低的一个点\(u\)(最上面的一个点):
令\(dist[v]\)表示从\(v\)到叶子节点的一条路径中的最大值
- 当该点为直径上的终点时,树的直径为\(w(u,v) + dist[v]\), \(v\) 为\(max(dist[v_i]), v_i \in S_u\)
- 当该点位直径上的一个中间点时,树的直径为\(w(u, v) + dist[v_1] + dist[v_2]\) 其中 \(v_i \in S_u\), 且\(v_1\)为\(dist[v_i]\)中最大值,\(v_2\)为次大值
状态转移方程:
#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\)的父节点
#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\)的情况下的最大苹果数量
复杂度 \(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\)
则状态机的转移方程为:
#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]\) 表示该点被覆盖
那么我们可以得到正确的状态转移方程:
#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