题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(N\) 个位置上的奶牛排列 \(p[1..N]\)\(p_i\) 表示位置 \(i\) 上的奶牛编号)和 \(M\) 个带宽度权值的虫洞(双向边)。奶牛只能通过虫洞交换位置,目标是让每个奶牛 \(i\) 回到位置 \(i\)。求所有使用虫洞中宽度最小值的最大值(最大化瓶颈),若无需虫洞则输出 \(-1\)。本质是一个带权图上的可行性判定 + 二分答案问题。

  2. 算法选择

    • 二分答案 + DFS连通性判定:对虫洞宽度阈值 \(x\) 进行二分,判定只使用宽度 \(\geq x\) 的虫洞时,所有"需要交换的位置"是否连通
    • 关键观察:若只保留宽度 \(\geq x\) 的虫洞,则这些虫洞形成若干连通块。某个连通块内的奶牛可以任意互相交换位置(通过虫洞链),因此一个连通块内的位置错位可以内部自我纠正
  3. 关键步骤

    • 预处理错位位置
      • 读取排列 \(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\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(\log W \cdot (N + M))\),二分约 \(30\) 次迭代,每次 DFS \(O(N + M)\)
    • 空间复杂度:\(O(N + M)\),邻接表和访问标记数组
  5. 二分答案 + 连通性判定的核心思想

    • 瓶颈最大化转化:将"最大化使用虫洞的最小宽度"转化为"寻找最大阈值 \(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
posted @ 2026-08-29 09:38  团爸讲算法  阅读(6)  评论(0)    收藏  举报