P6628 [省选联考 2020 B 卷] 丁香之路 解题报告
P6628 [省选联考 2020 B 卷] 丁香之路 解题报告
题目核心思想解读
首先,我们来拆解一下这个题目。我们需要做什么?
- 起点和终点:从一个固定的起点
s
出发,要到达一个目标终点t
(这个t
会从 1 变到n
,我们需要对每个t
都计算一次答案)。 - 必经之路:旅途中,必须把
m
条“丁香花”道路全部走过至少一遍。 - 图的结构:我们可以在一张 \(n\) 个点的“完全图”上行走,任意两点 \(i, j\) 之间都有一条边,走过去的时间(也就是边权)是 \(|i-j|\)。
- 目标:找到一条满足上述条件的最短路径,计算总花费时间。
这个“必须经过所有指定边”的要求,是解决本题的关键。这让我们联想到一个经典的图论问题:中国邮递员问题 (Chinese Postman Problem)。它的核心思想,和欧拉路径息息相关。
什么是欧拉路径?
简单来说,欧拉路径就是一条能“一笔画”走完图中所有边的路径。要满足什么条件才能“一笔画”呢?
- 欧拉回路(起点和终点是同一个点):图中所有点的度数(连接这个点的边的数量)都必须是偶数,并且图是连通的。
- 欧拉路径(起点和终点是不同的点):图中只有起点和终点的度数是奇数,其他所有点的度数都是偶数,并且图是连通的。
如何将题目转化为欧拉路径问题?
我们的目标是走完 m
条丁香路,并付出最小的额外代价。总花费可以分解为两部分:
总花费 = (必须走的 m
条丁香路的总长度) + (为了满足要求而额外走的路的总长度)
其中,m
条丁香路的总长度是固定的,我们无法改变。我们能优化的,只有“额外走的路”。怎么走才能让额外代价最小呢?答案是:让我们最终走过的所有边的集合,恰好构成一个从 s
到 t
的欧拉路径。
这样一来,我们就不需要重复走任何一条丁香路,也能把它们都串起来。
所以,我们的任务就变成了:在原图(有 n
个点,边权为 \(|i-j|\))中,添加一些代价最小的边,使得:
- 包含所有
m
条丁香路的图,变成一个连通图。 - 在这个新图中,起点
s
和终点t
的度数是奇数,其他所有点的度数都是偶数。
解题步骤详解
我们来一步步解决这个问题。对于每一个终点 t
(题目要求我们从 1 到 n
都计算一遍),我们都需要执行以下流程:
第 0 步:计算基础代价
首先,不管怎么走,m
条丁香路是必走的。我们先把它们的总长度算出来,记为 base_cost
。
base_cost = sum(|u-v|)
,对于所有给定的 m
条丁香路 (u,v)
。
// 对应代码
ll sum = 0;
for(int i=1; i<=m; i++) {
// ...读入 u, v
sum += abs(u-v);
}
第 1 步:分析度数(奇偶性问题)
欧拉路径对点的度数有严格要求。我们先统计一下只考虑 m
条丁香路时,每个点的度数。然后,因为我们要从 s
走到 t
,这相当于也引入了一条从 s
到 t
的路径,所以 s
和 t
的度数也要加 1。
现在,我们有了一份包含丁香路和 s->t
路径的度数表。表里可能会有很多度数为奇数的点。我们的目标是只保留 s
和 t
的奇数度,把其他所有奇数度的点都变成偶数度。怎么做呢?很简单,将奇数度的点两两配对,在它们之间连边。每连一条边,两个端点的度数都加 1,奇数就变成了偶数。
为了让额外代价最小,我们应该怎么配对呢?
有一个贪心结论:将所有奇数度的点按编号从小到大排序,然后依次将第1个和第2个配对,第3个和第4个配对,...,这样总代价最小。
例如,奇数点是 1, 4, 5, 8。最优配对是 (1,4) 和 (5,8),代价是 |1-4|+|5-8|=3+3=6
。
这个过程在代码中非常巧妙地实现了:
// 对应代码
ll ans = base_cost;
int pre = 0;
for(int j = 1; j <= n; j++) {
if (du[j] & 1) { // 如果j的度数是奇数
if (pre) { // 如果前面已经有一个未配对的奇数点pre
ans += j - pre; // 配对,增加代价
pre = 0; // 完成配对
} else {
pre = j; // 记录j,等待下一个奇数点
}
}
}
j-pre
就等于 |j-pre|
,因为循环中 j
总是大于 pre
。这部分代价我们称为 pairing_cost
。
第 2 步:保证图连通(连通性问题)
解决了度数问题后,还有一个要求:图必须是连通的。
最初的 m
条丁香路可能构成好几个独立的连通块(比如 (1,2)
和 (4,5)
就是两个块)。在第 1 步中,我们为了配对奇数点,又增加了一些边,这些边也可能会把一些连通块连接起来。
在完成度数配对后,我们检查一下图现在有几个连通块。如果多于一个,我们就必须在这些连通块之间“搭桥”,把它们全连起来。用什么方法搭桥代价最小呢?最小生成树 (MST)!
我们可以把每个连通块看作一个“超级点”,然后在这些超级点之间寻找最短的边来构建一棵最小生成树。
但是这里有个陷阱!我们构建的路径是一条线,不能凭空跳跃。如果我们从A连通块通过一座“桥”到了B连通块,走完B里面的路之后,我们还得原路返回A(或者去往C),才能继续我们的旅程。所以,每一座“桥”(MST中的每一条边)都必须被往返走两次。
因此,连接所有连通块的最小代价是:2 * (连接这些连通块的MST的总权值)。
这个过程在代码中是这样实现的:
- 初始连通块:用并查集(DSU)处理
m
条边,得到初始的连通块。 - 更新连通性:在第1步配对奇数点时,增加的边
(pre, j)
也连接了对应的连通块。 - 建MST:
- 找出所有可能用来“搭桥”的边。一个简单的策略是:考虑所有相邻点
(i, i+1)
,如果它们属于不同连通块,就是一座潜在的桥。代码中用了更优化的方法,只考虑了有边的点之间的连接。 - 将这些潜在的桥边按权值排序。
- 用 Kruskal 算法(基于并查集)找出 MST。
- 将 MST 的总权值乘以 2,加到最终答案里。
- 找出所有可能用来“搭桥”的边。一个简单的策略是:考虑所有相邻点
// 对应代码(Kruskal部分)
vector<edg> g; // 存储潜在的桥
// ... 生成g ...
sort(g.begin(), g.end()); // 按权值排序
for (auto edge : g) {
if (find(edge.u) != find(edge.v)) { // 如果连接了两个不同的块
fa[find(edge.u)] = find(edge.v);
ans += edge.w * 2; // 代价是边权的两倍
}
}
总结
对于每个终点 t
,总的最小花费时间 ans
的计算公式如下:
ans(t) = base_cost + pairing_cost(t) + bridging_cost(t)
base_cost
:m
条丁香路的总长度,固定不变。pairing_cost(t)
:为了修正奇数度点(包括s
和t
)而增加的边的总长度。bridging_cost(t)
:为了连通所有部分,搭桥产生的代价,等于2 * MST
权值。
我们对 t = 1, 2, ..., n
分别重复上述计算,就能得到所有答案。
这份解题报告希望能帮助你理解这个问题的思路和巧妙的解法。它完美地融合了欧拉路径、贪心和最小生成树等多个经典算法。