「REMAKE系列」树形dp篇
总结
树上背包
- 树上背包计数 \(I\) P4516 [JSOI2018] 潜入行动
基环树上dp
- 树上最大独及集 + 基环树 P2607 [ZJOI2008] 骑士
树上路径
- \(n,m\leq2000\) ,要求选择一些路径,使得每个点至多在一条路径上,并且路径的权值和最大。代码源树上路径1
换根dp
基础树形dp
- 选取最少点位,每个选取点位能是周围点和自己加入集合,最少点位数让所有点在集合中。P2899 [USACO08JAN]Cell Phone Network G
习题
一些做对了一半的题
CF533B. Work Group
状态设计的差不多,自己的转移太复杂而且玄学,应该重新想想是不是可以简化。
const int N = 2e5 + 10;
vector<int> edge[N];
ll n, a[N], f[N][2];
// f[u][0 / 1] 从 u 的子树中选 偶数/奇数 个点。
void dfs(int u) {
f[u][1] = -2e18;
for (auto v: edge[u]) {
dfs(v);
ll x = f[u][0], y = f[u][1];
f[u][0] = max(y + f[v][1], x + f[v][0]);
f[u][1] = max(y + f[v][0], x + f[v][1]);
}
f[u][1] = max(f[u][1], f[u][0] + a[u]);
}
int main() {
re(n);
for (int i = 1; i <= n; i++) {
int p;
re(p), re(a[i]);
if (p != -1) edge[p].pb(i);
}
dfs(1);
printf("%lld\n", max(f[1][0], f[1][1]));
return 0;
}
洛谷P2899 [USACO08JAN]Cell Phone Network G
题意
John 想让他的所有牛用上手机以便相互交流,他需要建立几座信号塔在 \(N\) 块草地中。已知与信号塔相邻的草地能收到信号。给你 \(N-1\) 个草地(A,B)的相邻关系,问:最少需要建多少个信号塔能实现所有草地都有信号。
思路
- 写的时候少加了一个状态,然后加上原来的思路就ac了。
- 看起来和没有上司的舞会有点像其实有一点区别,对于每个节点被儿子染色时,儿子节点可以放也可以不放,每个节点还可以被父亲染色。
- 设计状态 \(f_{u,0}\) 表示节点被儿子染色自己不放,\(f_{u,1}\) 表示结点自己放来染色自己,\(f_{u,2}\) 表示节点自己不放被父亲染色。
- 转移写在代码里,注意的点是,节点被儿子染色,只需要大于等于 1 个儿子放就可以了,所以需要记录最优的情况。
- 实现:如果存在一个儿子放比不放其值更优,就简单了,如果不存在就找一个 放与不放 差值最小的来替代。
const int N = 1e4 + 10, INF = 1e9;
vector<int> edge[N];
int n;
int f[N][3], leaf[N]; // 0, 儿子放 自己不放,1 自己放, 2 父亲放 儿子不放。
void dfs(int u, int p) {
f[u][1] = 1;
leaf[u] = true;
int sum = 0, d = INF;
vector<int> tmp;
bool find = false;
for (auto v: edge[u]) {
if (v == p) continue;
dfs(v, u);
leaf[u] = false;
if (f[v][1] <= f[v][0]) {
find = true;
f[u][0] += f[v][1];
}
else {
f[u][0] += f[v][0];
d = min(d, f[v][1] - f[v][0]);
}
f[u][1] += min({f[v][0], f[v][1], f[v][2]});
f[u][2] += min(f[v][1], f[v][0]);
}
if (!find)
f[u][0] += d;
}
/*
1
|
3
|\
5 4
|
2
*/
int main() {
re(n);
for (int i = 1; i < n; i++) {
int a, b;
re(a), re(b);
edge[a].pb(b), edge[b].pb(a);
}
dfs(1, 0);
printf("%d\n", min({f[1][0], f[1][1]}));
return 0;
}
dls动态规划中级
树上路径1
- \(dp[u]\) 表示当前 u 点对应路径最高点的路径选或不选的最大值。
- u 点没有路径,则最大值为儿子节点 dp 权值和。
- u 点有路径,要与路径上节点儿子权值加和后取 \(\max\) ,路径上的点 dp 权值和不计入。
- 所以有一个巧妙办法,选取了一段路径,对于路径上的点可以等价于 \(sdp_i-dp_i\) 。节点儿子dp权值和减去节点dp权值和。
- 以上可以用 BIT + DFS序优化到 nlogn 级别。
const int N = 2e3 + 10;
vector<int> edge[N];
vector<array<int, 3>> path[N];
int fa[N], n, m, dep[N];
ll dp[N], sdp[N];
void dfs(int u) {
for (auto v: edge[u]) {
dfs(v);
sdp[u] += dp[v];
}
dp[u] = sdp[u];
ll t = 0;
for (auto p: path[u]) {
ll tmp = 0;
int x = p[0];
while (x != u) {
tmp += sdp[x] - dp[x];
x = fa[x];
}
x = p[1];
while (x != u) {
tmp += sdp[x] - dp[x];
x = fa[x];
}
tmp += p[2];
t = max(t, tmp);
}
dp[u] += t;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 2; i <= n; i++) {
scanf("%d", &fa[i]);
edge[fa[i]].pb(i);
dep[i] = dep[fa[i]] + 1;
}
for (int i = 0, u, v, a; i < m; i++) {
scanf("%d%d%d", &u, &v, &a);
int x = u, y = v;
while (x != y) {
if (dep[x] > dep[y]) x = fa[x];
else y = fa[y];
}
path[x].pb({u, v, a});
}
dfs(1);
printf("%lld\n", dp[1]);
return 0;
}
洛谷——「能力综合提升题单-树形DP篇」
P3047 [USACO12FEB]Nearby Cows G
提高+/省选- , 简单容斥
给你一棵 \(n\) 个点的树,点带权,对于每个节点求出距离它不超过 \(k\) 的所有节点权值和 \(m_i\) 。
思路
- 定义状态 \(f[u][j]\) 距离点 u 距离不超过 j 的点权和。
- 第一次 dfs 转移:
f[u][j] += f[v][j - 1]
- 第二次 dfs 转移,由父节点更新子节点有点类似换根dp,但需要减去父节点和子节点的子树中重合的部分。
j:n->2, f[v][j] -= f[v][j - 2]
f[v][j] += f[u][j - 1]
ll f[N][25];
void dfs1(int u, int p) {
f[u][0] = c[u];
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == p) continue;
dfs1(v, u);
for (int j = 1; j <= k; j++) {
f[u][j] += f[v][j - 1];
}
}
}
void dfs2(int u, int p) {
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (v == p) continue;
for (int j = k; j >= 2; j--)
f[v][j] -= f[v][j - 2];
for (int j = 1; j <= k; j++)
f[v][j] += f[u][j - 1];
dfs2(v, u);
}
}
int main() {
init();
re(n), re(k);
for (int i = 1; i < n; i++) {
int a, b;
re(a), re(b);
add(a, b), add(b, a);
}
for (int i = 1; i <= n; i++)
re(c[i]);
dfs1(1, 0);
dfs2(1, 0);
for (int i = 1; i <= n; i++) {
ll ans = 0;
for (int j = 0; j <= k; j++)
ans += f[i][j];
printf("%lld\n", ans);
}
return 0;
}
P3698 [CQOI2017]小Q的棋盘
提高+/省选- ,简单树上背包
对于一颗树,现在想知道,当棋子从格点 0 出发,移动 N 步最多能经过多少格点。格点可以重复经过多次,但不重复计数。
const int N = 110;
vector<int> edge[N];
int n, m;
// f[u][j] 从 u 点选 j 个点的花费,并回到 u 点
// g[u][j] 从 u 点选 j 个点的花费,不回到 u 点。
// j: n->1 : f[u][j] = f[u][j - k] + f[v][k] + 1
// g[u][j] = min(g[u][j - k] + f[v][k], f[u][j - k] + g[v][k]) + 1;
int f[N][N], g[N][N], tf[N], tg[N];
void dfs(int u, int p) {
f[u][1] = g[u][1] = 0;
for (auto v: edge[u]) {
if (v == p) continue;
dfs(v, u);
for (int i = 1; i <= n; i++) {
tf[i] = f[u][i];
tg[i] = g[u][i];
}
for (int j = n; j >= 1; j--) {
for (int k = 1; k <= j - 1; k++) {
f[u][j] = min(f[u][j], tf[j - k] + f[v][k] + 2);
g[u][j] = min({g[u][j], tg[j - k] + f[v][k] + 2, g[v][k] + tf[j - k] + 1});
}
}
}
}
int main() {
re(n), re(m);
memset(f, 0x3f, sizeof f);
memset(g, 0x3f, sizeof g);
for (int i = 1, a, b; i < n; i++) {
re(a), re(b);
edge[a].pb(b), edge[b].pb(a);
}
dfs(0, -1);
for (int i = n; i >= 1; i--) {
if (g[0][i] <= m) {
printf("%d\n", i);
break;
}
}
return 0;
}
P3177 [HAOI2015] 树上染色
提高+/省选- 树上背包、细节题、贡献考虑
有一棵点数为 \(n\) 的树,树边有边权。给你一个在 \(0 \sim n\) 之内的正整数 \(k\) ,你要在这棵树中选择 \(k\) 个点,将其染成黑色,并将其他 的 \(n-k\) 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的受益。问受益最大值是多少。
对于 \(100\%\) 的数据,\(0 \leq n,k \leq 2000\)。
思路
- 直接设状态不好求,考虑按贡献求解,求出对于某一条边被经过了多少次。
- 显然是边左右两侧同色点的对数乘积。有一些细节,看代码实现就行。
const int N = 2e3 + 10;
vector<PLL> edge[N];
ll f[N][N];
int n, K;
int sz[N];
// f[u][j] u 从选取 j 个黑点
void dfs(int u, int p) {
sz[u] = 1;
f[u][0] = f[u][1] = 0;
for (auto [v, w]: edge[u]) {
if (v == p) continue;
dfs(v, u);
sz[u] += sz[v];
for (int j = min(sz[u], K); j >= 0; j--) {
if (f[u][j] != -1) {
ll tot = sz[v] * (n - K - sz[v]);
f[u][j] += f[v][0] + tot * w;
}
for (int k = min(j, sz[v]); k > 0; k--) {
if (f[u][j - k] == -1) continue;
ll tot = k * (K - k) + (sz[v] - k) * (n - K - (sz[v] - k));
f[u][j] = max(f[u][j], f[u][j - k] + f[v][k] + tot * w);
}
}
}
}
int main() {
re(n), re(K);
memset(f, -1, sizeof f);
for (int i = 1; i < n; i++) {
int a, b, c;
re(a), re(b), re(c);
edge[a].pb({b, c}), edge[b].pb({a, c});
}
dfs(1, 0);
printf("%lld\n", f[1][K]);
return 0;
}
P2607 [ZJOI2008] 骑士
const int N = 1e6 + 10;
const ll INF = 2e18;
ll n, a[N], fa[N], dp[N][2], dp2[N][2];
bool vis[N], oncyc[N];
vector<int> edge[N];
void dfs(int u) {
vis[u] = true;
dp[u][1] = a[u];
for (auto v: edge[u]) {
if (oncyc[v]) continue; // 在环上跳过
dfs(v);
dp[u][0] += max(dp[v][0], dp[v][1]);
dp[u][1] += dp[v][0];
}
}
int main() {
re(n);
for (int i = 1, p; i <= n; i++) {
re(a[i]), re(p);
edge[p].pb(i);
fa[i] = p;
}
ll ans = 0;
// 对于每个连通分量求解
for (int i = 1; i <= n; i++) {
if (vis[i]) continue;
// 找出每个环
int now = i;
while (!vis[now]) {
vis[now] = true;
now = fa[now];
}
vector<int> cyc;
while (!oncyc[now]) {
oncyc[now] = true;
cyc.pb(now);
now = fa[now];
}
// 对非环节点树形 dp
for (auto u: cyc)
dfs(u);
// 环上dp
int m = SZ(cyc);
ll res = -INF;
for (int t = 0; t < 2; t++) {
for (int j = 0; j < 2; j++) {
if (t == j) dp2[0][j] = dp[cyc[0]][t];
else dp2[0][j] = -INF;
}
for (int i = 1; i < m; i++) {
dp2[i][0] = max(dp2[i - 1][0], dp2[i - 1][1]) + dp[cyc[i]][0];
dp2[i][1] = dp2[i - 1][0] + dp[cyc[i]][1];
}
if (t == 0) res = max(dp2[m - 1][0], dp2[m - 1][1]);
else res = max(res, dp2[m - 1][0]);
}
ans += res;
}
printf("%lld\n", ans);
return 0;
}
P4516 [JSOI2018] 潜入行动
题意略
- 设状态为 \(dp[u][j][0/1][0/1]\) ,u 点子树放了 j 个装置,u 点有没有放装置,u 点有没有被监听的方案数。
- 对于转移时两点 u, v,考虑 u 点的情况
- 如果 u 没有放装置也没有被监听,v 一定不能放装置但 v 要被监听(否则 u 被监听)。
-
\[dp[u][i+j][0][0] = \sum dp[u][i][0][0] \times dp[v][j][0][1] \]
-
- 如果 u 没有放装置但被监听,v 的状态为被监听。对于 dp[u][k][0][1] 放不放装置无所谓, 对于 dp[u][k][0][0] v 必须放装置。
-
\[dp[u][i+j][0][1] = \sum dp[u][i][0][1] \times (dp[v][j][0][1] + dp[v][j][1][1]) + \sum dp[u][i][0][0] \times dp[v][j][1][1] \]
-
- 如果 u 放了装置但没有被监听,v 有没有被监听无所谓,一定没有放装置。
-
\[dp[u][i+j][1][0] = \sum dp[u][i][1][0]\times (dp[v][j][0][1] + dp[v][j][0][0]) \]
-
- 如果 u 放了装置且被监听,对于 dp[u][k][1][0], v 一定放装置,有没有被监听无所谓。对于 dp[u][k][1][1] v 放不放装置,被不被监听都无所谓。
-
\[dp[u][i+j][1][0]=\sum dp[u][i][1][0]\times (dp[v][j][1][0] + dp[v][j][1][1]) \]
-
\[dp[u][i+j][1][1]=\sum dp[u][i][1][1]\times (dp[v][j][0][0]+dp[v][j][0][1]+dp[v][j][1][0]+dp[v][j][1][1]) \]
-
- 转移复杂度是 \(O(nk)\) ,数组不能开 long long ,转以后再更新子树大小。
const int N = 1e5 + 10, mod = 1e9 + 7;
int n, K;
vector<int> edge[N];
int dp[N][110][2][2], tmp[110][2][2], sz[N];
void dfs(int u, int p) {
sz[u] = 1;
dp[u][0][0][0] = dp[u][1][1][0] = 1;
for (auto v: edge[u]) {
if (v == p) continue;
dfs(v, u);
memcpy(tmp, dp[u], sizeof tmp);
memset(dp[u], 0, sizeof tmp);
for (int i = 0; i <= min(sz[u], K); i++) {
for (int j = 0; j <= min(sz[v], K) && i + j <= K; j++) {
dp[u][i + j][0][0] += 1ll * tmp[i][0][0] * dp[v][j][0][1] % mod;
dp[u][i + j][0][1] += (1ll * tmp[i][0][1] * (dp[v][j][0][1] + dp[v][j][1][1]) + 1ll * tmp[i][0][0] * dp[v][j][1][1]) % mod;
dp[u][i + j][1][0] += 1ll * tmp[i][1][0] * (dp[v][j][0][1] + dp[v][j][0][0]) % mod;
dp[u][i + j][1][1] += (1ll * tmp[i][1][0] * (dp[v][j][1][0] + dp[v][j][1][1]) +
1ll * tmp[i][1][1] * (1ll * dp[v][j][0][0] + dp[v][j][0][1] + dp[v][j][1][0] + dp[v][j][1][1])) % mod;
if (dp[u][i + j][0][0] >= mod) dp[u][i + j][0][0] -= mod;
if (dp[u][i + j][0][1] >= mod) dp[u][i + j][0][1] -= mod;
if (dp[u][i + j][1][0] >= mod) dp[u][i + j][1][0] -= mod;
if (dp[u][i + j][1][1] >= mod) dp[u][i + j][1][1] -= mod;
}
}
sz[u] += sz[v]; // 转移后再更新子树大小
}
return ;
}
int main() {
re(n), re(K);
for (int i = 1; i < n; i++) {
int a, b;
re(a), re(b);
edge[a].pb(b), edge[b].pb(a);
}
dfs(1, -1);
ll ans = (dp[1][K][0][1] + dp[1][K][1][1]) % mod;
printf("%lld\n", ans);
return 0;
}
CodeForces
CF219D. Choosing Capital for Treeland
题意
Treeland国有n个城市,这n个城市连成了一颗树,有n-1条道路连接了所有城市。每条道路只能单向通行。现在政府需要决定选择哪个城市为首都。假如城市i成为了首都,那么为了使首都能到达任意一个城市,不得不将一些道路翻转方向,记翻转道路的条数为k。你的任务是找到所有满足k最小的首都。
思路
- 定义 \(f_u\) 表示到达子树所有点最小翻转次数, \(g_u\) 表示到达父节点以上的所有点最小翻转次数。
- \(f_u = \sum_v f[v] + w_{u,v}\), \(g_v = g_u + (f_u - f_v - w) + (w\; xor\; 1);\)
CF1187E. Tree Painting
题意
给定一棵n个点的树 初始全是白点
要求你做n步操作,每一次选定一个与一个黑点相隔一条边的白点,将它染成黑点,然后获得该白点被染色前所在的白色联通块大小的权值。
第一次操作可以任意选点。求可获得的最大权值
思路
- 发现第一次选择一个点后方案固定。
- 再画一下有点类似换根dp,需要求出父节点以上连通块和自己子树的答案。
- \(f_u\) 表示 u 子树的贡献,\(g_u\) 表示 u 父节点以上的贡献。
-
\[f_u=size_u + \sum_v f_v \]
-
\[g_v=f_u - size_u - f_v + g_u + n - size_v \]
-
- 对于每个节点答案为 \(f_i+g_i + n - size_i\) ,加的部分补偿 i 作为第一个选的得分。