「REMAKE系列」树形dp篇

总结

树上背包

树上背包合并复杂度证明

基环树上dp

树上路径

  • n,m2000 ,要求选择一些路径,使得每个点至多在一条路径上,并且路径的权值和最大。代码源树上路径1

换根dp

  • 简单换根。CF219D
  • 不明显换根,模拟后发现是换根。CF1187E

基础树形dp

习题

一些做对了一半的题

CF533B. Work Group

*2000

状态设计的差不多,自己的转移太复杂而且玄学,应该重新想想是不是可以简化。

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

提高+/省选- 基础树形dp

评测记录

题意

John 想让他的所有牛用上手机以便相互交流,他需要建立几座信号塔在 N 块草地中。已知与信号塔相邻的草地能收到信号。给你 N1 个草地(A,B)的相邻关系,问:最少需要建多少个信号塔能实现所有草地都有信号。

思路

  • 写的时候少加了一个状态,然后加上原来的思路就ac了。
  • 看起来和没有上司的舞会有点像其实有一点区别,对于每个节点被儿子染色时,儿子节点可以放也可以不放,每个节点还可以被父亲染色。
  • 设计状态 fu,0 表示节点被儿子染色自己不放,fu,1 表示结点自己放来染色自己,fu,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

link树上路径1

  • dp[u] 表示当前 u 点对应路径最高点的路径选或不选的最大值。
  • u 点没有路径,则最大值为儿子节点 dp 权值和。
  • u 点有路径,要与路径上节点儿子权值加和后取 max ,路径上的点 dp 权值和不计入。
  • 所以有一个巧妙办法,选取了一段路径,对于路径上的点可以等价于 sdpidpi 。节点儿子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 的所有节点权值和 mi

思路

  • 定义状态 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 的树,树边有边权。给你一个在 0n 之内的正整数 k ,你要在这棵树中选择 k 个点,将其染成黑色,并将其他 的 nk 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的受益。问受益最大值是多少。

对于 100% 的数据,0n,k2000

思路

  • 直接设状态不好求,考虑按贡献求解,求出对于某一条边被经过了多少次。
  • 显然是边左右两侧同色点的对数乘积。有一些细节,看代码实现就行。
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] 骑士

省选/NOI-基环树dp

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] 潜入行动

省选/NOI-树上背包计数

题意略

  • 设状态为 dp[u][j][0/1][0/1] ,u 点子树放了 j 个装置,u 点有没有放装置,u 点有没有被监听的方案数。
  • 对于转移时两点 u, v,考虑 u 点的情况
  • 如果 u 没有放装置也没有被监听,v 一定不能放装置但 v 要被监听(否则 u 被监听)。
    • dp[u][i+j][0][0]=dp[u][i][0][0]×dp[v][j][0][1]

  • 如果 u 没有放装置但被监听,v 的状态为被监听。对于 dp[u][k][0][1] 放不放装置无所谓, 对于 dp[u][k][0][0] v 必须放装置。
    • dp[u][i+j][0][1]=dp[u][i][0][1]×(dp[v][j][0][1]+dp[v][j][1][1])+dp[u][i][0][0]×dp[v][j][1][1]

  • 如果 u 放了装置但没有被监听,v 有没有被监听无所谓,一定没有放装置。
    • dp[u][i+j][1][0]=dp[u][i][1][0]×(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]=dp[u][i][1][0]×(dp[v][j][1][0]+dp[v][j][1][1])

    • dp[u][i+j][1][1]=dp[u][i][1][1]×(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

*1700 换根dp

评测记录

题意

Treeland国有n个城市,这n个城市连成了一颗树,有n-1条道路连接了所有城市。每条道路只能单向通行。现在政府需要决定选择哪个城市为首都。假如城市i成为了首都,那么为了使首都能到达任意一个城市,不得不将一些道路翻转方向,记翻转道路的条数为k。你的任务是找到所有满足k最小的首都。

思路

  • 定义 fu 表示到达子树所有点最小翻转次数, gu 表示到达父节点以上的所有点最小翻转次数。
  • fu=vf[v]+wu,v, gv=gu+(fufvw)+(wxor1);

CF1187E. Tree Painting

*2100 换根dp

评测记录

题意

给定一棵n个点的树 初始全是白点

要求你做n步操作,每一次选定一个与一个黑点相隔一条边的白点,将它染成黑点,然后获得该白点被染色前所在的白色联通块大小的权值。

第一次操作可以任意选点。求可获得的最大权值

思路

  • 发现第一次选择一个点后方案固定。
  • 再画一下有点类似换根dp,需要求出父节点以上连通块和自己子树的答案。
  • fu 表示 u 子树的贡献,gu 表示 u 父节点以上的贡献。
    • fu=sizeu+vfv

    • gv=fusizeufv+gu+nsizev

  • 对于每个节点答案为 fi+gi+nsizei ,加的部分补偿 i 作为第一个选的得分。

__EOF__

本文作者Roshin
本文链接https://www.cnblogs.com/Roshin/p/remake_tree_dp.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Roshin  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
-->
点击右上角即可分享
微信分享提示