题解:洛谷 P2860 Redundant Paths
【题目来源】
洛谷:P2860 [USACO06JAN] Redundant Paths G - 洛谷
【题目描述】
为了从 \(F(1\le F\le 5,000)\) 个牧场(编号为 \(1\) 到 \(F\))中的一个到达另一个牧场,贝西和其他牛群被迫经过腐烂苹果树附近。奶牛们厌倦了经常被迫走特定的路径,想要修建一些新路径,以便在任意一对牧场之间总是有至少两条独立的路线可供选择。目前在每对牧场之间至少有一条路径,他们希望至少有两条。当然,他们只能在官方路径上从一个牧场移动到另一个牧场。
给定当前 \(R(F-1\le R\le 10,000)\) 条路径的描述,每条路径恰好连接两个不同的牧场,确定必须修建的最少新路径数量(每条新路径也恰好连接两个牧场),以便在任意一对牧场之间至少有两条独立的路线。若两条路线不使用相同的路径,即使它们沿途访问相同的中间牧场,也被视为独立的。
在同一对牧场之间可能已经有多条路径,你也可以修建一条新路径连接与某条现有路径相同的牧场。
【输入】
第 \(1\) 行:两个用空格分隔的整数:\(F\) 和 \(R\)。
第 \(2\) 行到第 \(R+1\) 行:每行包含两个用空格分隔的整数,表示某条路径的两个端点牧场。
【输出】
第 \(1\) 行:一个整数,表示必须修建的新路径数量。
【输入样例】
7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7
【输出样例】
2
【核心思想】
-
问题分析:给定一个包含 \(F\) 个牧场 \(R\) 条路径的无向连通图,需要确定最少修建多少条新路径,使得任意两个牧场之间至少有两条独立的路径(即图变为边双连通图)。这是一个边双连通分量缩点 + 树形结构分析问题。
-
算法选择:
- Tarjan 算法求桥和边双连通分量:找出图中所有的桥,将边双连通分量缩点
- 缩点构建树:将每个边双连通分量看作一个节点,桥作为边,构建一棵树
- 叶子节点统计:统计缩点后树中度数为 \(1\) 的节点(叶子)数量
- 最少加边公式:\((leaf + 1) / 2\),即把叶子两两配对连接
-
关键步骤:
- 建图:读入 \(F\)(节点数)、\(R\)(边数),使用邻接表存储无向图
- Tarjan 求边双连通分量(从节点 \(x\) 出发,\(in_edg\) 为进入边):
- 初始化 \(dfn[x] = low[x] = ++tot\),节点入栈
- 遍历邻接点 \(y\):
- 树边(\(y\) 未访问):递归访问,回溯时 \(low[x] = \min(low[x], low[y])\)
- 判断桥:若 \(low[y] > dfn[x]\),标记边 \(i\) 和反向边 \(i \oplus 1\) 为桥
- 回边(\(y\) 已访问且不是来时的边):\(low[x] = \min(low[x], dfn[y])\)
- 缩点:若 \(dfn[x] == low[x]\),弹出栈中节点直到 \(x\),这些节点属于同一分量
- 统计度数:遍历所有边,若边 \(i\) 是桥,则 \(d[con_id[e[i].v]]++\)(增加目标分量的度数)
- 计算答案:统计度数为 \(1\) 的分量数量 \(cnt\),答案为 \((cnt + 1) / 2\)
-
时间/空间复杂度:
- 时间复杂度:\(O(F + R)\),Tarjan 算法线性时间
- 空间复杂度:\(O(F + R)\),邻接表、\(dfn\)/\(low\) 数组、栈、分量信息
- 适用于边双连通性分析和网络加固问题
-
边双连通分量缩点的核心思想:
- 边双连通分量:图中不存在桥的最大子图,分量内任意两点间有两条边不相交路径
- 桥的作用:桥是连接不同边双连通分量的唯一通道,删除桥会使图断开
- 缩点思想:将每个边双连通分量缩为一个节点,桥作为边,原图变成一棵树
- 树的性质:缩点后的树中,叶子节点(度数为 \(1\))代表只通过一座桥与其他分量连接的分量
- 最少加边策略:将叶子两两配对连接,\((leaf + 1) / 2\) 是最优解
- 适用场景:网络可靠性分析、关键路径识别、网络加固优化
【解题思路】




【算法标签】
提高+ #双连通分量
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 5005; // 最大节点数
const int M = 20005; // 最大边数(无向图需要两倍空间)
// 边结构体
struct edge
{
int v; // 边的终点
int ne; // 下一条边的索引
} e[M];
int h[N]; // 邻接表头指针数组
int idx = 1; // 边索引计数器(从2开始,便于异或操作)
int dfn[N]; // DFS序(时间戳)
int low[N]; // 通过回边能到达的最小DFN值
int tot; // 时间戳计数器
stack<int> stk; // Tarjan算法栈
int dcc[N]; // 存储每个节点所属的边双连通分量编号
int cnt; // 边双连通分量计数器
int bri[M]; // 标记边是否为桥
int d[N]; // 存储每个边双连通分量的度数
int n, m, a, b; // n:节点数, m:边数, a,b:临时变量
/**
* 添加无向边到邻接表
* @param a 边的起点
* @param b 边的终点
*/
void add(int a, int b)
{
e[++idx].v = b; // 记录边的终点
e[idx].ne = h[a]; // 新边指向原头节点
h[a] = idx; // 更新头节点指向新边
}
/**
* Tarjan算法求边双连通分量和桥
* @param x 当前节点
* @param in_edg 进入当前节点的边索引
*/
void tarjan(int x, int in_edg)
{
// 初始化当前节点的DFN和LOW值
dfn[x] = low[x] = ++tot;
// 将当前节点压入栈
stk.push(x);
// 遍历当前节点的所有邻接节点
for (int i = h[x]; i; i = e[i].ne)
{
int y = e[i].v; // 邻接节点
// 如果邻接节点y未被访问(树边)
if (!dfn[y])
{
tarjan(y, i); // 递归访问y
low[x] = min(low[x], low[y]); // 更新low值
// 桥的判断条件:low[y] > dfn[x]
if (low[y] > dfn[x])
{
bri[i] = bri[i ^ 1] = true; // 标记边i和反向边i^1为桥
}
}
// 如果y已被访问且不是来时的边(回边)
else if (i != (in_edg ^ 1))
{
low[x] = min(low[x], dfn[y]); // 通过回边更新low值
}
}
// 如果当前节点是边双连通分量的根
if (dfn[x] == low[x])
{
++cnt; // 增加边双连通分量计数
// 弹出栈中节点直到遇到当前节点
while (true)
{
int y = stk.top();
stk.pop();
dcc[y] = cnt; // 记录节点所属的边双连通分量
if (y == x)
{
break;
}
}
}
}
int main()
{
// 输入节点数和边数
cin >> n >> m;
// 输入所有边并构建无向图
while (m--)
{
cin >> a >> b;
add(a, b); // 添加边a->b
add(b, a); // 添加边b->a(无向图需要双向添加)
}
// 执行Tarjan算法(假设图连通,从节点1开始)
tarjan(1, 0);
// 统计每个边双连通分量的度数(桥的数量)
for (int i = 2; i <= idx; i++)
{
// 如果当前边是桥
if (bri[i])
{
// 增加目标节点所在分量的度数
d[dcc[e[i].v]]++;
}
}
// 统计度数为1的边双连通分量数量(叶子节点)
int sum = 0;
for (int i = 1; i <= cnt; i++)
{
if (d[i] == 1)
{
sum++;
}
}
// 输出需要添加的最少边数:(叶子节点数+1)/2
cout << (sum + 1) / 2 << endl;
return 0;
}
#include <bits/stdc++.h>
using namespace std;
const int N = 5005, M = 10005 * 2; // 注意:边数要乘2,因为是无向图
int n, m; // n: 节点数,m: 边数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int dfn[N], low[N], tot; // dfn: DFS序,low: 追溯值,tot: 时间戳
stack<int> stk; // 栈,用于Tarjan算法
int bri[M]; // 标记边是否为桥
int con_cnt, con_size[N], con_id[N]; // con_cnt: 边双连通分量数量,con_size: 分量大小,con_id: 节点所属分量编号
int d[N]; // 记录每个边双连通分量的度数
vector<int> ans[N]; // 存储每个边双连通分量的节点
// 添加无向边
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// Tarjan算法求边双连通分量
void tarjan(int x, int in_edg)
{
dfn[x] = low[x] = ++tot; // 初始化dfn和low为时间戳
stk.push(x); // 节点入栈
for (int i = h[x]; i != -1; i = ne[i]) // 遍历x的所有邻接点
{
int y = e[i]; // 邻接点y
if (!dfn[y]) // 如果y未被访问
{
tarjan(y, i); // 递归访问y
low[x] = min(low[x], low[y]); // 更新low[x]
if (low[y] > dfn[x]) // 判断边(x,y)是否为桥
bri[i] = bri[i ^ 1] = true; // 标记该边和其反向边为桥
}
else if (i != (in_edg ^ 1)) // 如果是回退边且不是反向边
{
low[x] = min(low[x], dfn[y]); // 更新low[x]
}
}
if (dfn[x] == low[x]) // 如果x是边双连通分量的根
{
con_cnt++; // 增加边双连通分量计数
while (1) // 弹出栈中节点
{
int y = stk.top(); // 取出栈顶
stk.pop(); // 弹出栈顶
con_id[y] = con_cnt; // 记录节点y所属的分量编号
con_size[con_cnt]++; // 更新分量大小
ans[con_cnt].push_back(y); // 将节点加入分量
if (y == x) // 直到弹出x为止
break;
}
}
}
int main()
{
cin >> n >> m; // 输入节点数和边数
memset(h, -1, sizeof(h)); // 初始化邻接表
while (m--) // 读入所有无向边
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a); // 添加无向边
}
for (int i = 1; i <= n; i++) // 遍历所有节点
if (dfn[i] == 0) // 如果节点未访问
tarjan(i, 0); // 执行Tarjan算法
// 统计每个边双连通分量的度数
for (int i = 0; i < idx; i++) // 遍历所有边
if (bri[i]) // 如果该边是桥
d[con_id[e[i]]]++; // 增加该边连接的分量的度数
int cnt = 0; // 统计度数为1的分量数量
for (int i = 1; i <= con_cnt; i++) // 遍历所有边双连通分量
if (d[i] == 1) // 如果分量的度数为1
cnt++; // 计数加1
cout << (cnt + 1) / 2 << endl; // 输出需要添加的边数
return 0;
}
【运行结果】
7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7
2
浙公网安备 33010602011771号