Codeforces 1294F Three Paths on a Tree
Description
描述
给一棵 $n$ 个点的树,找到三个点,使得两两之间,共三条路径,所经过的边的并集尽可能大。
输入
第一行为一个正整数 $n$($3 \le n \le 2 \times 10^5$)。
接下来 $n -1 $ 行,每行两个数 $u,v$ 表示一条边($1 \le u, v \le n$,$u \neq v$)。
输出
第一行为一个数 $res$,表示并集的大小。
第二行为三个数 $a, b, c$,表示找的三个点,输出任意解($a \neq b$,$b \neq c$,$a \neq c$)。
样例
输入
8
1 2
2 3
3 4
4 5
4 6
3 7
3 8输出
5
1 8 6解释
这是另一种可行解。$1 \to 5$ 经过 $(1, 2)$,$(2, 3)$,$(3, 4)$,$(4, 5)$;$1 \to 6$ 经过 $(1, 2)$,$(2, 3)$,$(3, 4)$,$(4, 6)$;$5 \to 6$ 经过 $(4,5)$,$(4,6)$。并集是 $(1, 2)$,$(2, 3)$,$(3, 4)$,$(4, 5)$,$(4, 6)$ 共 $6$ 条边。
Solution
介绍一种自己的乱搞做法。
首先设选取的三个点分别为 $du, dv, dw$。
我们可以发现,三条路径的并集可以看作是有唯一的一个交点,例如样例:
交点为 $4$。
那么,问题就变成了,找到一个 $u$ 作为交点,$du, dv, dw$ 一定是到 $u$ 距离最远的,且不在同一棵子树里 的三个点。
为什么不在同一棵子树里?
因为如果 $du, dv$ 在一棵子树里,显然,我们可以找到它们分叉的地方,比如叫做 $v$,那 $u \to v \to du$ 和 $u \to v \to dv$ 两条路径就有 $u \to v$ 这一段相交的地方,那么交点就大可以设成 $v$ 了。
于是 $\mathcal O(n^2)$ 的做法不难想到:枚举 $u$,$n$ 次 BFS,暴力求出最远点更新答案。
这个做法显然是会 TLE 的,正解需要下面这个结论:
$du, dv$ 一定是树的直径的两个端点。
为什么呢?
比如我们有这么一棵树,它的直径用红色标出。
分两种情况:
- 枚举到的 $u$ 在直径上。
那么我们要证明的就是最远的两条链 $du, dv$ 一定是树的直径的端点。
比如我们枚举到了这样的 $u$,我们发现 $u$ 除了直径还有一棵子树,这个里面离 $u$ 最远的叫做 $w$。
那么我们有:
$$ dis(u, a) \ge dis(u, w) \cdots \cdots (1) \\ dis(u, b) \ge dis(u, w) \cdots \cdots (2) $$
$(1)$ 是因为,如果有 $dis(u, w) > dis(u, b)$ 了,那么树的直径就不可能是 $a \to b$,而是 $a \to w$ 了;$(2)$ 也同理可证。
那么,$u$ 除了子树里面的点,也就 $a, b$ 两个选择了,所以 $du = a$,$dv = b$。
- 枚举到的 $u$ 不在直径上。
我们还是设 $u$ 的子树(这次有两棵)离 $u$ 最远的点为 $w$。再设 $u$ 不断沿着父亲追溯到直径上的点为 $v$。
同上,可以得到:
$$ dis(v, a) \ge dis(v, w) = dis(u, v) + dis(u, w) \\ dis(v, b) \ge dis(v, w) = dis(u, v) + dis(u, w) $$
右边去掉一个 $dis(u, v)$ 不影响不等式,所以:
$$ dis(v, a) \ge dis(u, w) \\ dis(v, b) \ge dis(u, w) $$
左边再加上一个 $dis(u, v)$ 也不影响不等式,所以:
$$ dis(v, a) + dis(u, v) = dis(u, a) \ge dis(u, w) \\ dis(v, b) + dis(u, v) = dis(u, b) \ge dis(u, w) $$
所以我们还是会先选 $du = a$,$dv = b$!
然后既然这样了,我们就可以先找到直径,于是 $du, dv$ 就出来了。
然后记录直径上的点为 $\text{path}$,在 $\text{path}$ 上枚举 $u$,找到「不在直径上」且 「距离 $u$ 最远」的点 $w$,用来更新答案。
具体怎么实现呢?我们可以把直径拉出来,比如样例,$1 \to 5$ 是直径,那么我们就:
把直径拉出来,然后我们从上至下记录为 $\text{path}_{1 \to len}$,我们发现,只要 $\text{path}_i$ 的儿子不是 $\text{path}_{i + 1}$,就是可以更新的。
容易设计出一个树形 DP,$f_{u}$ 表示 $u$ 最佳的长度,$g_{u}$ 表示来源,显然:
$\textbf{if } u \not \in \text{path } \textbf{then } f_u = \max\limits_{v \in \text{sons(u)}} \{f_v +1\}$
$\textbf{else } \text{let } u \text{ be path}_i, f_u = \max\limits_{v \in \text{sons(u)}, v \text{ isn't path}_{i + 1} }\{ f_v +1\}$
$g_u$ 相应更新。
时间复杂度 $\mathcal O(n)$,代码如下,仅供参考:
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 5; int n, dep[N], fa[N], du, dv, dlen; vector<int> G[N]; void dfs(int u) // 预处理 { for(int v : G[u]) if(v ^ fa[u]) { dep[v] = dep[u] + 1; fa[v] = u; dfs(v); } } int tmp[N], cur, path[N]; void find_path(int u) // 拉出直径 { tmp[++cur] = u; if(u == dv) { for(int i = 1; i <= cur; i++) path[i] = tmp[i]; return; } for(int v : G[u]) if(v ^ fa[u]) find_path(v); cur--; } int f[N], g[N], maxl = -1, dw; void solve(int u, int pos) // DP { if(u ^ path[pos]) { for(int v : G[u]) if(v ^ fa[u]) { solve(v, pos); if(f[u] < f[v] + 1) f[u] = f[v] + 1, g[u] = g[v]; } } else { for(int v : G[u]) if(v ^ fa[u]) { solve(v, pos + 1); if(v ^ path[pos + 1] && f[u] < f[v] + 1) f[u] = f[v] + 1, g[u] = g[v]; } } if(f[u] > maxl && g[u] != du && g[u] != dv) // 注意,如果来源是 du 或 dv 就重复了,所以不行 maxl = f[u], dw = g[u]; } int main() { ios::sync_with_stdio(false); cin >> n; for(int i = 1; i < n; i++) { int u, v; cin >> u >> v; G[u].push_back(v); G[v].push_back(u); } dep[1] = fa[1] = 0; dfs(1); for(int i = 1; i <= n; i++) if(dep[i] > dep[du]) du = i; dep[du] = fa[du] = 0; dfs(du); for(int i = 1; i <= n; i++) if(dep[i] > dep[dv]) dv = i; dlen = dep[dv]; find_path(du); cur = n; for(int u = 1; u <= n; u++) g[u] = u; solve(du, 1); cout << maxl + dlen << endl; cout << du << ' ' << dv << ' ' << dw << endl; return 0; }