B3609 [图论与代数结构 701] 强连通分量
SCC, https://www.luogu.com.cn/problem/B3609
给定一张
注意,本题可能存在重边和自环。
输入格式
第一行两个正整数
接下来
输出格式
第一行一个整数表示这张图的强连通分量数目。
接下来每行输出一个强连通分量。第一行输出
输入输出样例 #1
输入 #1
6 8
1 2
1 5
2 6
5 6
6 1
5 3
6 4
3 4输出 #1
3
1 2 5 6
3
4说明/提示
对于所有数据,
这是一个经典的图论问题,要求寻找有向图中的所有强连通分量(Strongly Connected Components, SCC)。
算法选择
处理强连通分量最常用的算法是 Tarjan 算法。它的核心思想是利用 DFS 过程中每个节点的“发现时间”和“能够回溯到的最早节点”来确定 SCC。
- dfn[u]:记录节点
被 DFS 访问的次序(时间戳)。 - low[u]:记录节点
或其子树能够通过反向边追溯到的最早节点的 dfn值。 - 栈 (Stack):用于存放当前 DFS 路径上的节点。如果发现某个节点的
dfn[u] == low[u],说明找到了一个强连通分量的根,此时栈中该节点及其上方的所有节点共同构成一个 SCC。
题目要求细节
- 输出顺序:按照节点编号
的顺序检查,如果该节点所在的 SCC 没被输出过,则输出。 - 分量内部顺序:每个 SCC 内部的节点编号需从小到大排序。
- 处理重边和自环:Tarjan 算法天然支持处理重边和自环。
Python 代码实现
python
import sys
# 增加递归深度,防止 DFS 导致栈溢出
sys.setrecursionlimit(200000)
def solve():
# 使用 sys.stdin.read().split() 快速读取大数据量
input_data = sys.stdin.read().split()
if not input_data:
return
n = int(input_data[0])
m = int(input_data[1])
# 建立邻接表
adj = [[] for _ in range(n + 1)]
idx = 2
for _ in range(m):
u = int(input_data[idx])
v = int(input_data[idx+1])
adj[u].append(v)
idx += 2
dfn = [0] * (n + 1) # 发现时间戳
low = [0] * (n + 1) # 最早追溯时间戳
stack = [] # 模拟栈
in_stack = [False] * (n + 1)
timer = 0
# 用于存储所有找到的 SCC
sccs = []
# node_to_scc_id[i] 记录节点 i 属于第几个找到的 SCC (sccs列表的索引)
node_to_scc_id = [-1] * (n + 1)
def tarjan(u):
nonlocal timer
timer += 1
dfn[u] = low[u] = timer
stack.append(u)
in_stack[u] = True
for v in adj[u]:
if dfn[v] == 0: # 还没访问过
tarjan(v)
low[u] = min(low[u], low[v])
elif in_stack[v]: # 在栈中,说明构成了环
low[u] = min(low[u], dfn[v])
# 如果当前节点是 SCC 的根
if low[u] == dfn[u]:
current_scc = []
while True:
node = stack.pop()
in_stack[node] = False
current_scc.append(node)
if node == u:
break
# 按题目要求:内部排序
current_scc.sort()
sccs.append(current_scc)
scc_idx = len(sccs) - 1
for node in current_scc:
node_to_scc_id[node] = scc_idx
# 遍历所有点,处理非连通图
for i in range(1, n + 1):
if dfn[i] == 0:
tarjan(i)
# 输出结果
# 1. 第一行:SCC 的数量
print(len(sccs))
# 2. 按题目要求的顺序输出每个 SCC
scc_visited = [False] * len(sccs)
for i in range(1, n + 1):
sid = node_to_scc_id[i]
if sid != -1 and not scc_visited[sid]:
# 输出该 SCC 的所有成员
print(*(sccs[sid]))
scc_visited[sid] = True
if __name__ == "__main__":
solve()关键点解释:
- 快速 I/O:题目数据范围
,使用普通的 input()可能会导致超时。sys.stdin.read().split()是一次性读取所有内容并分割,效率极高。 - 递归深度:Python 默认递归深度仅为 1000 左右。对于
的图(如果图退化成一条链),必须使用 sys.setrecursionlimit调大深度。 - 输出逻辑:
- 先通过
node_to_scc_id记录每个点属于哪个分量。 - 然后从点
循环到 ,利用 scc_visited数组保证每个分量只被输出一次,且顺序符合“号点所在分量优先,其次 号...”的要求。
- 先通过
- 复杂度:Tarjan 算法的时间复杂度为
,排序所有 SCC 的总代价为 ,完全符合题目时限要求。