树的直径=>学习笔记

定义

树的直径是指 树上任意两节点之间最长的简单路径

显然一棵树可能不止一条直径,但它们长度相等。

求法

2 2 2 种解法求树的直径,分别是两次 dfs 和 dp。

两次 dfs

先从随机的一个点,假设是根节点,第一次 dfs 求出距离它最远的节点,假设这个节点为 u u u,然后从 u u u 开始再次 dfs,求出距离点 u u u 最远的节点, 2 2 2 个节点之间的距离就是树的直径。

例题:洛谷 B4016 树的直径

代码

#include
#include
#include
#include
#include

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_1 d1 为每个节点作为子树的根向下所能延伸的最长路径长度,而 d 2 d_2 d2 则是每个节点作为子树的根向下所能延伸的次长路径长度,但是 d 2 d_2 d2 d 1 d_1 d1 的路径没有公共边。所以说树的直径就是 max ⁡ { d 2 + d 1 } \max \left \{ d_2 + d_1\right \} max{d2+d1}

代码

#include
#include
#include
#include
#include

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。设 d p u dp_u dpu 表示从 u u u 出发的最长路径。

其状态转移方程为 d p u = max ⁡ ( d p u , d p v + e d g e ( u , v ) ) dp_u = \max (dp_u, dp_v + edge(u, v)) dpu=max(dpu,dpv+edge(u,v)),其中 v v v u u u 的子节点, e d g e ( u , v ) edge(u,v) edge(u,v) 代表 u u u v v v 之间的边权。而树的直径,可以看作从一个节点出发,不同的两条路径加起来的和取最大值,所以我们有: d = max ⁡ ( d , d p u + d p v + e d g e ( u , v ) ) d = \max(d, dp_u + dp_v + edge(u, v)) d=max(d,dpu+dpv+edge(u,v)),其中 d d d 是树的直径,这个转移要放在 d p u dp_u dpu 的转移之前。因为如果放在后面,假设 d p u = d p v + e d g e ( u , v ) dp_u = dp_v + edge(u, v) dpu=dpv+edge(u,v),那么同一条便会被算两次,使得答案不正确。

代码

#include
#include
#include
#include
#include

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 1,具体要看各个题目的要求)

总结

(1) 的做法可能不太常见,但多学一点也没坏处。

虽然 dp 的做法已经解决了两次 dfs 遇到负边权会 GG 的问题,但是,dp 的做法也有个缺点。

这个做法它只求直径长度,并不知道经过了哪些节点。

小结

dp 的做法和两次 dfs 在有些题目中相辅相成,所以两种做法都要熟悉 (典型例子:洛谷 P3629 [APIO2010] 巡逻 后续会出题解,可在主页查看)

感谢 这里

你可能感兴趣的:(算法数据结构学习笔记,学习,笔记,深度优先)