[IOI 2008] Island
[IOI 2008] Island
题目描述
你准备浏览一个公园,该公园由 $N$ 个岛屿组成,当地管理部门从每个岛屿 $i$ 出发向另外一个岛屿建了一座长度为 $L_i$ 的桥,不过桥是可以双向行走的。同时,每对岛屿之间都有一艘专用的往来两岛之间的渡船。相对于乘船而言,你更喜欢步行。你希望经过的桥的总长度尽可能长,但受到以下的限制:
- 可以自行挑选一个岛开始游览。
- 任何一个岛都不能游览一次以上。
- 无论任何时间,你都可以由当前所在的岛 $S$ 去另一个从未到过的岛 $D$。从 $S$ 到 $D$ 有如下方法:
- 步行:仅当两个岛之间有一座桥时才有可能。对于这种情况,桥的长度会累加到你步行的总距离中。
- 渡船:你可以选择这种方法,仅当没有任何桥和以前使用过的渡船的组合可以由 $S$ 走到 $D$ (当检查是否可到达时,你应该考虑所有的路径,包括经过你曾游览过的那些岛)。
注意,你不必游览所有的岛,也可能无法走完所有的桥。
请你编写一个程序,给定 $N$ 座桥以及它们的长度,按照上述的规则,计算你可以走过的桥的长度之和的最大值。
输入格式
第一行包含一个整数 $N$,即公园内岛屿的数目。
随后的 $N$ 行每一行用来表示一个岛。第 $i$ 行由两个以单空格分隔的整数,表示由岛 $i$ 筑的桥。第一个整数表示桥另一端的岛,第二个整数表示该桥的长度 $L_i$。你可以假设对于每座桥,其端点总是位于不同的岛上。
输出格式
仅包含一个整数,即可能的最大步行距离。
输入输出样例 #1
输入 #1
7
3 8
7 2
4 2
1 4
1 9
3 4
2 3
输出 #1
24
说明/提示
样例解释:
样例 $N=7$ 座桥,分别为 $(1-3), (2-7), (3-4), (4-1), (5-1), (6-3)$ 以及 $(7-2)$。注意连接岛 $2$ 与岛 $7$ 之间有两座不同的桥。
其中一个可以取得最大的步行距离的方法如下:
- 由岛 $5$ 开始。
- 步行长度为 $9$ 的桥到岛 $1$。
- 步行长度为 $8$ 的桥到岛 $3$。
- 步行长度为 $4$ 的桥到岛 $6$。
- 搭渡船由岛 $6$ 到岛 $7$。
- 步行长度为 $3$ 的桥到岛 $2$。
最后,你到达岛 $2$,而你的总步行距离为 $9+8+4+3=24$。
只有岛 $4$ 没有去。注意,上述游览结束时,你不能再游览这个岛。更准确地说:
- 你不可以步行去游览,因为没有桥连接岛 $2$ (你现在的岛) 与岛 $4$。
- 你不可以搭渡船去游览,因为你可由当前所在的岛 $2$ 到达岛 $4$。一个方法是:走 $(2-7)$ 桥,再搭你曾搭过的渡船由岛 $7$ 去岛 $6$,然后走 $(6-3)$ 桥,最后走 $(3-4)$ 桥。
数据范围:
对于 $100\%$ 的数据,$2\leqslant N\leqslant 10^6,1\leqslant L_i\leqslant 10^8$。
解题思路
题目大意就是给定一个基环树森林(即有若干个相互独立的基环树),求每个基环树的最长路径的和。一个基环树的最长路径又称为基环树直径(类比树的直径),其中路径上的每个节点只能出现一次。
基环树就是有一个环,环上的每个节点都挂着一棵树(可以为空)。基环树直径其实就是基环树上的一条简单路径,根据路径的两个端点,可以把所有路径分成两大类:
- 两个端点在同一棵树中。
- 两个端点分别在不同的树中。
其中第一种情况就是我们熟知的树的直径。为此,首先我们需要找到基环树的环,对于环上的每个节点,由于挂着一棵树,因此以环上的节点作为树根进行 dp 求树的直径。找环以及求树的直径在之后的部分详细描述。
对于第二种情况,由于路径两个端点在不同的树中,因此往环的方向走会走到环上两个不同节点。因此为了求出第二种情况的最长路径,我们可以枚举环上两个不同的点 $u$ 和 $v$。由于其中一个端点在 $u$ 挂着的树中,我们可以选择树中距离 $u$ 最远的那点为端点,最远距离记作 $d_u$;同理选择 $v$ 挂着的树中距离 $v$ 最远的点为另外一个端点,最远距离记作 $d_v$。假设在环上 $u$ 和 $v$ 的最远距离为 $\text{dist}(u,v)$,那么 $d_u + d_v + \text{dist}(u,v)$ 就是经过环上 $u \to v$(或 $v \to u$)的所有路径的最长距离。
显然我们不可能暴力枚举环上任意两点,考虑破环成链。假设顺时针遍历环上的点得到序列 $g$,把 $g$ 拷贝接到 $g$ 的后面(即 $g \gets \{g,g\}$),就是破环成链。假设环的大小为 $\text{sz}$(即环上有 $\text{sz}$ 个节点),枚举每个点 $u$,逆时针考虑另外一个点 $v \, (\{v \mid g_{u - \text{sz} + 1} \sim g_{u-1}\})$,使得 $d_u + d_v + \text{dist}(u,v)$ 有最大值。此时 $\text{dist}(u,v)$ 可以换成 $s_u - s_v$(因为一定会枚举到 $u$ 到 $v$ 最远距离的情况),其中 $s_u = \sum\limits_{i=1}^{u}{w_{g_{i-1},g_{i}}}$,$w_{u,v}$ 表示边 $(u,v)$ 的权值。
式子可以改写成 $d_u + s_u + d_v - s_v$ 的形式,当我们枚举到 $u$ 时,为了得到 $\{v \mid g_{u - \text{sz} + 1} \sim g_{u-1}\}$ 中关于 $d_v -s_v$ 的最大值,只需用单调队列去维护大小为 $\text{sz}-1$ 的窗口内关于 $d_v -s_v$ 最大值的点即可。
下面讲如何找到基环树上的环。从基环树上任意一点开始 dfs,并记录递归的栈中有哪些点。当枚举到 $u$,然后其邻接点 $v$ 在栈内,如下图的情况,说明我们找到了环。当然在 dfs 的时候我们记录每个节点在搜索树中对应的父节点,这样我们就可以从 $u$ 开始,不断往父节点走直到 $v$,来找到环上所有点。具体实现参考 AC 代码中的函数 dfs1
。
然后是求树的直径。在找到环上的点后,对于环上的每一个点以这个点为树根,求挂在这个点的树的直径。由于树已经定根了,我们可以把树中所有路径根据路径的最高点进行分类。定义 $d_1[u]$ 表示从 $u$ 往下走的最远距离,$d_2[u]$ 表示从 $u$ 往下走的次远距离,那么 $d_1[u] + d_2[u]$ 就是以 $u$ 为最高点的路径的最长距离。更新方式为
$$\text{for } v \in \text{son}(u), \begin{cases}
(d_1[u],d_2[u]) = (d_1[v] + w_{u,v}, d_1[u]) &\text{if } d_1[v] + w_{u,v} \geq d_1[u] \\
d_2[u] = d_1[v] + w_{u,v} &\text{if } d_1[v] + w_{u,v} > d_2[u]
\end{cases}$$
最后树的直径就是 $\max\limits_{u}\{d_1[u]+d_2[u]\}$。另外上面第二种情况中的 $d_u$ 其实就是 $d_1[u]$。
剩下的细节参考代码。
AC 代码如下,时间复杂度为 $O(n)$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6 + 5, M = N * 2;
int h[N], e[M], wt[M], ne[M], idx;
int fa[N], fw[N], g[M], sz;
bool vis[N], ins[N];
LL s[N], d1[N], d2[N], mx;
int q[N];
void add(int u, int v, int w) {
e[idx] = v, wt[idx] = w, ne[idx] = h[u], h[u] = idx++;
}
void dfs1(int u, int p) {
vis[u] = ins[u] = true; // vis[u] 用来记录 u 是否被访问过,ins[u] 用来表示 u 是否在递归栈中
for (int i = h[u]; i != -1; i = ne[i]) {
if ((i ^ 1) == p) continue;
int v = e[i];
if (!vis[v]) {
fa[v] = u, fw[v] = wt[i]; // fa[v] 表示 v 在搜索树中的父节点,fw[v] 表示 v 到父节点的边权
dfs1(v, i);
}
else if (ins[v]) { // 找到环
fa[v] = u, fw[v] = wt[i];
for (int i = u; ; i = fa[i]) {
g[sz++] = i;
if (i == v) break;
}
}
}
ins[u] = false;
}
void dfs2(int u, int p) {
d1[u] = d2[u] = 0;
for (int i = h[u]; i != -1; i = ne[i]) {
int v = e[i];
if (v == p || ins[v]) continue; // 避免搜到环上的点
dfs2(v, u);
LL w = d1[v] + wt[i];
if (w >= d1[u]) d2[u] = d1[u], d1[u] = w;
else if (w > d2[u]) d2[u] = w;
}
mx = max(mx, d1[u] + d2[u]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
memset(h, -1, sizeof(h));
for (int i = 1; i <= n; i++) {
int x, y;
cin >> x >> y;
add(i, x, y), add(x, i, y);
}
LL ret = 0;
for (int i = 1; i <= n; i++) {
if (vis[i]) continue;
sz = mx = 0;
dfs1(i, -1);
memmove(g + sz, g, sz << 2);
for (int i = 1; i < sz << 1; i++) {
s[i] = s[i - 1] + fw[g[i - 1]];
ins[g[i]] = true; // 标记环上的点,在求树的直径是避免访问到环上的点
}
for (int i = 0; i < sz; i++) {
dfs2(g[i], 0);
}
int hh = 0, tt = -1;
LL t = mx;
for (int i = 0; i < sz << 1; i++) {
if (q[hh] <= i - sz) hh++;
if (hh <= tt) t = max(t, d1[g[i]] + s[i] + d1[g[q[hh]]] - s[q[hh]]);
while (hh <= tt && d1[g[q[tt]]] - s[q[tt]] < d1[g[i]] - s[i]) {
tt--;
}
q[++tt] = i;
}
ret += t;
}
cout << ret;
return 0;
}
参考资料
AcWing 358. 岛屿:https://www.acwing.com/activity/content/code/content/612104/
P4381 [IOI2008]Island:https://www.luogu.com.cn/article/t3ufkiek
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/18829720