找连通块、判断是否有环(如 207 题)等。部分题目做法不止一种。
模板(计算每个连通块的大小):
def solve(n: int, edges: List[List[int]]) -> List[int]:
# 节点编号从 0 到 n-1
g = [[] for _ in range(n)]
for x, y in edges:
g[x].append(y)
g[y].append(x) # 无向图
vis = [False] * n
def dfs(x: int) -> int:
vis[x] = True # 避免重复访问节点
size = 1
for y in g[x]:
if not vis[y]:
size += dfs(y)
return size
# 计算每个连通块的大小
ans = []
for i, b in enumerate(vis):
if not b: # i 没有访问过
size = dfs(i)
ans.append(size)
return ans思维扩展:
求最短路等。要求边权都是 (或者说都是同一个正数)。
模板(单源最短路):
# 计算从 start 到各个节点的最短路长度
# 如果节点不可达,则最短路长度为 -1
# 节点编号从 0 到 n-1,边权均为 1
def bfs(n: int, edges: List[List[int]], start: int) -> List[int]:
g = [[] for _ in range(n)]
for x, y in edges:
g[x].append(y)
g[y].append(x) # 无向图
dis = [-1] * n # -1 表示尚未访问到
dis[start] = 0
q = deque([start])
while q:
x = q.popleft()
for y in g[x]:
if dis[y] < 0:
dis[y] = dis[x] + 1
q.append(y)
return dis把状态抽象成图上的点,用 BFS 遍历这张图,计算从初始状态到目标状态的最短路长度。
可以锻炼状态设计能力。
专题:跳跃游戏
注:关于网格图的 DFS 和 BFS,请看 网格图题单。

把拓扑排序想象成一个黑盒,给它一堆杂乱的先修课约束,它会给你一个井井有条的课程学习安排。
这一种在图上的「排序」,可以把杂乱的点排成一排。前提条件是图中无环,从而保证每条边都是从排在前面的点,指向排在后面的点。即对于任意有向边 , 一定在 之前。
模板:
# 返回有向无环图(DAG)的其中一个拓扑序
# 如果图中有环,返回空列表
# 节点编号从 0 到 n-1
def topologicalSort(n: int, edges: List[List[int]]) -> List[int]:
g = [[] for _ in range(n)]
in_deg = [0] * n
for x, y in edges:
g[x].append(y)
in_deg[y] += 1 # 统计 y 的先修课数量
topo_order = []
q = deque(i for i, d in enumerate(in_deg) if d == 0) # 没有先修课,可以直接上
while q:
x = q.popleft()
topo_order.append(x)
for y in g[x]:
in_deg[y] -= 1 # 修完 x 后,y 的先修课数量减一
if in_deg[y] == 0: # y 的先修课全部上完
q.append(y) # 加入学习队列
if len(topo_order) < n: # 图中有环
return []
return topo_order学习拓扑排序前,请先完成 1557. 可以到达所有点的最少点数目,有助于理解拓扑排序。
思维扩展:
一般是刷表法。
思维扩展:
模板:
# 返回从起点 start 到每个点的最短路长度 dis,如果节点 x 不可达,则 dis[x] = math.inf
# 要求:没有负数边权
# 时间复杂度 O(n + mlogm),其中 m 是 edges 的长度。注意堆中有 O(m) 个元素
def shortestPathDijkstra(n: int, edges: List[List[int]], start: int) -> List[int]:
# 注:如果节点编号从 1 开始(而不是从 0 开始),可以把 n 加一
g = [[] for _ in range(n)] # 邻接表
for x, y, wt in edges:
g[x].append((y, wt))
# g[y].append((x, wt)) # 无向图加上这行
dis = [inf] * n
dis[start] = 0 # 起点到自己的距离是 0
h = [(0, start)] # 堆中保存 (起点到节点 x 的最短路长度,节点 x)
while h:
dis_x, x = heappop(h)
if dis_x > dis[x]: # x 之前出堆过
continue
for y, wt in g[x]:
new_dis_y = dis_x + wt
if new_dis_y < dis[y]:
dis[y] = new_dis_y # 更新 x 的邻居的最短路
# 懒更新堆:只插入数据,不更新堆中数据
# 相同节点可能有多个不同的 new_dis_y,除了最小的 new_dis_y,其余值都会触发上面的 continue
heappush(h, (new_dis_y, y))
return dis分层图最短路:
SPFA 与差分约束:
力扣上的 SPFA 题很少。
Floyd 算法本质是三维 DP。
模板:
# 返回一个二维列表,其中 (i,j) 这一项表示从 i 到 j 的最短路长度
# 如果无法从 i 到 j,则最短路长度为 math.inf
# 允许负数边权
# 如果计算完毕后,存在 i,使得从 i 到 i 的最短路长度小于 0,说明图中有负环
# 节点编号从 0 到 n-1
# 时间复杂度 O(n^3 + m),其中 m 是 edges 的长度
def shortestPathFloyd(self, n: int, edges: List[List[int]]) -> List[List[int]]:
f = [[inf] * n for _ in range(n)]
for i in range(n):
f[i][i] = 0
for x, y, wt in edges:
f[x][y] = min(f[x][y], wt) # 如果有重边,取边权最小值
f[y][x] = min(f[y][x], wt) # 无向图
for k in range(n):
for i in range(n):
if f[i][k] == inf: # 针对稀疏图的优化
continue
for j in range(n):
f[i][j] = min(f[i][j], f[i][k] + f[k][j])
return fBitset 优化 Floyd
涉及到 Kruskal 算法和 Prim 算法。前者一般用于稀疏图,后者一般用于稠密图。
注:如果要求最大生成树,把边权从大到小排序。
Kruskal 算法模板(用到了并查集,完整模板见 数据结构题单):
class UnionFind:
def __init__(self, n: int):
# 一开始有 n 个集合 {0}, {1}, ..., {n-1}
# 集合 i 的代表元是自己
self._fa = list(range(n)) # 代表元
self.cc = n # 连通块个数
# 返回 x 所在集合的代表元
# 同时做路径压缩,也就是把 x 所在集合中的所有元素的 fa 都改成代表元
def find(self, x: int) -> int:
# 如果 fa[x] == x,则表示 x 是代表元
if self._fa[x] != x:
self._fa[x] = self.find(self._fa[x]) # fa 改成代表元
return self._fa[x]
# 把 from 所在集合合并到 to 所在集合中
# 返回是否合并成功
def merge(self, from_: int, to: int) -> bool:
x, y = self.find(from_), self.find(to)
if x == y: # from 和 to 在同一个集合,不做合并
return False
self._fa[x] = y # 合并集合。修改后就可以认为 from 和 to 在同一个集合了
self.cc -= 1 # 成功合并,连通块个数减一
return True
# 计算图的最小生成树的边权之和
# 如果图不连通,返回 math.inf
# 节点编号从 0 到 n-1
# 时间复杂度 O(n + mlogm),其中 m 是 edges 的长度
def mstKruskal(n: int, edges: List[List[int]]) -> int:
edges.sort(key=lambda e: e[2])
uf = UnionFind(n)
sum_wt = 0
for x, y, wt in edges:
if uf.merge(x, y):
sum_wt += wt
if uf.cc > 1: # 图不连通
return inf
return sum_wt思维扩展
涉及到 Hierholzer 算法。
涉及到 Tarjan 算法。
模板(交替染色法):
# 返回图的二染色
# 如果是二分图,返回每个节点的颜色,用 1 和 2 表示两种颜色
# 如果不是二分图,返回空列表
# 时间复杂度 O(n+m),n 是点数,m 是边数
def colorBipartite(n: int, edges: List[List[int]]) -> List[int]:
# 建图(节点编号从 0 到 n-1)
g = [[] for _ in range(n)]
for x, y in edges:
g[x].append(y)
g[y].append(x)
# colors[i] = 0 表示未访问节点 i
# colors[i] = 1 表示节点 i 为红色
# colors[i] = 2 表示节点 i 为蓝色
colors = [0] * n
def dfs(x: int, c: int) -> bool:
colors[x] = c # 节点 x 染成颜色 c
for y in g[x]:
# 邻居 y 的颜色与 x 的相同,说明不是二分图,返回 False
# 或者继续递归,发现不是二分图,返回 False
if colors[y] == c or \
colors[y] == 0 and not dfs(y, 3 - c): # 1 和 2 交替染色
return False
return True
# 可能有多个连通块
for i, c in enumerate(colors):
if c == 0 and not dfs(i, 1):
# 从节点 i 开始递归,发现 i 所在连通块不是二分图
return []
return colors关于二分图的最大匹配,见下面网络流的题目。其中标有「一对一」的题目也可以用带权二分图最大匹配做。
由于有其他做法(比如状压 DP),难度分仅供参考。
模拟费用流
见 链表、树、回溯 题单的第三章节。
如果你发现有题目可以补充进来,欢迎评论反馈。