题解:洛谷 P1656 炸铁路

【题目来源】

洛谷:P1656 炸铁路 - 洛谷

【题目描述】

A 国派出将军 uim,对 B 国进行战略性措施,以解救涂炭的生灵。

B 国有 \(n\) 个城市,这些城市以铁路相连。任意两个城市都可以通过铁路直接或者间接到达。

uim 发现有些铁路被毁坏之后,某两个城市无法互相通过铁路到达。这样的铁路就被称为 key road。

uim 为了尽快使该国的物流系统瘫痪,希望炸毁铁路,以达到存在某两个城市无法互相通过铁路到达的效果。

然而,只有一发炮弹(A 国国会不给钱了)。所以,他能轰炸哪一条铁路呢?

【输入】

第一行 \(n,m\ (1 \leq n\leq 150\)\(1 \leq m \leq 5000)\),分别表示有 \(n\) 个城市,总共 \(m\) 条铁路。

以下 \(m\) 行,每行两个整数 \(a, b\),表示城市 \(a\) 和城市 \(b\) 之间有铁路直接连接。

【输出】

输出有若干行。

每行包含两个数字 \(a,b\),其中 \(a<b\),表示 \(\lang a,b\rang\) 是 key road。

请注意:输出时,所有的数对 \(\lang a,b\rang\) 必须按照 \(a\) 从小到大排序输出;如果 \(a\) 相同,则根据 \(b\) 从小到大排序。

【输入样例】

6 6
1 2
2 3
2 4
3 5
4 5
5 6

【输出样例】

1 2
5 6

【核心思想】

  1. 问题分析:给定一个包含 \(n\) 个节点 \(m\) 条边的无向连通图,需要找出所有的桥(割边)。桥是指删除后会使图不再连通的边。这是一个无向图桥问题,可以使用 Tarjan 算法求解。

  2. 算法选择

    • Tarjan 算法求桥:基于 DFS,通过 \(dfn\)\(low\) 数组判断桥
    • \(dfn\) 数组:记录每个节点的 DFS 序(时间戳)
    • \(low\) 数组:记录通过回边能到达的最小 \(dfn\)
    • 桥的判断条件:对于边 \((x, y)\),若 \(low[y] > dfn[x]\),则该边是桥
  3. 关键步骤

    • 建图:读入 \(n\)(节点数)、\(m\)(边数),使用邻接表存储无向图(每条边存两次)
    • Tarjan DFS(从节点 \(x\) 出发,\(in_edg\) 为进入 \(x\) 的边索引):
      • 初始化 \(dfn[x] = low[x] = ++tot\)(时间戳递增)
      • 遍历 \(x\) 的所有出边,设邻接节点为 \(y\)
        • 树边\(y\) 未被访问):
          • 递归执行 \(tarjan(y, j)\)
          • 回溯时更新 \(low[x] = \min(low[x], low[y])\)
          • 判断桥:若 \(low[y] > dfn[x]\),说明 \(y\) 无法通过回边到达 \(x\) 或其祖先,边 \((x, y)\) 是桥
          • 记录桥(确保 \(x < y\) 便于排序)
        • 回边\(y\) 已被访问且不是来时的边):
          • 更新 \(low[x] = \min(low[x], dfn[y])\)
    • 处理非连通图:遍历所有节点,对未访问的节点执行 Tarjan
    • 排序输出:将所有桥按起点升序、起点相同按终点升序排序后输出
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n + m)\),每个节点和边只被访问一次
    • 空间复杂度:\(O(n + m)\),邻接表、\(dfn\) 数组、\(low\) 数组、桥数组
    • 适用于无向图桥的查找问题
  5. Tarjan 求桥的核心思想

    • DFS 树:通过 DFS 构建搜索树,边分为树边和回边
    • \(dfn\)\(low\) 的意义
      • \(dfn[x]\):节点 \(x\) 在 DFS 中的访问顺序
      • \(low[x]\)\(x\) 及其子树通过回边能到达的最小 \(dfn\)
    • 桥的判断原理:若 \(low[y] > dfn[x]\),说明 \(y\) 的子树无法通过回边到达 \(x\) 或其祖先,删除边 \((x, y)\)\(y\) 的子树与图其他部分断开
    • 无向图边的处理:每条无向边在邻接表中存两次,通过 \(j \neq (in_edg \oplus 1)\) 避免走回头路
    • 重边的影响:若 \(x\)\(y\) 之间有多条边,则这些边都不是桥(因为删除一条还有另一条连接)
    • 适用于网络关键路径识别、网络脆弱性分析等场景

【算法标签】

普及- #双连通分量

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 155;      // 最大节点数
const int M = 10005;    // 最大边数

// 边结构体
struct edge
{
    int u, v;           // u:起点, v:终点
};

// 桥结构体,支持排序
struct bridge
{
    int x, y;           // 桥的两个端点

    // 重载小于运算符,用于排序
    bool operator<(const bridge &t) const
    {
        if (x == t.x)
            return y < t.y;
        return x < t.x;
    }
} bri[M];               // 存储所有桥

int n, m, a, b, cnt;    // n:节点数, m:边数, cnt:桥的数量
int dfn[N];             // DFS序(时间戳)
int low[N];             // 通过回边能到达的最小DFN值
int tot;                // 时间戳计数器

vector<edge> e;         // 存储所有边的数组
vector<int> h[N];       // 邻接表,h[i]存储从节点i出发的边在e中的索引

/**
 * 添加无向边到图中
 * @param a 边的起点
 * @param b 边的终点
 */
void add(int a, int b)
{
    e.push_back({a, b});            // 添加边到边数组
    h[a].push_back(e.size() - 1);   // 记录边在数组中的索引
}

/**
 * Tarjan算法寻找桥
 * @param x 当前节点
 * @param in_edg 进入当前节点的边索引(用于避免重复访问)
 */
void tarjan(int x, int in_edg)
{
    // 初始化当前节点的DFN和LOW值
    dfn[x] = low[x] = ++tot;

    // 遍历当前节点的所有出边
    for (int i = 0; i < h[x].size(); i++)
    {
        int j = h[x][i];            // 当前边在e数组中的索引
        int y = e[j].v;             // 邻接节点

        // 如果邻接节点y未被访问(树边)
        if (!dfn[y])
        {
            tarjan(y, j);           // 递归访问y
            low[x] = min(low[x], low[y]);  // 更新low值

            // 桥的判断条件:low[y] > dfn[x]
            if (low[y] > dfn[x])
            {
                // 记录桥(确保x<y,便于排序输出)
                if (x < y)
                    bri[++cnt] = {x, y};
                else
                    bri[++cnt] = {y, x};
            }
        }
        // 如果y已被访问且不是来时的边(回边)
        else if (j != (in_edg ^ 1))
        {
            low[x] = min(low[x], dfn[y]);  // 通过回边更新low值
        }
    }
}

int main()
{
    // 输入节点数和边数
    cin >> n >> m;

    // 输入所有边并构建无向图
    while (m--)
    {
        cin >> a >> b;
        add(a, b);  // 添加边a->b
        add(b, a);  // 添加边b->a(无向图需要双向添加)
    }

    // 对每个未访问的节点执行Tarjan算法
    for (int i = 1; i <= n; i++)
    {
        if (!dfn[i])
        {
            tarjan(i, 0);
        }
    }

    // 对桥进行排序(按起点升序,起点相同按终点升序)
    sort(bri + 1, bri + cnt + 1);

    // 输出所有桥
    for (int i = 1; i <= cnt; i++)
    {
        cout << bri[i].x << " " << bri[i].y << endl;
    }

    return 0;
}

【运行结果】

6 6
1 2
2 3
2 4
3 5
4 5
5 6
1 2
5 6
posted @ 2026-06-16 10:15  团爸讲算法  阅读(11)  评论(0)    收藏  举报