本篇文章根据labuladong的算法小抄汇总图的常见算法,采用python3实现
文章目录
图论基础

邻接表的特点:占用空间少,但无法快速判断两个节点是否相邻。
图的遍历框架:
def traverse(graph,s):
if visited[s]:
return
visited[s] = True
onPath[s] = True
for neighbor in graph.neighbor(s):
traverse(graph,neighbor)
onPath[s] = False
所有可能路径,T797
def allPathsSourceTarget(graph):
def traverse(graph,s,path):
path.append(s)
n = len(graph)
if s == n-1:
res.append(path[:])
for v in graph[s]:
traverse(graph,v,path)
path.pop()
path = [] #递归过程中经过的路径
res = [] #记录目标路径
traverse(graph,0,path)
return res
(课程表)判断有向图是否存在环,T207
依赖问题一般转化为有向图,存在环就说明存在循环依赖
def canFinish(numCourses,prerequisites):
#构图
def buildGraph(numCourses,prerequisites):
graph = [[] for i in range(numCourses)]
for edge in prerequisites:
from_ = edge[1]
to = edge[0]
graph[from_].append(to)
return graph
def traverse(graph,s):
if onPath[s]:
self.hasCycle = True
if visited[s] or hasCycle:
return
visited[s] = True
onPath[s] = True
for t in graph[s]:
traverse(graph,t)
onPath[s] = False
graph = buildGraph(numCourses,prerequisites)
visited = [False for i in range(numCourses)]
onPath = [False for i in range(numCourses)]
self.hasCycle = False
for i in range(numCourses):
traverse(graph,i)
return not self.hasCycle
(课程表)拓扑排序,T210
拓扑排序:
实际上就是把一幅图拉平,使所有箭头方向一致。显然,如果有向图中有环,则无法进行拓扑排序。拓扑排序只针对有向无环图。
如果把课程抽象成结点,依赖关系抽象成有向边,则图的拓扑排序结果就是上课顺序。
而将后序遍历的结果反转,就是拓扑排序的结果。
def findOrder(numCourses,prerequisites):
def buildGraph(numCourses,prerequisites):
graph = [[] for i in range(numCourses)]
for x in prerequisites:
from_ = x[1]
to = x[0]
graph[from_].append(to)
return graph
def traverse(graph,s):
if onPath[s]:
self.hasCycle = True
return
if visited[s]:
return
onPath[s] = True
visited[s] = True
for v in graph[s]:
traverse(graph,v)
postorder.append(s)
onPath[s] = False
graph = buildGraph(numCourses,prerequisites)
self.hasCycle = False
visited = [False for i in range(numCourses)]
onPath = [False for i in range(numCourses)]
postorder = []
for i in range(numCourses):
traverse(graph,i)
if self.hasCycle:
return []
postOrder.reverse()
return postOrder
二分图
二分图的顶点集可分割为两个互不相交的子集,图中每条边依附的两个顶点都分属于这两个子集,且两个子集内的顶点不相邻。
从简单实用的角度看,二分图结构在某些场景可以更高效地存储数据。
判断二分图
主要思想:遍历一遍图,一边遍历一边染色,看能不能用两种颜色给所有节点染色,且相邻节点的颜色都不相同。
这里用DFS算法解决。
#二叉树遍历框架
def traverse(root):
if not root:
return
traverse(root.left)
traverse(root.right)
#多叉树遍历框架
def traverse(root):
if not root:
return
for child in root.children:
traverse(child)
#图遍历框架
visited = [False for i in range(len(graph))]
def traverse(graph,v):
if visited(v):
return
#前序遍历位置
visited = True
for neighbor in graph.neighbors(v):
traverse(graph,neighbor)
判定二分图的代码逻辑:
def traverse(graoh,visited,v):
visited[v] = True
#遍历v的所有相邻节点
for neighbor in graph.neighbors(v):
if not visited[neighbor]:
#如果没有访问过,就涂上和v不同的颜色
traverse(graph,visited,neighbor)
else:
#如果访问过,就比较neighbor和v的颜色,若相同则不是二分图
DFS判断二分图,T785
def isBipartite(graph):
def traverse(graph,v):
if not ok:
return
visited[v] = True
for w in graph[v]:
if not visited[w]:
color[w] = not color[v]
traverse(graph,w)
else:
if color[w] == color[v]:
ok = False
return
ok = True #是否为二分图
n = len(graph)
color = [False for i in range(n)] #记录节点颜色,True和False代表两种不同颜色
visited = [False for i in range(n)] #记录节点是否被访问过
for v in range(n):
if not visited[v]:
traverse(graph,v)
return ok
BFS判断二分图,T785
def isBipartite(graph):
def bfs(graph,start):
q = []
visited[start] = True
q.append(start)
while (len(q) != 0) and ok:
v = q.pop(0)
for w in graph[v]:
if not visited[w]:
color[w] = not color[v]
visited[w] = True
q.append(w)
else:
if color[w] == color[v]:
ok = False
n = len(graph)
ok = True
color = [False for i in range(n)]
visited = [False for i in range(n)]
for i in range(n):
if not visited[i]:
bfs(graph,i)
return ok
可能的二分法,T886
把N个人(1,…,N)分进两组,每个人都可能不喜欢其他人,如果这样,那他们不属于同一组,问是否可以实现
def possibleBipartition(n,dislikes):
def buildGraph(n,dislikes):
graph = [[] for i in range(n+1)]
for x in dislikes:
a = x[0]
b = x[1]
graph[a].append(b)
graph[b].append(a)
return graph
def traverse(graph,s):
if not self.ok:
return
visited[s] = True
for w in graph[s]:
if not visited[w]:
color[w] = not color[s]
traverse(graph,w)
elif color[w] == color[s]:
self.ok = False
return
color = [False for i in range(n+1)]
visited = [False for i in range(n+1)]
graph = buildGraph(n,dislikes)
self.ok = True
for v in range(1,n+1):
if not visited[v]:
traverse(graph,v)
return self.ok
Union-Find并查集算法
动态连通性
并查集算法主要是解决图论中的动态连通性问题。
动态连通性可以看作给一幅图连线。

并查集算法主要实现以下API:
class UF:
#连接p和q
def union(p,q)
#判断p和q是否连通
def connected(p,q)
#返回途中有多少连通分量
def count()
其中连通是指一种等价关系,具有自反性(p和p连通),对称性(p和q连通则q和p连通),传递性(如果p和q连通,q和r连通,那么p和r也连通)。
我们使用森林(若干树)来表示图的动态连通性,用数组来实现这个森林。
对于森林,设定树的每个节点有个指针指向其父节点,如果是根节点,则指针指向自己。
如果两个节点被连通,则让其中一个节点的根节点接到另一个节点的根节点上。
class UF:
def __init__(self,n):
self.count = n
self.parent = [i for i in range(n)]
#返回节点的根节点
def find(x):
while self.parent[x] != x:
x = self.parent[x]
return x
def union(p,q):
rootP = find(p)
rootQ = find(q)
if rootP == rootQ:
return
parent[rootP] = rootQ
self.count -= 1
def connected(p,q):
rootP = find(p)
rootQ = find(q)
return rootP == rootQ
def count():
return self.count
find,connected,union的复杂度都是O(N),而变为平衡的二叉树就是O(logN)。
我们希望将小一些的树接到大一些的树下面,可以避免头重脚轻,要平衡一些。解决方法是额外使用一个size数组,记录每棵树包含的节点数,称之为重量。
class UF:
def __init__(self,n):
self.count = n #记录连通分量个数
self.parent = [i for i in range(n)] #记录若干棵树
self.size = [1 for i in range(n)] #记录树的重量
#返回节点的根节点
def find(self,x):
while self.parent[x] != x:
x = self.parent[x]
return x
def union(self,p,q):
rootP = self.find(p)
rootQ = self.find(q)
if rootP == rootQ:
return
if self.size[rootP] > self.size[rootQ]:
self.parent[rootQ] = rootP
self.size[rootP] += self.size[rootQ]
else:
self.parent[rootP] = rootQ
self.size[rootQ] += self.size[rootP]
self.count -= 1
def connected(p,q):
rootP = self.find(p)
rootQ = self.find(q)
return rootP == rootQ
def count():
return self.count
这样使得树的生长相对平衡,树的高度大致在logN左右。
路径压缩,进一步压缩每棵树的高度,使树高始终保持为常数。这样find,connected和union复杂度都降为O(1)。
只需将find改为:
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
可以在调用find时,顺手将树高缩短,最终所有树高都不会超过3。
class UF:
def __init__(self,n):
self.count = n #记录连通分量个数
self.parent = [i for i in range(n)] #记录若干棵树
self.size = [1 for i in range(n)] #记录树的重量
#返回节点的根节点
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,p,q):
rootP = self.find(p)
rootQ = self.find(q)
if rootP == rootQ:
return
if self.size[rootP] > self.size[rootQ]:
self.parent[rootQ] = rootP
self.size[rootP] += self.size[rootQ]
else:
self.parent[rootP] = rootQ
self.size[rootQ] += self.size[rootP]
self.count -= 1
def connected(p,q):
rootP = self.find(p)
rootQ = self.find(q)
return rootP == rootQ
def count():
return self.count
DFS的替代方案
被围绕的区域,T130
给定一个M*N的二维矩阵,包含X和O,让你找到矩阵中四面被X围住的O,并且把他们替换成X。
可以发现,边角上的O一定不会被围,与边角上的O相连的O也不会被围。
可以把不需要被替换的O看作一个门派,有个共同的祖师爷dummy,这些O和dummy互相连通,而需要被替换的O与dummy不连通。
二维坐标(x,y)可以映射到一维,转换成x*n+y(m为棋盘行数,n为棋盘列数)
[ 0 , . . . , m ∗ n − 1 ] [0,...,m*n-1] [0,...,m∗n−1]是棋盘内坐标,让dummy占据 m ∗ n m*n m∗n
def solve(board):
if len(board) == 0:
return
m = len(board)
n = len(board[0])
uf = UF(m * n + 1)
dummy = m * n
#将首列和末列的O与dummy连通
for i in range(m):
if board[i][0] == 'O':
uf.union(i * n, dummy)
if board[i][n-1] == 'O':
uf.union(i * n + n - 1,dummy)
#将首列和末列的O与dummy连通
for j in range(n):
if board[0][j] == 'O':
uf.union(j,dummy)
if board[m-1][j] == 'O':
uf.union(n * (m - 1) + j,dummy)
#方向数组d是上下左右搜索的常用手法
d = [[1,0],[0,1],[0,-1],[-1,0]]
for i in range(1,m-1):
for j in range(1,n-1):
if board[i][j] == 'O':
for k in range(4):
#将此O与上下左右的O连通
x = i + d[k][0]
y = j + d[k][1]
if board[x][y] == 'O':
uf.union(x * n + y, i * n + j)
#所有不和dummy连通的O都替换
for i in range(1,m-1):
for j in range(1,n-1):
if not uf.connected(dummy,i * n + j):
board[i][j] = 'X'
等式方程的可满足性,T990
将equations中的算式根据和!=分为两部分,先处理,使他们连通;再处理!=,检查是否破坏相等关系的连通性。
class Solution:
class UnionFind:
def __init__(self):
self.parent = list(range(26))
def find(self,x):
while x!= self.parent[x]:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self,x,y):
self.parent[self.find(x)] = self.find(y)
def equationsPossible(equations):
uf = Solution.UnionFind()
#建立连通
for eq in quations:
if eq[1] == '=':
x = ord(eq[0]) - ord("a")
y = ord(eq[3]) - ord("a")
uf.union(x,y)
for eq in equations:
if eq[1] == '!':
x = ord(eq[0]) - ord("a")
y = ord(eq[3]) - ord("a")
if uf.find(x) == uf.find(y):
return False
return True
Kruskal最小生成树算法
最小生成树主要有Prim算法和Kruskal算法。
树和图的区别:树不包含环,图可以包含环
图的生成树:在图中找一棵包含图中所有结点的树。生成树是含有图中所有顶点的无环连通子图。
最小生成树:所有可能的生成树中,权重和最小的那棵生成树。
可以用Union-Find算法保证最小生成树的合法性。
以图判树,T261
问题给你编号从0到n-1的n个结点,和一个无向边列表edges,请你判断输入的这些边组成的结构是否是一棵树。
那么什么情况下加入一条边会使得树变成图呢?对于添加的这条边,如果该边的两个节点本来就在同一连通分量里,那么添加这条边就会产生环;否则不会。
def validTree(n,edges):
uf = UF(n)
for edge in edges:
u = edge[0]
v = edge[1]
if uf.connected(u,v):
return False
uf.union(u,v)
return uf.count() == 1
最低成本联通所有城市,T1135
所谓最小生成树,就是图中若干边的集合(mst),满足:
- 包含图中所有结点
- 形成的结构是树结构(不存在环)
- 权重和最小 → 贪心思路
贪心思路:将所有边按权重从小到大排序,从权中最小的边开始遍历,如果这条边和mst中其它边不会形成环,则这条边是最小生成树的一部分,将它加入mst集合;否则,这条边不是最小生成树的一部分,不要把它加入mst集合。
def minimumCost(n,connections):
#城市编号为1,,,n
uf = UF(n+1)
connections.sort(key = lambda a: a[2])
mst = 0
for edge in connections:
u = edge[0]
v = edge[1]
weight = edge[2]
if uf.connected(u,v):
continue
mst += weight
uf.union(u,v)
#0没有被连通
return mst if uf.count() == 2 else -1
连接所有点的最小费用,T1584
给定points数组,表示平面上的点,连接两点的费用为他们的曼哈顿距离,返回连接所有点的最小费用
def minCostConnectPoints(points):
#首先要生成边和权重
n = len(points)
edges = []
for i in range(n):
xi = points[i][0]
yi = points[i][1]
for j in range(i+1,n):
xj = points[j][0]
yj = points[j][1]
edges.append([i,j,abs(xi-xj)+abs(yi-yj)])
#排序
edges.sort(key = lambda a : a[2])
#Kruskal算法
mst = 0
uf = UF(n)
for edge in edges:
u = edge[0]
v = edge[1]
weight = edge[2]
if uf.connected(u,v):
continue
mst += weight
uf.union(u,v)
return mst
Dijkstra最短路径算法
Dijkstra条件:加权有向图;没有负权重边
Dijkstra算法框架
Dijkstra可以理解成一个带dp数组或备忘录的BFS算法,伪码如下:
from queue import PriorityQueue
class State:
def __init__(self,distFromStart,id):
self.id = id
self.distFromStart = distFromStart
#返回from_到to的边的权重
def weight(from_,to)
#s的相邻节点
def adj(s)
#计算start到其它结点的最短距离
def dijkstra(start,graph):
#结点个数
V = len(graph)
#distTo[i]记录start到达i的最短路径权重
distTo = [float("inf") for i in range(V)]
distTo[start] = 0
#建立一个优先级队列,distFromStart小的排前面
pq = queue.PriorityQueue()
#从起点开始BFS
pq.put([0,start])
while not pq.empty():
curState = pq.pop()
curNodeID = curState[1]
curDistFromStart = curState[0]
if curDistFromStart > distTo[curNodeID]:
continue
for nextNodeID in adj(curNodeID):
distToNextNode = distTo[curNodeID] + weight(curNodeID,nextNodeID)
if distTo[nextNodeID] > distToNextNode:
distTo[nextNodeID] = distToNextNode
pq.put(distToNextNode,nextNodeID)
网络延迟时间,T743
#从k出发,传遍n个结点(从1开始),至少需要多久
def networkDelayTime(times,n,k):
#建图
graph = [[] for i in range(n+1)]
for from_,to,weight in times:
graph[from_].append([to,weight])
#k到其他节点的最短路径
distTo = dijkstra(k,graph)
#找到最长的最短路径
res = 0
for x in distTo:
if x == float("inf"):
return -1
res = max(res,x)
return res
class State:
def __init__(self,id,distFromStart):
self.id = id
self.distFromStart = distFromStart
def dijstra(start,graph):
distTo = [float("inf") for i in range(len(graph))]
distTo[start] = 0
pq = queue.PriorityQueue()
pq.put(State(start,0))
while not pq.empty():
curState = pq.pop()
curNodeID = curState.id
curDistFromStart = curState.distFromStart
if curDistFromStart > distTo[curNodeID]:
continue
for neighbor in graph[curNodeID]:
nextNodeID = neighbor[0]
distToNextNode = distTo[curNodeID] + neighbor[1]
if distToNextNode < distTo[nextNodeID]:
distTo[nextNodeID] = distTonextNode
pq.put(State(nextNodeID,distToNextNode))
return distTo
官方解法:
import heapq
def networkDelayTime(times,n,k):
#建图
graph = [[] for i in range(n)]
for from_,to,weight in times:
graph[from_-1].append([to-1,weight])
#dijkstra
dist = [float("inf")] * n
dist[k-1] = 0
#起点(权重,编号)
q = [(0,k-1)]
while q:
weight,id = heapq.heappop(q)
if dist[id] < weight:
continue
for y,time in graph[id]:
d = dist[id] + time
if d < dist[y]:
dist[y] = d
heapq.heappush(q,(d,y))
ans = max(dist)
return ans if ans < float('inf') else -1
最小体力消耗路径,T1631
从矩阵左上角(0,0)到右下角(rows-1,columns-1),每次可以上下左右动,找到耗费体力最小的路径。
体力耗费是路径上相邻各自之间高度差绝对值的最大值决定的。
class Solution:
def minimumEffortPath(self, heights: List[List[int]]) -> int:
m = len(heights)
n = len(heights[0])
dist = [0] + [float('inf')] * (m * n - 1)
q = [(0,0,0)]
seen = set()
while q:
weight,x,y = heapq.heappop(q)
if x * n + y in seen:
continue
if weight > dist[x * n + y]:
continue
if (x,y) == (m - 1,n - 1):
break
seen.add(x*n+y)
for nx,ny in [(x+1,y),(x-1,y),(x,y+1),(x,y-1)]:
if (0 <= nx < m) and (0 <= ny < n) and (max(weight,abs(heights[x][y] - heights[nx][ny])) <= dist[nx * n + ny]):
dist[nx * n + ny] = max(weight,abs(heights[x][y] - heights[nx][ny]))
heapq.heappush(q,(dist[nx*n+ny],nx,ny))
return dist[m * n - 1]
概率最大的路径,T1514
class Solution:
def maxProbability(self, n: int, edges: List[List[int]], succProb: List[float], start: int, end: int) -> float:
graph = collections.defaultdict(list)
for i, (x, y) in enumerate(edges):
graph[x].append((succProb[i], y))
graph[y].append((succProb[i], x))
que = [(-1.0, start)]
prob = [0.0] * n
prob[start] = 1.0
while que:
pr, node = heapq.heappop(que)
pr = -pr
if pr < prob[node]:
continue
for prNext, nodeNext in graph[node]:
if prob[nodeNext] < prob[node] * prNext:
prob[nodeNext] = prob[node] * prNext
heapq.heappush(que, (-prob[nodeNext], nodeNext))
return prob[end]