2025 Summer Day8
Content:DP (Interval, Tree)
Date:2025.7.24
概览
- 区间 DP
- 树形 DP
例题
洛谷-P4516 潜入行动
题目大意
题目大意
给定一颗树,要求在树上选取恰好 个节点 (不得重复),每个选取的节点可以覆盖它的邻居,但是不能覆盖自己本身。要求选取的这 个节点覆盖所有的 个节点。求方案数。 数据范围 。
思路
我们考虑树形动态规划。定义状态 表示以 为根的子树内,选取了 个节点,其中点 放了/不放,被/不被覆盖的方案数。初始状态为:
可以发现如果只看前两维的话,是一个典型的树形背包问题。接下来我们考虑加上后两维后如何转移 (本质还是树形背包)。
对于 ,即点 既不选择,也 不覆盖,所以 的子节点 一定不能选择,而 必须 覆盖,所以其转移如下:
对于 ,即点 不选择,但是被覆盖。因为状态为点 被覆盖,所以既可以是 覆盖的,也可能是 的其他子节点覆盖的,而因为 不被选择,所以 一定是已经被覆盖了的,所以其转移如下:
对于 ,即点 选择了,但还没有被覆盖。因为不被覆盖,所以点 一定不能被选择,而点 已经被选择了,所以点 覆不覆盖无所谓。转移如下:
对于 ,即点 选择了,也被覆盖了的情况。由 两种情况类似的推导就可以得到。转移为:
最后,注意 取模 和 long long!!!
Code
#include <bits/stdc++.h>
namespace IO { template<typename name> name read() { name x = 0, f = 1; char ch = getchar(); while (!isdigit(ch)) { if (ch == '-') f = -1; ch = getchar(); } while (isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } return x * f; } template<typename name> void _write(name x) { if (x > 9) _write(x / 10); putchar(x % 10 + '0'); } template<typename name> void write(name x) { if (x < 0) putchar('-'), x = -x; _write(x); }}
constexpr int N = 1e5 + 5, K = 105, MOD = 1e9 + 7;int head[N], next[N << 1], to[N << 1], cnt = 0;int dp[N][K][2][2], size[N], tmp[K][2][2];int n, k, u, v;
inline void AddEdge(const int u, const int v) { to[cnt] = v; next[cnt] = head[u]; head[u] = cnt++;}
inline void Dfs(int u, int father) { dp[u][0][0][0] = dp[u][1][1][0] = 1; size[u] = 1;
for (int i = head[u]; ~i; i = next[i]) { const int v = to[i]; if (v == father) continue;
Dfs(v, u); size[u] += size[v];
// ! 这里要进行备份,不然转移的时候会出现问题(用已修改的状态修改未修改的状态) for (int t = 0; t <= std::min(k, size[u]); t++) { tmp[t][0][0] = dp[u][t][0][0]; tmp[t][1][0] = dp[u][t][1][0]; tmp[t][0][1] = dp[u][t][0][1]; tmp[t][1][1] = dp[u][t][1][1]; dp[u][t][0][0] = dp[u][t][1][0] = dp[u][t][0][1] = dp[u][t][1][1] = 0; }
// ! 注意枚举范围,不能直接枚举区间 [0,k],不然会 TLE for (int t = 0; t <= std::min(size[u], k); t++) { // ! 这里同理 for (int j = 0; j <= std::min(size[v], t); j++) { if (t - j > size[u] - size[v]) continue;
// // 可恶的转移 (dp[u][t][0][0] += (long long) tmp[t - j][0][0] * dp[v][j][0][1] % MOD) %= MOD; (dp[u][t][0][1] += ((long long) tmp[t - j][0][1] * ((dp[v][j][0][1] + dp[v][j][1][1]) % MOD) % MOD + (long long) tmp[t - j][0][0] * dp[v][j][1][1] % MOD) % MOD) %= MOD; (dp[u][t][1][0] += (long long) tmp[t - j][1][0] * ((dp[v][j][0][1] + dp[v][j][0][0]) % MOD) % MOD) %= MOD; (dp[u][t][1][1] += ((long long) (tmp[t - j][1][1] * (((long long) dp[v][j][0][1] + dp[v][j][0][0] + dp[v][j][1][0] + dp[v][j][1][1]) % MOD) % MOD) + (long long) tmp[t - j][1][0] * ((dp[v][j][1][1] + dp[v][j][1][0]) % MOD) % MOD) % MOD) %= MOD; } } }}
int main() { using namespace IO;
n = read<int>(); k = read<int>();
for (int i = 1; i <= n; i++) head[i] = -1;
for (int i = 1; i < n; i++) { u = read<int>(), v = read<int>(); AddEdge(u, v); AddEdge(v, u); }
Dfs(1, 1);
write((1ll * dp[1][k][0][1] + dp[1][k][1][1]) % MOD);
return 0;}洛谷-P1352 没有上司的舞会 (经典题)
题目大意
题目大意
给定一颗有 个节点的树,表示员工的架构,现在有一个舞会需要举办,你要选取一些人参加,第 个人参加可以为舞会带来 的快乐值,但是如果第 个人的直接上司 ( 的父亲节点) 参加了,那么 就不会参加,你要最大化选择的人的快乐值之和。 数据范围:。
思路
定义状态 表示节点 参加/不参加能为舞会带来的最大价值。
转移如下:
注意我的树存的是双向边,和边有关的数组要开 倍。
Code
Code
#include <bits/stdc++.h>
using std::cin;using std::cout;
constexpr int MAXN = 6e4 + 5;int head[MAXN], to[MAXN], next[MAXN], cnt = 0;int n, r[MAXN], u, v;int dp[MAXN][2];
void add_edge(const int u, const int v) { to[cnt] = v; next[cnt] = head[u]; head[u] = cnt++;}
void dfs(const int u, const int father) { dp[u][1] = r[u];
for (int i = head[u]; ~i; i = next[i]) { const int v = to[i]; if (v == father) continue;
dfs(v, u); dp[u][0] += std::max(dp[v][0], dp[v][1]); dp[u][1] += dp[v][0]; }}
int main() { std::ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
std::memset(head, -1, sizeof(head));
cin >> n;
for (int i = 1; i <= n; i++) { cin >> r[i]; }
for (int i = 1; i < n; i++) { cin >> u >> v; add_edge(u, v); add_edge(v, u); }
dfs(1, 1);
cout << std::max(dp[1][0], dp[1][1]) << '\n';
return 0;}洛谷-P3478 消防局的 STA-Station
题目大意
P3478 [POI 2008] STA-Station 题目大意
给定一个 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。 一个结点的深度之定义为该节点到根的简单路径上边的数量。 数据范围:.
思路
换根 DP。设 表示以 为根节点的时候所有节点的深度之和是多少。换根一般都有两个 ,我们在第一遍 的时候记录节点的深度 ,子树大小 ,然后 的状态为:。
在第二遍 时,我们自上而下更新 的值,转移为:
即子树 内所有点的深度减一,子树外的所有深度加一。
注意 long long!!!
Code
Code
#include <bits/stdc++.h>
using std::cin;using std::cout;
constexpr int N = 1e6 + 5;int head[N], next[N << 1], to[N << 1], cnt = 0;int n, u, v, dp[N], dep[N], size[N];
void AddEdge(const int u, const int v) { to[cnt] = v; next[cnt] = head[u]; head[u] = cnt++;}
void Dfs1(const int u, const int father) { dp[u] += dep[u]; dep[u] = dep[father] + 1; size[N] = 1;
for (register int i = head[u]; ~i; i = next[i]) { register const int v = to[i]; if (v == father) continue;
Dfs1(v, u); size[u] += size[v]; }}
void Dfs2(const int u, const int father) { for (register int i = head[u]; ~i; i = next[i]) { register const int v = to[i]; if (v == father) continue;
dp[v] = dp[u] - size[v] + n - size[v]; Dfs2(v, u); }}
int main() { std::ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
std::memset(head, -1, sizeof(head));
cin >> n;
for (int i = 1; i < n; i++) { cin >> u >> v;
AddEdge(u, v); AddEdge(v, u); }
Dfs1(1, 1); Dfs2(1, 1);
int ans = 1; for (int i = 2; i <= n; i++) { if (dp[i] > dp[ans]) ans = i; }
cout << ans << '\n';
return 0;}
Comments
Quiet notes for this article.