题解:洛谷 P6004 Wormhole Sort
【题目来源】
洛谷:P6004 [USACO20JAN] Wormhole Sort S - 洛谷
【题目描述】
Farmer John 的奶牛们已经厌倦了他对她们每天早上排好序离开牛棚的要求。她们刚刚完成了量子物理学的博士学位,准备将这一过程搞快点。
今天早上,如同往常一样,Farmer John 的 \(N\) 头编号为 \(1…N\) 的奶牛(\(1\le N\le 10^5\)),分散在牛棚中 \(N\) 个编号为 \(1…N\) 的不同位置,奶牛 \(i\) 位于位置 \(p_i\)。但是今天早上还出现了 \(M\) 个编号为 \(1…M\) 的虫洞(\(1\le M\le 10^5\)),其中虫洞 \(i\) 双向连接了位置 \(a_i\) 和 \(b_i\),宽度为 \(w_i(1\le a_i,b_i\le N,a_i\neq b_i,1\le w_i\le 10^9)\)。
在任何时刻,两头位于一个虫洞两端的奶牛可以选择通过虫洞交换位置。奶牛们需要反复进行这样的交换,直到对于 \(1\le i\le N\),奶牛 \(i\) 位于位置 \(i\)。
奶牛们不想被虫洞挤坏。帮助她们最大化被她们用来排序的虫洞宽度的最小值。保证奶牛们有可能排好序。
【输入】
输入的第一行包含两个整数 \(N\) 和 \(M\)。
第二行包含 \(N\) 个整数 \(p_1,p_2,…,p_N\)。保证 \(p\) 是 \(1…N\) 的一个排列。
对于 \(1\) 到 \(M\) 之间的每一个 \(i\),第 \(i+2\) 行包含整数 \(a_i,b_i,w_i\)。
【输出】
输出一个整数,为在排序过程中奶牛必须挤进的虫洞的最小宽度的最大值。如果奶牛们不需要用任何虫洞来排序,输出 \(−1\)。
【输入样例】
4 4
3 2 1 4
1 2 9
1 3 7
2 3 10
2 4 3
【输出样例】
9
【核心思想】
-
问题分析:给定 \(N\) 个位置上的奶牛排列 \(p[1..N]\)(\(p_i\) 表示位置 \(i\) 上的奶牛编号)和 \(M\) 个带宽度权值的虫洞(双向边)。奶牛只能通过虫洞交换位置,目标是让每个奶牛 \(i\) 回到位置 \(i\)。求所有使用虫洞中宽度最小值的最大值(最大化瓶颈),若无需虫洞则输出 \(-1\)。本质是一个带权图上的可行性判定 + 二分答案问题。
-
算法选择:
- 二分答案 + DFS连通性判定:对虫洞宽度阈值 \(x\) 进行二分,判定只使用宽度 \(\geq x\) 的虫洞时,所有"需要交换的位置"是否连通
- 关键观察:若只保留宽度 \(\geq x\) 的虫洞,则这些虫洞形成若干连通块。某个连通块内的奶牛可以任意互相交换位置(通过虫洞链),因此一个连通块内的位置错位可以内部自我纠正
-
关键步骤:
- 预处理错位位置:
- 读取排列 \(p\),建立值到位置的映射 \(p[x] = i\)(值 \(x\) 在位置 \(i\))
- 收集所有 \(x \neq i\) 的错位值存入数组 \(a[1..cur]\),这些是需要被"归位"的奶牛
- 若 \(cur = 0\)(已全部有序),输出 \(-1\)
- 建图:用邻接表存储所有虫洞(无向边,带权值 \(w\))
- 判定函数 \(\text{check}(x)\):
- 从第一个错位位置 \(u = p[a[1]]\) 开始 DFS,只遍历宽度 \(\geq x\) 的边
- 标记所有可达位置
- 检查所有错位位置 \(p[a[i]]\) 是否都被访问到,若全部连通则返回真
- 二分查找:
- 范围 \([1, 10^9]\),二分最大可行阈值
- 若 \(\text{check}(mid)\) 为真,说明可以承受更窄的虫洞(尝试更大 \(x\)),\(l = mid\)
- 否则 \(r = mid - 1\)
- 输出答案:\(l\)
- 预处理错位位置:
-
时间/空间复杂度:
- 时间复杂度:\(O(\log W \cdot (N + M))\),二分约 \(30\) 次迭代,每次 DFS \(O(N + M)\)
- 空间复杂度:\(O(N + M)\),邻接表和访问标记数组
-
二分答案 + 连通性判定的核心思想:
- 瓶颈最大化转化:将"最大化使用虫洞的最小宽度"转化为"寻找最大阈值 \(x\),使得只保留宽度 \(\geq x\) 的边后,所有需要交换的位置仍连通"
- 连通块内可任意交换:在一个连通块内部,通过虫洞链可以任意重排奶牛位置,因此只要所有错位位置落在同一连通块,就能完成排序
- 二分单调性:若阈值 \(x\) 可行(连通),则所有 \(\leq x\) 的阈值也可行(边更多),满足二分条件
- DFS筛选边:判定过程中不修改图结构,只通过 \(w[i] \geq x\) 条件过滤遍历的边,实现 \(O(1)\) 额外空间判定
- 适用于带权图瓶颈优化、网络可靠性、最小边权最大化类问题
【算法标签】
普及+ #整数二分
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 100005, M = 100005 * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx; // 邻接表
int p[N], cur, a[N]; // p: 值到位置的映射, a: 需要交换的值
bool vis[N]; // 访问标记
// 添加边
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// 深度优先搜索
void dfs(int u, int x)
{
vis[u] = true;
for (int i = h[u]; i != -1; i = ne[i]) // 遍历邻接点
{
int v = e[i];
if (w[i] >= x && !vis[v]) // 如果边权≥x且未访问
{
dfs(v, x);
}
}
}
// 检查在阈值x下是否可行
bool check(int x)
{
int u = p[a[1]]; // 获取第一个需要交换的位置
memset(vis, 0, sizeof(vis));
dfs(u, x);
for (int i = 1; i <= cur; i++) // 检查所有需要交换的位置是否连通
{
if (!vis[p[a[i]]])
{
return false;
}
}
return true;
}
int main()
{
cin >> n >> m; // 输入数组长度和边数
for (int i = 1; i <= n; i++) // 读取排列
{
int x;
cin >> x;
p[x] = i; // 记录值x所在的位置
if (x != i) // 如果位置不正确,需要交换
{
a[++cur] = x;
}
}
memset(h, -1, sizeof(h));
for (int i = 1; i <= m; i++) // 建图
{
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
add(v, u, w);
}
int l = 1, r = 1e9; // 二分边权阈值
while (l < r)
{
int mid = (l + r + 1) / 2;
if (check(mid)) // 如果mid可行,尝试更大的
{
l = mid;
}
else
{
r = mid - 1;
}
}
if (l == 1e9) // 如果阈值达到最大值
{
cout << -1 << endl;
}
else
{
cout << l << endl; // 输出最大可行阈值
}
return 0;
}
【运行结果】
4 4
3 2 1 4
1 2 9
1 3 7
2 3 10
2 4 3
9
浙公网安备 33010602011771号