最近在准备算法竞赛的同学,一定对图论中的连通性问题不陌生。今天我们就来拆解一道经典题目——PTA平台的L2-013红色警报,用Python从零实现一个完整的解决方案。不同于常见的C++实现,我们将用更符合Python风格的邻接表结构,带你深入理解关键节点对图连通性的影响。
这道题的核心在于判断图中某个节点是否为"关键节点"——即删除该节点后,图的连通分量数是否增加。我们先来看题目给出的示例:
code复制5 4
0 1
1 3
3 0
0 4
5
1 2 0 4 3
这表示有5个城市(编号0-4),4条连接道路。随后5个城市依次被攻占。我们需要在每次攻占后判断连通性是否被破坏。
关键概念解析:
注意:即使原图本身不连通(有多个连通分量),只要删除节点后连通分量数比删除前增加,该节点就是关键节点。
Python中我们通常使用邻接表来表示图,这比邻接矩阵更节省空间,尤其适合稀疏图。
python复制from collections import defaultdict
class Graph:
def __init__(self, num_nodes):
self.adj_list = defaultdict(list)
self.num_nodes = num_nodes
self.removed = set() # 记录被删除的节点
def add_edge(self, u, v):
self.adj_list[u].append(v)
self.adj_list[v].append(u)
def remove_node(self, node):
self.removed.add(node)
def get_neighbors(self, node):
return [n for n in self.adj_list[node] if n not in self.removed]
计算连通分量数的经典方法是深度优先搜索(DFS)。我们实现一个非递归版的DFS,避免Python递归深度限制的问题。
python复制def count_components(graph):
visited = set()
components = 0
for node in range(graph.num_nodes):
if node not in graph.removed and node not in visited:
# 开始一个新的连通分量
components += 1
stack = [node]
visited.add(node)
while stack:
current = stack.pop()
for neighbor in graph.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
stack.append(neighbor)
return components
性能考虑:
现在我们把所有部分组合起来,实现完整的红色警报检测系统。
python复制def red_alert():
import sys
input = sys.stdin.read().split()
ptr = 0
N, M = int(input[ptr]), int(input[ptr+1])
ptr += 2
graph = Graph(N)
for _ in range(M):
u, v = int(input[ptr]), int(input[ptr+1])
graph.add_edge(u, v)
ptr += 2
K = int(input[ptr])
ptr += 1
attacks = list(map(int, input[ptr:ptr+K]))
ptr += K
original_components = count_components(graph)
current_components = original_components
for i, city in enumerate(attacks):
# 计算删除前的连通分量数
before = count_components(graph)
# 删除节点
graph.remove_node(city)
# 计算删除后的连通分量数
after = count_components(graph)
# 判断是否发出警报
if after > before:
print(f"Red Alert: City {city} is lost!")
else:
print(f"City {city} is lost.")
# 检查是否是最后一个城市
if i == K - 1:
remaining = N - len(graph.removed)
if remaining == 0:
print("Game Over.")
让我们用题目中的样例来测试我们的实现:
python复制# 测试样例
test_input = """
5 4
0 1
1 3
3 0
0 4
5
1 2 0 4 3
"""
import io
import sys
def test_red_alert():
sys.stdin = io.StringIO(test_input.strip())
red_alert()
test_red_alert()
预期输出应该与题目描述一致:
code复制City 1 is lost.
City 2 is lost.
Red Alert: City 0 is lost!
City 4 is lost.
City 3 is lost.
Game Over.
虽然DFS实现简单直观,但在处理大规模图时可能会遇到性能瓶颈。我们可以考虑以下优化方案:
并查集是处理连通性问题的另一种高效数据结构,特别适合动态连通性问题。
python复制class UnionFind:
def __init__(self, size):
self.parent = list(range(size))
self.count = size
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]] # 路径压缩
x = self.parent[x]
return x
def union(self, x, y):
x_root = self.find(x)
y_root = self.find(y)
if x_root != y_root:
self.parent[y_root] = x_root
self.count -= 1
def count_components_uf(graph):
uf = UnionFind(graph.num_nodes)
for node in range(graph.num_nodes):
if node in graph.removed:
continue
for neighbor in graph.get_neighbors(node):
if neighbor > node: # 避免重复处理边
uf.union(node, neighbor)
# 需要减去被删除的节点数
return uf.count - len(graph.removed)
| 特性 | DFS实现 | 并查集实现 |
|---|---|---|
| 时间复杂度 | O(V+E) | O(Eα(V)) |
| 空间复杂度 | O(V) | O(V) |
| 适合场景 | 静态图查询 | 动态图操作 |
| 实现难度 | 简单 | 中等 |
在实际应用中,如果只需要单次查询,DFS可能更简单直接;如果需要多次动态修改和查询,并查集会是更好的选择。
在实现这类图算法时,有几个常见的陷阱需要注意:
调试时可以尝试以下方法:
python复制# 调试示例
def debug_graph(graph):
print("当前图状态:")
for node in range(graph.num_nodes):
if node not in graph.removed:
neighbors = graph.get_neighbors(node)
print(f"{node}: {neighbors}")
print(f"被删除的节点: {graph.removed}")
这道题目虽然来自算法竞赛,但其核心思想在实际中有广泛应用:
如果想进一步挑战自己,可以尝试:
在实现这类算法时,Python的简洁语法和丰富的数据结构能大大降低编码复杂度,让我们更专注于算法逻辑本身。不过也要注意Python在性能敏感场景下的局限,必要时可以考虑使用Cython优化或转向其他语言实现核心部分。