定义
树的直径是指 树上任意两节点之间最长的简单路径。
显然一棵树可能不止一条直径,但它们长度相等。
求法
有 \(2\) 种解法求树的直径,分别是两次 dfs 和 dp。
两次 dfs
先从随机的一个点,假设是根节点,第一次 dfs 求出距离它最远的节点,假设这个节点为 \(u\),然后从 \(u\) 开始再次 dfs,求出距离点 \(u\) 最远的节点,\(2\) 个节点之间的距离就是树的直径。
例题:洛谷 B4016 树的直径
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>using namespace std;int read() {int x = 0, f = 1; char ch = getchar();while (!isdigit(ch)) {if (ch == '-') f = -1;ch = getchar();}while (isdigit(ch)) {x = (x << 3) + (x << 1) + (ch ^ 48);ch = getchar();}return x * f;
}const int maxn = 100005;
int n, c, d[maxn];
vector<int> g[maxn];
int leaf;void dfs(int u, int fa) {for (int i = 0; i < g[u].size(); i++) {int v = g[u][i];if (v == fa) continue;d[v] = d[u] + 1;// 更新最远距离节点 if (d[v] > d[leaf]) leaf = v;dfs(v, u);}
}int main() {n = read();for (int i = 1; i < n; i++) {int u = read(), v = read();g[u].push_back(v);g[v].push_back(u);}dfs(1, 0);// 从最远的节点出发 d[leaf] = 0, dfs(leaf, 0);printf("%d\n", d[leaf]);return 0;
}
如果它叫你输出树的直径上的节点怎么办呢?很简单,直接从 leaf 向上遍历就行。在第二次 dfs 时可以这样:
void dfs(int u, int fa, int time) {if (time == 2) f[u] = fa;for (int i = 0; i < g[u].size(); i++) {int v = g[u][i];if (v == fa) continue;d[v] = d[u] + 1;// 更新最远距离节点 if (d[v] > d[leaf]) leaf = v;dfs(v, u, time);}
}
time 是我们新增的参数,方便处理每个节点的父亲,这样就可以从 leaf 开始,一步一步地去访问树的直径的下一个节点。
在主函数增加这个代码:
for (int i = leaf; i; i = f[i])printf("%d ", i);
这就是输出,虽然会输出 (树的直径 + 1) 个节点,但其实就是 树的直径 条边。
如果还有边权,就直接把 d[v] = d[u] + 1
改成 d[v] = d[u] + edge
,其中 edge 是边权。
总结
两次 dfs 的做法固然简便,但是,它也有个致命的缺点,就是 如果遇到负边权,那么直接 GG。
所以我们就要引出我们的 dp 做法啦。
dp 做法
(1) 开两个数组的 dp 做法
我们记 \(d_1\) 为每个节点作为子树的根向下所能延伸的最长路径长度,而 \(d_2\) 则是每个节点作为子树的根向下所能延伸的次长路径长度,但是 \(d_2\) 和 \(d_1\) 的路径没有公共边。所以说树的直径就是 \(\max \left \{ d_2 + d_1\right \}\)
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>using namespace std;int read() {int x = 0, f = 1; char ch = getchar();while (!isdigit(ch)) {if (ch == '-') f = -1;ch = getchar();}while (isdigit(ch)) {x = (x << 3) + (x << 1) + (ch ^ 48);ch = getchar();}return x * f;
}const int maxn = 100005;
int n, c;
int d1[maxn], d2[maxn], d;
vector<int> g[maxn];void dp(int u, int fa) {d1[u] = d2[u] = 0;for (int i = 0; i < g[u].size(); i++) {int v = g[u][i];if (v == fa) continue;dp(v, u);int t = d1[v] + 1;if (t > d1[u]) {d2[u] = d1[u];d1[u] = t;}else if (t > d2[u])d2[u] = t;}d = max(d, d1[u] + d2[u]);
}int main() {n = read();for (int i = 1; i < n; i++) {int u = read(), v = read();g[u].push_back(v);g[v].push_back(u);}dp(1, 0);printf("%d\n", d);return 0;
}
(2) 开一个数组的 dp 做法
(1) 的做法,有个显而易见的缺点,就是空间复杂度大,数据一大就 GG。
所以我们采用 树形dp。设 \(dp_u\) 表示从 \(u\) 出发的最长路径。
其状态转移方程为 \(dp_u = \max (dp_u, dp_v + edge(u, v))\),其中 \(v\) 为 \(u\) 的子节点,\(edge(u,v)\) 代表 \(u\) 和 \(v\) 之间的边权。而树的直径,可以看作从一个节点出发,不同的两条路径加起来的和取最大值,所以我们有:\(d = \max(d, dp_u + dp_v + edge(u, v))\),其中 \(d\) 是树的直径,这个转移要放在 \(dp_u\) 的转移之前。因为如果放在后面,假设 \(dp_u = dp_v + edge(u, v)\),那么同一条便会被算两次,使得答案不正确。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>using namespace std;int read() {int x = 0, f = 1; char ch = getchar();while (!isdigit(ch)) {if (ch == '-') f = -1;ch = getchar();}while (isdigit(ch)) {x = (x << 3) + (x << 1) + (ch ^ 48);ch = getchar();}return x * f;
}const int maxn = 100005;
int n, c;
int d, dp[maxn];
vector<int> g[maxn];void dfs(int u, int fa) {for (int i = 0; i < g[u].size(); i++) {int v = g[u][i];if (v == fa) continue;dfs(v, u);d = max(d, dp[u] + dp[v] + 1);dp[u] = max(dp[u], dp[v] + 1);}
}int main() {n = read();for (int i = 1; i < n; i++) {int u = read(), v = read();g[u].push_back(v);g[v].push_back(u);}dfs(1, 0);printf("%d\n", d);return 0;
}
(注意: 这里边权假设都是 \(1\),具体要看各个题目的要求)
总结
(1) 的做法可能不太常见,但多学一点也没坏处。
虽然 dp 的做法已经解决了两次 dfs 遇到负边权会 GG 的问题,但是,dp 的做法也有个缺点。
这个做法它只求直径长度,并不知道经过了哪些节点。
小结
dp 的做法和两次 dfs 在有些题目中相辅相成,所以两种做法都要熟悉 (典型例子:洛谷 P3629 [APIO2010] 巡逻 后续会出题解,可在主页查看)
感谢 这里