1. 问题描述:
给定一个由 n 个点 m 条边构成的无向图,请你求出该图删除一个点之后,连通块最多有多少。
输入格式
输入包含多组数据。每组数据第一行包含两个整数 n,m。接下来 m 行,每行包含两个整数 a,b,表示 a,b 两点之间有边连接。数据保证无重边。点的编号从 0 到 n−1。读入以一行 0 0 结束。
输出格式
每组数据输出一个结果,占一行,表示连通块的最大数量。
数据范围
1 ≤ n ≤ 10000,
0 ≤ m ≤15000,
0 ≤ a,b < n
输入样例:
3 3
0 1
0 2
2 1
4 2
0 1
2 3
3 1
1 0
0 0
输出样例:
1
2
2
来源:https://www.acwing.com/problem/content/description/1185/
2. 思路分析:
分析题目可以知道我们可以先统计出连通块的数目count,枚举从哪一个连通块中删除点,然后再枚举删除哪一个点,计算删除当前点之后可以得到的的连通块数目s,则最终的答案为s + count - 1,所以问题的核心是如何枚举删除某个连通块中的每一个点并且计算出s,我们其实可以使用求解割点的方法来解决,割点属于点的双连通分量的内容,我们可以使用tarjan算法来求解割点,类似于之前无向图的边双连通分量算法,需要借助一个时间戳timestamp和两个数组dfn,low,其中dfn[u]表示dfs遍历到节点u对应的时间戳,low[u]表示从节点u出发往下走能够遍历到的最早的时间戳,我们主要是分为两种情况结合递归调用完每一个子节点y之后的low[y] >= dfn[x]判断是否成立来计算连通块的数目:
- x不是根节点,删除节点x之后那么x的子树与x的父节点对应的连通块是不连通的,连通块的数目在原来删除节点x之后的子节点对应的连通块数目上加1
- x是根节点,那么去掉根节点x之后与删掉当前的根节点得到的子节点的连通块的数目还是一样的,也即没有影响
我们只需要递归调用完当前的节点u的某一个子节点next之后判断low[next] >= dfn[u]是否成立,如果成立说明当前的节点u是一个割点,那么连通块的数目加1,当我们遍历完根节点u的所有子节点之后那么表示枚举删除节点u之后得到的子节点的连通块的数目已经求解出来了,此时还需要判断一下当前删除的节点是否是父节点,如果不是父节点,还需要加上u的父节点对应的连通块,也即连通块数目需要加1。
因为节点编号为0 ~ n - 1,所以我们可以遍历每一个节点,以每一个点作为根节点调用tarjan算法,每调用一次那么连通块的数目加1,循环结束那么就可以计算出原图中总共的连通块的数目,并且每调用一次tarjan方法,其实上枚举的是以当前的节点编号作为根节点对应的连通块中的所有节点,因为tarjan算法基于dfs,所以可以在遍历每一个节点的时候枚举删除当前的节点,因为需要根据子节点的low[next]与当前根节点的dfn[u]的大小关系判断当前的删除的节点是否是割点,所以需要在递归调用tarjan算法之后来判断dfn[u]与low[next]的大小关系,这样实际上是枚举删除某个连通块中的某个节点,而dfs会遍历所有的节点这样就可以在某一个连通块中枚举删除每一个节点得到的对应的连通块数目,在所有的情况中取一个max就是当前连通块删除一个点之后得到的连通块的最大数目。
3. 代码如下:
c++代码:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 30010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int root, ans;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
int cnt = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
if (low[j] >= dfn[u]) cnt ++ ;
}
else low[u] = min(low[u], dfn[j]);
}
if (u != root && cnt) cnt ++ ;
ans = max(ans, cnt);
}
int main()
{
while (scanf("%d%d", &n, &m), n || m)
{
memset(dfn, 0, sizeof dfn);
memset(h, -1, sizeof h);
idx = timestamp = 0;
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
ans = 0;
int cnt = 0;
for (root = 0; root < n; root ++ )
if (!dfn[root])
{
cnt ++ ;
tarjan(root);
}
printf("%d\n", ans + cnt - 1);
}
return 0;
}
python代码(只有一个数据运行超过1s超时了):
from typing import List
import sys
class Solution:
root, timestamp, res = None, None, None
def tarjan(self, u: int, dfn: List[int], low: List[int], g: List[List[int]]):
dfn[u] = low[u] = self.timestamp + 1
self.timestamp += 1
count = 0
for next in g[u]:
if dfn[next] == 0:
self.tarjan(next, dfn, low, g)
low[u] = min(low[u], low[next])
# 结合后面直接更新low[u]的判断所以这里可以取等号
if low[next] >= dfn[u]: count += 1
else:
low[u] = min(low[u], dfn[next])
if u != self.root: count += 1
# 这里并不需要求解双连通分量, 枚举完删除当前子节点之后的连通块数目然后更新res即可
self.res = max(self.res, count)
def process(self):
while True:
n, m = map(int, input().split())
if n == 0 and m == 0: break
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)
# count计算原图中连通块的数目
count = self.res = self.timestamp = 0
# 枚举当前的根节点
for i in range(n):
# 枚举每一个连通块
if dfn[i] == 0:
count += 1
# 当前的根节点是i
self.root = i
self.tarjan(i, dfn, low, g)
print(self.res + count - 1)
if __name__ == "__main__":
# 设置递归最大调用次数
sys.setrecursionlimit(5000)
Solution().process()