题解:冗余路径问题
一、题目背景
有 (F) 个草场,奶牛们希望在任意两个草场之间至少有两条相互分离(路径无重合道路)的路径,现有 (R) 条连接不同草场的双向路。需要计算出最少新建道路的数量,以满足奶牛们的需求。
二、解题思路
本题可通过Tarjan算法找出图中的桥(割边),将图中的双连通分量缩点,然后根据缩点后图中度数为 (1) 的节点数量来计算最少新建道路的数量。因为要使图中任意两点间至少有两条分离路径,最终图应是一个边双连通图,而对于一个由边双连通分量缩点后形成的树状图,添加边的数量可通过度数为 (1) 的节点(叶子节点)数量来确定。
具体步骤如下:
1. 定义图的存储结构,包括邻接表相关数组、时间戳数组 dfn[N]
(记录节点首次访问的时间戳)、追溯数组 low[N]
(记录节点或其子孙能追溯到的最早时间戳)、栈数组 stk[N]
及栈顶指针 top
、节点所属双连通分量编号数组 id[N]
、双连通分量数量 dcc_cnt
、标记边是否为桥的数组 is_bridge[M]
、每个双连通分量的度数数组 d[N]
。
2. 编写添加边的函数 add
,用于构建邻接表。
3. 实现Tarjan算法函数 tarjan
,用于找出图中的桥和双连通分量:
- 更新当前节点的 dfn
和 low
值,并将节点入栈。
- 遍历当前节点的邻接边,对于未访问的节点,递归调用 tarjan
并更新 low
值,若发现桥则标记;对于已访问且不是反向边的节点,更新 low
值。
- 若当前节点的 dfn
和 low
值相等,说明找到了一个双连通分量,将栈中节点弹出并记录所属双连通分量编号。
4. 在 main
函数中:
- 读取草场数量 n
和道路数量 m
,初始化邻接表。
- 读取每条道路,构建图的邻接表。
- 从节点 1
开始调用Tarjan算法,找出所有桥和双连通分量。
- 遍历所有边,统计每个双连通分量的度数(与该双连通分量相连的桥的数量)。
- 统计度数为 (1) 的双连通分量的数量 cnt
。
- 输出最少新建道路的数量,计算公式为 (cnt + 1) / 2
。
三、代码逐段分析
(一)头文件和全局变量
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5010, M = 20010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int id[N], dcc_cnt;
bool is_bridge[M];
int d[N];
- 头文件:
#include <cstring>
:用于字符串操作和内存初始化函数,如memset
。#include <iostream>
:提供C++风格的输入输出流,如cin
和cout
。#include <algorithm>
:包含一些常用的算法函数。using namespace std;
:使用标准命名空间,以便直接使用标准库中的函数和类型。
- 常量定义:
const int N = 5010, M = 20010;
:定义节点的最大数量(草场数量)和边的最大数量。
- 变量定义:
int n, m;
:分别表示草场的数量和道路的数量。int h[N], e[M], ne[M], idx;
:邻接表相关数组,h[N]
存储每个节点的头指针,e[M]
存储边的终点,ne[M]
存储下一条边的指针,idx
用于边的编号。int dfn[N], low[N], timestamp;
:dfn[N]
记录节点首次访问的时间戳,low[N]
记录节点或其子孙能追溯到的最早时间戳,timestamp
是时间戳计数器。int stk[N], top;
:stk[N]
是栈数组,top
是栈顶指针。int id[N], dcc_cnt;
:id[N]
记录节点所属双连通分量编号,dcc_cnt
是双连通分量数量。bool is_bridge[M];
:标记每条边是否为桥。int d[N];
:记录每个双连通分量的度数(与该双连通分量相连的桥的数量)。
(二)添加边函数
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
该函数用于向邻接表中添加一条边,参数 a
为起点,b
为终点。通过更新邻接表数组,将边的信息存储起来。
(三)Tarjan算法函数
void tarjan(int u, int from)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j, i);
low[u] = min(low[u], low[j]);
if (dfn[u] < low[j])
is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if (i != (from ^ 1))
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ dcc_cnt;
int y;
do {
y = stk[top -- ];
id[y] = dcc_cnt;
} while (y != u);
}
}
- 初始化当前节点的
dfn
和low
值为当前时间戳,将节点入栈。 - 遍历当前节点的邻接边:
- 若邻接节点未访问,递归调用
tarjan
函数,并更新当前节点的low
值,若dfn[u] < low[j]
,说明边i
是桥,标记i
及其反向边i ^ 1
为桥。 - 若邻接节点已访问且不是反向边(
i != (from ^ 1)
),更新当前节点的low
值为当前low
值和邻接节点dfn
值的较小值。
- 若邻接节点未访问,递归调用
- 若当前节点的
dfn
和low
值相等,说明找到了一个双连通分量:- 双连通分量数量加 (1)。
- 从栈中弹出节点,记录所属双连通分量编号。
(四)main
函数
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
tarjan(1, -1);
for (int i = 0; i < idx; i ++ )
if (is_bridge[i])
d[id[e[i]]] ++ ;
int cnt = 0;
for (int i = 1; i <= dcc_cnt; i ++ )
if (d[i] == 1)
cnt ++ ;
printf("%d\n", (cnt + 1) / 2);
return 0;
}
- 读取草场数量
n
和道路数量m
,初始化邻接表。 - 读取每条道路,构建图的邻接表。
- 从节点
1
开始调用Tarjan算法,找出所有桥和双连通分量。 - 遍历所有边,若边是桥,则对应双连通分量的度数加 (1)。
- 统计度数为 (1) 的双连通分量的数量
cnt
。 - 输出最少新建道路的数量,计算公式为
(cnt + 1) / 2
。 - 程序结束。
四、时间复杂度和空间复杂度分析
(一)时间复杂度
- 建图:读取 (m) 条道路构建邻接表,时间复杂度为 (O(m))。
- Tarjan算法:每个节点最多被访问一次,每条边最多被遍历一次,时间复杂度为 (O(n + m))。
- 统计度数和结果:遍历 (m) 条边统计桥对应的双连通分量度数,时间复杂度为 (O(m));遍历 (dcc_cnt) 个双连通分量统计度数为 (1) 的数量,时间复杂度为 (O(dcc_cnt)),(dcc_cnt\leq n)。
总体时间复杂度为 (O(n + m))。
(二)空间复杂度
- 邻接表:存储边的信息,空间复杂度为 (O(m))。
- 其他数组:时间戳数组
dfn[N]
、追溯数组low[N]
、栈数组stk[N]
、节点所属双连通分量编号数组id[N]
、双连通分量数量dcc_cnt
、标记边是否为桥的数组is_bridge[M]
、每个双连通分量的度数数组d[N]
,空间复杂度均为 (O(n + m))(is_bridge[M]
为 (O(m)),其他与节点相关的数组为 (O(n)))。
总体空间复杂度为 (O(n + m))。
希望这份题解能帮助你理解这道题的解题思路和代码实现。如果有任何疑问,欢迎随时提问。