题解:洛谷 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
【核心思想】
-
问题分析:给定一个包含 \(n\) 个节点 \(m\) 条边的无向连通图,需要找出所有的桥(割边)。桥是指删除后会使图不再连通的边。这是一个无向图桥问题,可以使用 Tarjan 算法求解。
-
算法选择:
- Tarjan 算法求桥:基于 DFS,通过 \(dfn\) 和 \(low\) 数组判断桥
- \(dfn\) 数组:记录每个节点的 DFS 序(时间戳)
- \(low\) 数组:记录通过回边能到达的最小 \(dfn\) 值
- 桥的判断条件:对于边 \((x, y)\),若 \(low[y] > dfn[x]\),则该边是桥
-
关键步骤:
- 建图:读入 \(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])\)
- 树边(\(y\) 未被访问):
- 处理非连通图:遍历所有节点,对未访问的节点执行 Tarjan
- 排序输出:将所有桥按起点升序、起点相同按终点升序排序后输出
-
时间/空间复杂度:
- 时间复杂度:\(O(n + m)\),每个节点和边只被访问一次
- 空间复杂度:\(O(n + m)\),邻接表、\(dfn\) 数组、\(low\) 数组、桥数组
- 适用于无向图桥的查找问题
-
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
浙公网安备 33010602011771号