1. 问题描述:
为了从 F 个草场中的一个走到另一个,奶牛们有时不得不路过一些她们讨厌的可怕的树。奶牛们已经厌倦了被迫走某一条路,所以她们想建一些新路,使每一对草场之间都会至少有两条相互分离的路径,这样她们就有多一些选择。每对草场之间已经有至少一条路径。给出所有 R 条双向路的描述,每条路连接了两个不同的草场,请计算最少的新建道路的数量,路径由若干道路首尾相连而成。两条路径相互分离,是指两条路径没有一条重合的道路。但是,两条分离的路径上可以有一些相同的草场。对于同一对草场之间,可能已经有两条不同的道路,你也可以在它们之间再建一条道路,作为另一条不同的道路。
输入格式
第 1 行输入 F 和 R。接下来 R 行,每行输入两个整数,表示两个草场,它们之间有一条道路。
输出格式
输出一个整数,表示最少的需要新建的道路数。
数据范围
1 ≤ F ≤ 5000,
F − 1 ≤ R ≤ 10000
输入样例:
7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7
输出样例:
2
来源:https://www.acwing.com/problem/content/397/
2. 思路分析:
分析题目可以知道我们需要使得每一对点之间都存在两条相互分离的路径(也即任意两点一定包含两条不相交的路径),由这个特点可以知道其实原问题等价于我们至少需要加多少条边使其变成一个边的双连通分量,这里有一个结论是将原图缩点之后会变成一棵树,那么树中度数为1的节点个数count除以2向上取整就是需要至少加的边的数目,也即(count + 1)/ 2==> 向下取整,这个结论的证明比较复杂,在使用的时候记住即可。因为是无向连通图所以经过tarjan算法求解边的双连通分量,缩点之后最终会变成一棵树(将每一个边的双连通分量看成是一个点)。类似于有向图的强联通分量求解方法,我们也需要借助于一个栈stk来记录当前遍历的节点,将找到的边的双连通分量中的所有点记录在对应边的双连通分量idx编号中,在递归调用tarjan方法的过程中找到每一条桥,因为使用的是python语言所以需要使用列表rec来记录桥的两个节点编号,当求解完边的双连通分量之后将所有桥的两个端点所在的边的双连通分量编号的度数加1,最终遍历一下所有边的双连通分量,计算度数为1的节点个数,也即树中叶子节点的个数count即可,最终(count + 1) / 2就是答案:
3. 代码如下:
from typing import List
class Solution:
# 定义无向图的tarjan算法需要使用到的全局变量
stk, idx, timestamp, top, dcc_cnt, rec = None, None, None, None, None, None
# u表示当前遍历的节点,fa表示当前遍历节点的父节点
def tarjan(self, u: int, fa: int, dfn: List[int], low: List[int], g: List[List[int]]):
dfn[u] = low[u] = self.timestamp + 1
self.timestamp += 1
self.stk[self.top + 1] = u
self.top += 1
for next in g[u]:
if dfn[next] == 0:
self.tarjan(next, u, dfn, low, g)
low[u] = min(low[u], low[next])
# 当前节点无法到达祖先节点所以边: u<-->next是桥
if dfn[u] < low[next]:
# 因为需要记录点所以需要使用列表来记录桥的两个端点, 而使用字典来记录会忽略掉一些重复值导致答案错误, 下面记录的是桥的两个端点
self.rec.append(u)
self.rec.append(next)
# 需要注意下一个点不等于父节点才可以更新low[u]
elif next != fa:
low[u] = min(low[u], dfn[next])
# 找到一个边的双连通分量将其所有点记录在对应边的双连通分量编号中
if dfn[u] == low[u]:
self.dcc_cnt += 1
while True:
t = self.stk[self.top]
self.top -= 1
self.idx[t] = self.dcc_cnt
if t == u: break
def process(self):
n, m = map(int, input().split())
g = [list() for i in range(n + 10)]
for i in range(m):
a, b = map(int, input().split())
# 因为是无向边所以要记录两个方向
g[a].append(b)
g[b].append(a)
dfn, low = [0] * (n + 10), [0] * (n + 10)
self.stk, self.idx = [0] * (n + 10), [0] * (n + 10),
self.top = self.timestamp = self.dcc_cnt = 0
# 使用列表来记录桥
self.rec = list()
# 因为保证图是连通的所以只需要从1号点调用tarjan方法即可遍历到所有的点
self.tarjan(1, -1, dfn, low, g)
count = 0
d = [0] * (self.dcc_cnt + 1)
# 遍历记录的桥的两个端点, 对应的边的双连通分量编号的度数加1
for x in self.rec:
d[self.idx[x]] += 1
for i in range(1, self.dcc_cnt + 1):
# 计算叶子节点的个数
if d[i] == 1: count += 1
# 无向图需要添加(count + 1) / 2条边使其变成边的双连通分量
return (count + 1) // 2
if __name__ == "__main__":
print(Solution().process())