[luogu p6628] [省选联考 2020 B 卷] 丁香之路
\(\mathtt{Link}\)
P6628 [省选联考 2020 B 卷] 丁香之路 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
\(\mathtt{Description}\)
给定一个 \(n\) 个顶点(编号 \(1 \ldots n\))构成的完全无向图,边 \((u ,v)\) 的边权为 \(| u - v|\),其中给定 \(m\) 条边,构成边集 \(S\),问从点 \(s\) 的单源最短路径,并且路径必须包含 \(S\).
\(\mathtt{Data} \text{ } \mathtt{Range} \text{ } \mathtt{\&} \text{ } \mathtt{Restrictions}\)
-
\(1 \le s \le n \le 2500\)
-
\(0 \le m \le \dfrac{n(n - 1)}{2}\)
\(\mathtt{Solution}\)
丁香是哈尔滨的市花,因此给我做这个题加了顺畅buff(?
转化问题
首先观察到题目中给出的是无向完全图,也就是任意两对点之间有一条无向边。考虑转化一下问题:
对可重边集 \(S\) 进行添加边的操作,使边集 \(S\) 构成一条从 \(s\) 通向 \(i\) 的欧拉路径。可以有很多种加边方案,最后求的是加边之后 \(S\) 中边权和的最小值。
那么你可能就会问了,为什么是欧拉路径啊?欧拉路径明明是仅通过一次,你这个道不是随便走吗。其实我们思考一下,欧拉路径的算法在重边环境下也可以跑,一个道路你走三遍,那边集 \(S\) 里有三个这个道路就行了。因此 \(S\) 要求可重。
转化问题之后,我们观察到欧拉路径的性质:起点和终点是奇点,其他的点都是偶点,并且连通。
有奇有偶,处理不方便,因此我们可以do一个小trick:
建一条从 \(i \rightarrow s\) 的虚边。
于是,\(s\) 到 \(i\) 的欧拉路径可以转化为 \(s\) 到 \(i\) 的欧拉回路。欧拉回路的性质:所有点都是偶点,并且连通。那么我们干掉所有奇点,处理一下连通性就好了,当然还需要注意维护边权和最小。
处理奇点
其实对于一张图,奇点只能是偶数个(不在这里证明了),我们将奇点两两配对连边,就可以解决问题。
但是注意维护边权和最小,怎么做呢?
观察到题目中的边权比较特殊,是 \(|u - v|\) 的形式,因此为了让连上的边权和最小,我们将奇点按照编号排序……举个例子吧。
现在我们有 \(7\) 个点 \(n = 7\),其中第2,3,5,7号点是奇点。
为了让所连边权和最小,很显然是从小到大两两配对连,也就是2和3连,5和7连。很容易证明出来这样连一定边权和最小(你可以想一下2和5连,3和7连,边权和显然会更大,其他的也一样)
但是在这里我们再次注意,虽然我们是处理奇点,但最后图还需要连通,为了连通我们应该尽量多找几个点连在一起,也就是尽可能多加边。现在我们想一下这样一个事:
5和6连,6和7连,边权和是几?2,5和7连边权和是几?也是2,边权和是一样的。然而,5 - 6 - 7 这样的连法就多带了个 6 进来,图就更容易连通了。
因此我们应该是从小到大两两配对,对于一对奇点 \((u, v)\),我们应该连上\((u, u + 1), (u +1, u + 2), \ldots , (v - 1, v)\),也就是 \(u\) 和 \(v\) 之间相邻的点连成一条链。
于是奇点我们就处理完了,将加的边产生的代价统计到最终答案中。
处理连通性
这么处理完之后,图还有可能不连通,怎么办呢?
在经过上述操作之后,图已经形成了一些连通块,现在我们要做的就是在这些连通块之间加边,使得整张图连通,还要让加的这些边权和最小……
让图连通的最小代价,那么最小生成树已经呼之欲出了,具体做法就是把每个连通块看成一个点,跑最小生成树即可!
将最小生成树边权和的二倍统计到最终答案中。为什么是二倍呢,两种角度理解:
-
加边会让边的两个端点变成奇点,因此这条边添加两次,维护奇偶性;
-
欧拉回路,都回路了,你去另外一个连通块的时候总得回来吧?
最后的最后,再将原先那些必走的丁香道路边权和(初始的 \(S\) 集合边权和)统计到最终答案,那么最终答案就华丽的变成了最终答案(?
于是思路我们讲完了,再说一些事:
-
第一:我们可以发现,上述所有用到的操作,其实只用到了点的度数和并查集查询点所在连通块,因此你根本就不用建图;
-
第二:处理连通性跑最小生成树之前,提供给最小生成树选择的边也是有讲究的,为了维护边权和最小的性质,提供选择的边显然两个端点是“不在同一个连通块中的两个点”分别的并查集祖先(也就是连通块编号);但是边权需要做到应该是两个连通块中端点编号差最小值(对应最小的边权),实际我们可以这样做:
-
e.clear(); lst = 0; for (int j = 1; j <= n; ++j) { if (deg[j] == 0) continue; if (lst == 0 || d.get(a.get(lst)) == d.get(a.get(j))) { lst = j; continue; } edge now; now.u = a.get(lst); now.v = a.get(j); now.dis = j - lst; e += now; lst = j; }
-
代码中的
e
是一个basic_string <edge>
类型的变量,存的就是供最小生成树选择的边。观察这段代码,你会发现lst
只要不是deg[j] == 0
就会跟随j
,也就是存储j
的上一个度数不为 \(0\) (不是一个孤岛)的顶点编号。当我们发现j
和 上一个不是孤岛的点不在一个连通块,就连。你是否想问这样是否会漏掉?比如1和3之间本来不在一个并查集但是被2淹没了于是他们对应的连通块没有连。确实是这样的,但是,什么都不影响!原因如下(设 \(d_i\) 为 \(i\) 所在连通块号,有 \(d_1 \neq d_3\)): -
如果2是孤岛点,显然1和3会正常处理;
-
如果 \(d_2 = d_1\),于是 \(d_3\) 与 \(d_2\) 连,代价为 \(1\),是比你连 \(d_3\) 和 \(d_1\),代价 \(2\) 更优的;
-
如果 \(d_2 = d_3\),于是 \(d_2\) 与 \(d_1\) 连,代价为 \(1\),同样更优;
-
如果 \(d_2 \neq d_1\) 且 \(d_2 \neq d_3\),于是 \(d_2\) 与 \(d_1\) 连,\(d_3\) 与 \(d_2\) 连,代价分别为 \(1\),等效于连了一条 \(d_3\) 到 \(d_1\) 的代价为 \(2\) 的边。
-
所以这样是不会影响的。另外,这样你还会发现,给最小生成树的边最多只有 \(n - 1\) 条。
-
第三:要把 \(m\) 个丁香道路用并查集统一处理好,不需要每个循环都处理一遍。
-
第四:思考一下刚刚我们连奇点的时候,不直接连这对奇点而是,将这对奇点之间的所有点用链连起来。那如果我们直接连,会不会影响算法的正确性呢?会。原因:假设本来5和7是两个奇点,如果你的代码连了5和7,而6和5,7不连通,那么后面最小生成树我们还会跑一个5连6,6连7的结果,于是我们就花费了将5,6,7连成一个三角的代价(\(3\)),但事实上需要5 - 7这条边吗?不需要,你去掉它,5,6,7之间通向任意一个点的代价是不变的,因此你多花费了 \(1\) 的代价,答案不最优。
-
ps:这里是我们将连奇点方式改成直连的测评结果,果不其然WA60,鼠标移到WA的数据点,会发现我们得到的答案一直比正确答案要大。
\(\mathtt{Time} \text{ } \mathtt{Complexity}\)
\(\operatorname{O}(n^2\log n)\)
最外层 \(n\) 循环,里层最大的是边数为 \(n - 1\) 的最小生成树。
\(\mathtt{Code}\)
/*
* @Author: crab-in-the-northeast
* @Date: 2022-07-03 23:56:12
* @Last Modified by: crab-in-the-northeast
* @Last Modified time: 2022-07-04 02:14:54
*/
#include <bits/stdc++.h>
#define int long long
inline int read() {
int x = 0;
bool flag = true;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-')
flag = false;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + ch - '0';
ch = getchar();
}
if(flag) return x;
return ~(x - 1);
}
struct edge {
int u, v, dis;
};
std :: basic_string <edge> e;
bool cmp(edge a, edge b) {
return a.dis < b.dis;
}
const int maxn = 2505;
int deg[maxn];
struct query_set {
int fa[maxn];
void init(int n) {
for (int i = 1; i <= n; ++i)
fa[i] = i;
}
int get(int x) {
while (x != fa[x])
x = fa[x] = fa[fa[x]];
return x;
}
void merge(int x, int y) {
fa[get(x)] = get(y);
}
}a, d;
inline int abs(int x) {
return x > 0 ? x : -x;
}
signed main() {
int n = read(), m = read(), s = read(), flower = 0;
a.init(n);
for (int _ = 1; _ <= m; ++_) {
int u = read(), v = read();
++deg[u];
++deg[v];
a.merge(u, v);
flower += abs(u - v);
}
for (int i = 1; i <= n; ++i) {
d.init(n);
++deg[s];
++deg[i]; // 在 s 和 i 中连边 i -> s,变为构造欧拉回路
d.merge(a.get(s), a.get(i));
// 将奇点两两配对连边,同时考虑到此处边权的性质,u 和 v 连,为了让他最能有连通性
// 不连 u 和 v,而是把 u 和 v 中的所有点连成一条链
int lst = -1LL, free = 0;
for (int j = 1; j <= n; ++j) {
if ((deg[j] & 1) == 0)
continue;
if (lst == -1) {
lst = j;
continue;
}
for (int k = lst; k < j; ++k)
d.merge(a.get(k), a.get(k + 1));
free += j - lst;
lst = -1;
}
// 连通块连边
e.clear();
lst = 0;
for (int j = 1; j <= n; ++j) {
if (deg[j] == 0)
continue;
if (lst == 0 || d.get(a.get(lst)) == d.get(a.get(j))) {
lst = j;
continue;
}
edge now;
now.u = a.get(lst);
now.v = a.get(j);
now.dis = j - lst;
e += now;
lst = j;
}
// 在连通块连的边中跑 MST,获得最少代价联通方案
std :: sort(e.begin(), e.end(), cmp);
for (int i = 0; i < e.length(); ++i) {
edge now = e[i];
if (d.get(now.u) != d.get(now.v)) {
d.merge(now.u, now.v);
free += now.dis * 2; // 为了连通时不破坏奇偶性,只能连两次咯
}
}
// 把加来的 i -> s 这条边还回去
--deg[s];
--deg[i];
printf("%lld ", flower + free);
}
puts("");
return 0;
}
/*
4 3 1
1 2
4 2
3 1
*/
非常神奇的一道题,可以看出出题人有着非常强大的功底。
就算你不知道这题怎么做,翻看了题解之后,也不会有那么大的失落感,而是完全被这道题的绝妙思路而惊叹。(虽然我自己做出来了)
另外,这个题不需要建图,导致代码特别的诗意……
不夸了,反正很适合码代码的一道题。