树形DP
树形 DP,即在树上进行的 DP。
由于树固有的递归性质,树形 DP 一般都是递归进行的。
树的最长路径
题目描述
给定一个含有 n 个节点的 树,以及树中每条边的权值 wedgei。
现需要在树中找出一条路径,使得该路径上所有边的权值之和最大。
思路:
记录以i为根节点的子树中,从子树某个节点到i的最长路和次长路。
那么以经过i的最长边就是最长路+次长路。
而且很明显可以递归求解。
闫氏DP分析法
状态表示—集合,: 以节点 i 为根的子树中,从子树某个节点到 i 的最长路为 f1,次长路为 f2
状态表示—属性: 路径长度的最大值 Max
状态计算.......
代码:
#include <bits/stdc++.h> #define endl '\n' #define int long long #define pii pair<int, int> #define pll pair<int, int> #define ull unsigned long long using namespace std; const int N = 10010, M = N * 2; int n; int to[M], w[M], pre[M], h[N], idx; void add(int a, int b, int c) { to[idx] = b, w[idx] = c, pre[idx] = h[a], h[a] = idx++; } int ans = 0; int dfs(int u, int fa) { int d1 = 0, d2 = 0; for (int i = h[u]; i != -1; i = pre[i]) { int j = to[i]; if (j == fa) continue; int d = dfs(j, u)+w[i]; if (d >= d1) d2 = d1, d1 = d; else if (d > d2) d2 = d; } ans = max(ans, d1 + d2); return d1; } void solve() { memset(h, -1, sizeof h); cin >> n; for (int i = 0; i < n-1; i++) { int a, b, c; cin >> a >> b >> c; add(a, b, c), add(b, a, c); } dfs(1, -1); cout << ans << endl; } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t = 1; // cin >> t; while (t--) solve(); return 0; }
应用
数字转换
题意:
如果一个数 x 的约数之和 y(不包括他本身)比他本身小,那么 x 可以变成 y,y 也可以变成 x。
例如,4的约数有1,2。因此 4可以变为 3,3 可以变为 4。
限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。
思路:
本题的重点在建图:如何分析出题目是一棵树或者是一个森林。
根据题意,单看一个数字 x,那么他能转换到的数有:
- x 的约数之和
- 所有大于 x,且约数之和等于 x 的数
又因为每个数 最多只能有比他小的,所以每个数只能向比他小的数字建立一个边,所以一定是个森林。
森林: 每一个 点,至多只有一条 入边
建好图后,本题就 等价于 找出一个森林中,直径最长 的树,并求出该树的 直径
如何建图呢有两种方法
第一种就是暴力,暴力每个数的约数是谁,求出约数和。
另一种是看这个数能组成哪些数(求他是谁的约数)
然后因为 1需要乘n次,2需要乘n/2次,3需要乘n/3次....所以复杂度是
代码:
#include <bits/stdc++.h> #define endl '\n' #define int long long #define pii pair<int, int> #define pll pair<int, int> #define ull unsigned long long using namespace std; const int N = 50000, M = N * 2; int n; int pre[M], to[M], h[N], idx; int fsum[N]; void add(int a, int b) { to[idx] = b, pre[idx] = h[a], h[a] = idx++; } int ans = 0; bool vis[N]; int dfs(int u) { vis[u] = true; int d1 = 0, d2 = 0; for (int i = h[u]; i != -1; i = pre[i]) { int j = to[i]; int d = dfs(j); if (d > d1) d2 = d1, d1 = d; else if (d > d2) d2 = d; } ans = max(ans, d1 + d2); return d1 + 1; } void solve() { memset(h, -1, sizeof h); cin >> n; for (int i = 1; i <= n; i++) { for (int j = 2; i * j <= n; j++) { fsum[i * j] += i; } } for (int i = 2; i <= n; i++) { if (fsum[i] < i) add(fsum[i], i); } for (int i = 1; i <= n; i++) if (!vis[i]) dfs(i); cout << ans << endl; } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t = 1; // cin >> t; while (t--) solve(); return 0; }
树的中心 换根DP
题意:
给定一棵树,树中包含 n 个结点(编号1~n)和 n−1 条无向边,每条边都有一个权值。
请你在树中找到一个点,使得该点到树中其他结点的最远距离 最近。
思路:
思考一下:求一个节点的 最远距离 时,会有几种路径呢:
-
从当前节点往下,直到子树中某个节点的最长路径
-
从当前节点往上走到其父节点,再从其父节点出发且不回到该节点的最长路径
因此我们可以先 dfs 一遍,预处理出当前子树对于根的最大贡献(距离)和 次大贡献(距离)
处理 次大贡献(距离) 的原因是:
如果 当前节点 是其 父节点子树 的 最大路径 上的点。此时 父节点子树 的 最大贡献 不能算作对该节点的贡献,就要使用次大贡献
因为我们的路径是 简单路径(不能 走回头路)
然后我们再 dfs 一遍,求解出每个节点的父节点对他的贡献(即每个节点往上能到的最远路径
两者比较,取一个 max 即可
时间复杂度: T(2n)=O(n)
到这里就可以总结出换根DP 的思想了:
换根DP 一般分为三个步骤:
-
指定任意一个根节点
-
一次dfs遍历,统计出当前子树内的节点对当前节点的贡献
-
一次dfs遍历,统计出当前节点的父节点对当前节点的贡献,然后合并统计答案
代码:
#include <bits/stdc++.h> #define endl '\n' #define int long long #define pii pair<int, int> #define pll pair<int, int> #define ull unsigned long long using namespace std; const int N = 10010, M = N * 2; int n; int to[M], w[M], pre[M], h[N], idx; int d1[N], d2[N]; int fa1[N]; int up[N]; void add(int a, int b, int c) { to[idx] = b, w[idx] = c, pre[idx] = h[a], h[a] = idx++; } int dfs_d(int u, int father) { for (int i = h[u]; i != -1; i = pre[i]) { int j = to[i]; if (j == father) continue; int d = dfs_d(j, u) + w[i]; if (d > d1[u]) { d2[u] = d1[u], d1[u] = d; fa1[u] = j; } else if (d > d2[u]) { d2[u] = d; } } return d1[u]; } void dfs_u(int u, int father) { for (int i = h[u]; i != -1; i = pre[i]) { int j = to[i]; if (j == father) continue; if (fa1[u] == j) up[j] = max(up[u], d2[u]) + w[i]; else up[j] = max(up[u], d1[u]) + w[i]; dfs_u(j, u); } } void solve() { memset(h, -1, sizeof h); cin >> n; for (int i = 0; i < n - 1; i++) { int a, b, c; cin >> a >> b >> c; add(a, b, c), add(b, a, c); } dfs_d(1, -1); dfs_u(1, -1); int ans = 0x3f3f3f3f3f; for (int i = 1; i <= n; i++) ans = min(ans, max(up[i], d1[i])); cout << ans << endl; } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int t = 1; // cin >> t; while (t--) solve(); return 0; }
主要参考:一只野生彩色铅笔
https://www.acwing.com/user/myspace/index/55909/
本文作者:kingwzun
本文链接:https://www.cnblogs.com/kingwz/p/16548828.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步