优先队列示例:数字小,排在前面
from heapq import *
pqueue=[]
heappush(pqueue,(1,"A"))
heappush(pqueue,(7,"B"))
heappush(pqueue,(3,"C"))
heappush(pqueue,(6,"D"))
heappush(pqueue,(2,"E"))
print(pqueue)#堆
heappop(pqueue)
输出:
[(1, 'A'), (2, 'E'), (3, 'C'), (7, 'B'), (6, 'D')]
(1, 'A')
Python堆队列算法——heapq
Python优先队列——queue.PriorityQueue
1.图的遍历
1.1 广度优先搜索遍历BFS——队列
#s:开始结点
def BFS(graph,s):
queue=[]
queue.append(s)
visit=set()
bfs=[]
while len(queue)>0:
vertex=queue.pop(0)
bfs.append(vertex)
neighbor=graph[vertex]
for node in neighbor:
if node not in visit:
queue.append(node)
visit.add(node)
return bfs
graph={
"A":["B","C"],
"B":["A","C","D"],
"C":["A","B","D","E"],
"D":["B","C","E","F"],
"E":["C","D"],
"F":["D"]
}
bfs=BFS(graph,"E")
print(bfs)
["E","C","D","A","B","F"]
1.2 深度优先搜索遍历DFS——栈
#s:第一个开始的结点
def DFS(graph,s):
stack=[]
stack.append(s)
visit=set()
dfs=[]
while len(stack)>0:
vertex=stack.pop()
dfs.append(vertex)
neighbor=graph[vertex]
for node in neighbor:
if node not in visit:
stack.append(node)
visit.add(node)
return dfs
graph={
"A":["B","C"],
"B":["A","C","D"],
"C":["A","B","D","E"],
"D":["B","C","E","F"],
"E":["C","D"],
"F":["D"]
}
dfs=DFS(graph,"A")
print(dfs)
["A","C","E","D","F","B"]
2.生成树
- 连通图 G G G的一个子图如果是一棵包含 G G G的所有顶点的树,则该子图称为 G G G的生成树;
- 图的生成树不是唯一的,从不同的顶点出发进行遍历,可以得到不同的生成树;
- 对于带权连通图 G = ( V , E ) G=(V,E) G=(V,E),把生成树各边的权值总和称为生成树的权,并把权最小的生成树称为 G G G的最小生成树。
2.1 BFS生成树
#s:第一个开始的结点
def BFS(graph,s):
queue=[]
queue.append(s)
visit=set()
visit.add(s)
bfs=[]
parent={s:None}
while len(queue)>0:
vertex=queue.pop(0)
bfs.append(vertex)
neighbor=graph[vertex]
for node in neighbor:
if node not in visit:
queue.append(node)
visit.add(node)
parent[node]=vertex
return bfs,parent
graph={
"A":["B","C"],
"B":["A","C","D"],
"C":["A","B","D","E"],
"D":["B","C","E","F"],
"E":["C","D"],
"F":["D"]
}
bfs.parent=BFS(graph,"E")
print(parent)
v="B"
while v!=None:
print(v)
v=parent[v]
{"E":None,"C":"E","D":"E","A":"C","B":"C","F":"D"}
B
C
E
2.2 最小生成树(Kruskal算法&Prim算法)
MST性质:设 G = ( V , E ) G=(V,E) G=(V,E)是一个连通网络, U U U是顶点集 V V V的一个真子集。若 ( u , v ) (u,v) (u,v)是 G G G中所有的一个端点在 U U U、另一个端点不在 U U U里的边中,具有最小权值的一条边,则一定存在 G G G的一颗最小生成树包括边 ( u , v ) (u,v) (u,v)。
Kruskal算法
设 G = ( V , E ) G=(V,E) G=(V,E)是一个网络,其中 ∣ V ∣ = n |V|=n ∣V∣=n。Kruskal算法构造最小生成树的过程是:
- 初始时取包含 G G G中所有 n n n个顶点但没有任何边的孤立点子图 T = ( V , ∅ ) T=(V,\emptyset) T=(V,∅),T里的每个顶点自成一个连通分量;
- 将边集 E E E中的边按权值递增的顺序排序,顺序地检查这个边序列:若边 ( u , v ) (u,v) (u,v)的两个端点分别是当前 T T T的两个不同连通分量中的顶点,则将该边加入到 T T T中;若两端点是同一连通分量中的顶点,则舍去此边;
- 依次类推,直到 T T T中所有顶点都在同一连通分量上为止, T T T便是 G G G的一棵最小生成树。
- 可以先用一个优先队列存储所有的边,这样可保证每次取到的都是最短边。
- 为每个连通分量确定一个代表元,如果两个顶点的代表元相同,它们就属于同一连通分量。
- 加入一条边减少了连通分量,这时需要选一个顶点,让被合并的两个连通分量里的顶点都以它为代表元。完成这件事的简单方法是从原来的两个代表元中任选一个,而后更新另一连通分量中顶点的代表元。
from heapq import *
def Kruskal(graph):
#初始化代表元为本身
#初始化生成树
#Trep为T中的连通分量代表元
rep,T,Trep={},{},{}
for key in graph.keys():
rep[key]=key
T[key]={}
#存储所有边
edges=[]
for key,values in graph.items():
for k,v in values.items():
if (v,key,k) not in edges and (v,k,key) not in edges:
heappush(edges,(v,key,k))
while edges:
v,key,k=heappop(edges)
if rep[key]!=rep[k]:
if rep[key] not in Trep and rep[k] not in Trep:
Trep[key]=[key,k]
rep[k]=key
elif rep[key] in Trep and rep[k] in Trep:
d=Trep.pop(rep[key])
for i in d:
rep[i]=rep[k]
Trep[rep[key]].append(i)
#Trep[rep[key]]=Trep[rep[key]]+Trep[rep[k]]
elif rep[key] not in Trep and rep[k] in Trep:
Trep[rep[k]].append(key)
rep[key]=rep[k]
elif rep[k] not in Trep and rep[key] in Trep:
Trep[rep[key]].append(k)
rep[k]=rep[key]
T[key][k]=v
T[k][key]=v
return T
graph={
"a":{"b":5,"c":11,"d":5},
"b":{"a":5,"d":3,"e":9,"g":7},
"c":{"a":11,"d":7,"f":6},
"d":{"a":5,"b":3,"c":7,"g":20},
"e":{"b":9,"g":8},
"f":{"c":6,"g":8},
"g":{"b":7,"d":20,"e":8,"f":8}
}
T=Kruskal(graph)
print(T)
输出:{'a': {'b': 5}, 'b': {'d': 3, 'a': 5, 'g': 7}, 'c': {'f': 6, 'd': 7}, 'd': {'b': 3, 'c': 7}, 'e': {'g': 8}, 'f': {'c': 6}, 'g': {'b': 7, 'e': 8}}
Prim算法
- 从图 G G G的顶点集 V V V中任取一顶点(例如顶点 v 0 v_0 v0)放人集合 U U U中,这时 U = { v 0 } U=\{v0\} U={v0},令边集合 E T = { } , ET=\{\}, ET={},显然 T = ( U , E T ) T=(U,ET) T=(U,ET)是一棵树(只包含一个顶点且没有边);
- 检查所有一个端点在集合 U U U里而另一个端点在集合 V − U V-U V−U的边,找出其中权最小的边 e = ( u , v ) e=(u,v) e=(u,v),将顶点 v v v加人顶点集合 U U U,并将 e e e加人边集合 E T ET ET。易见,扩充之后的 T = ( U , E T ) T=(U,ET) T=(U,ET)仍是一棵树;
- 重复上面步骤直到 U = V U=V U=V(所构造的树已经包含了所有顶点),这时集合 E T ET ET里有 n − 1 n-1 n−1条边,子图 T = ( U , E T ) T=(U,ET) T=(U,ET),就是 G G G的一棵最小生成树。
def Prim(graph):
T={}
inT=[list(graph.keys())[0]]
T[inT[0]]={}
while len(inT)<len(graph):
minw=float('inf')
for v in inT:
for vn in graph[v]:
if vn not in inT:
if graph[v][vn]<minw:
minV1,minV2=v,vn
minw=graph[v][vn]
inT.append(minV2)
T[minV1][minV2]=minw
T[minV2]={}
T[minV2][minV1]=minw
return T
graph={
"a":{"b":5,"c":11,"d":5},
"b":{"a":5,"d":3,"e":9,"g":7},
"c":{"a":11,"d":7,"f":6},
"d":{"a":5,"b":3,"c":7,"g":20},
"e":{"b":9,"g":8},
"f":{"c":6,"g":8},
"g":{"b":7,"d":20,"e":8,"f":8}
}
T=Prim(graph)
print(T)
输出:{'a': {'b': 5}, 'b': {'a': 5, 'd': 3, 'g': 7}, 'd': {'b': 3, 'c': 7}, 'g': {'b': 7, 'e': 8}, 'c': {'d': 7, 'f': 6}, 'f': {'c': 6}, 'e': {'g': 8}}
3. 最短路径
3.1 单源最短路径
Dijkstra算法
单源最短路径问题:对于给定的有向网络 G = ( V , E ) G=(V,E) G=(V,E)及单个源点 s s s,求 s s s到 G G G的其余各顶点的最短路径。
Dijkstra算法的底层逻辑是贪心,也可以理解成贪心算法在图论当中的使用。设图 G G G顶点集为 V V V,权重表示为 w e i g h t weight weight,
- 定义已经找到最短路径的顶点集合为 S S S,集合 U U U为其余顶点,即 U = V − S U=V-S U=V−S:初始时, S S S只包含源点 s s s,即 S = { s } S=\{s\} S={s}, U U U包含除 s s s 外的其他顶点;
- 定义 d i s dis dis 保存源点到各个顶点的当前最短距离,初始时,点 s s s 的最短距离为0,其他点的最短距离为 ∞ \infty ∞,即 d i s [ v ] = { 0 , v = s + ∞ , v ≠ s dis[v]=\begin{cases}0,\qquad v=s\\+\infty,\quad v\neq s\end{cases} dis[v]={0,v=s+∞,v=s
- 从集合 U U U中选取一个距离 s s s 最小的顶点 k k k,则此时的 w e i g h t ( s , k ) weight(s,k) weight(s,k) 就是源点 s s s 到 k k k 的最短距离,把 k k k 加入到 S S S中,即 d i s [ k ] = W [ s ] [ k ] , S = S ⋃ { k } dis[k]=W[s][k],S=S\bigcup\{k\} dis[k]=W[s][k],S=S⋃{k};
- 以
k
k
k 为中间点,修改源点
s
s
s到
U
U
U中各顶点的距离:通过
k
k
k 到达其他点的路径长度是否之前路径短
d i s [ n o d e ] = m i n { d i s [ n o d e ] , d i s [ k ] + w e i g h t ( k , n o d e ) } , n o d e ∈ U dis[node]=min\{dis[node],dis[k]+weight(k,node)\},\qquad node\in U dis[node]=min{dis[node],dis[k]+weight(k,node)},node∈U - 重复步骤3、4直到所有顶点都包含在S中。
- 使用前提:每条边的权重不能是负数;
- 时间复杂度: O ( V 2 + E ) O(V^2+E) O(V2+E),其中 V V V为顶点数量, E E E为边的数量。
- 使用heap,时间复杂度 O ( V log V + E ) O(V\log V+E) O(VlogV+E)
Python实现
1. 字典存储图:
#s:第一个开始的结点
from heapq import heappush
from heapq import heappop
import math
def init_distance(graph,s):
dis={s:0}
for vertex in graph:
if vertex!=s:
dis[vertex]=math.inf
return dis
def dijkstra(graph,s):
pqueue=[]
heappush(pqueue,(0,s))
dis=init_distance(graph,s)
parent={s:None}
visit=set()
while pqueue:
d,vertex=heappop(pqueue)
visit.add(vertex)
for node in graph[vertex].keys():
if node not in visit:
if d+graph[vertex][node]<dis[node]:
heappush(pqueue,(d+graph[vertex][node],node))
parent[node]=vertex
dis[node]=d+graph[vertex][node]
return parent,dis
调用:
graph={
"A":{"B":5,"C":1},
"B":{"A":5,"C":2,"D":1},
"C":{"A":1,"B":2,"D":4,"E":8},
"D":{"B":1,"C":4,"E":3,"F":6},
"E":{"C":8,"D":3},
"F":{"D":6}
}
parent,dis=dijkstra(graph,"A")
print(parent)
print(dis)
v="B"
while v!=None:
print(v)
v=parent[v]
输出:
{'A': None, 'B': 'C', 'C': 'A', 'D': 'B', 'E': 'D', 'F': 'D'}
{'A': 0, 'B': 3, 'C': 1, 'D': 4, 'E': 7, 'F': 10}
B
C
A
2. 二维矩阵存储图:
from heapq import heappush
from heapq import heappop
import math
def dijkstra(graph,s):
dis=[math.inf]*len(graph)
dis[s]=0
pqueue=[]
heappush(pqueue,(0,s))
parent={s:None}
visit=set()
while pqueue:
d,vertex=heappop(pqueue)
visit.add(vertex)
for node in range(len(graph)):
if graph[vertex][node]==0:
continue
else:
if node not in visit:
if d+graph[vertex][node]<dis[node]:
heappush(pqueue,(d+graph[vertex][node],node))
parent[node]=vertex
dis[node]=d+graph[vertex][node]
return parent,dis
graph=[[0,5,1,0,0,0],[5,0,2,1,0,0],[1,2,0,4,8,0],[0,1,4,0,3,6],[0,0,8,3,0,0],[0,0,0,6,0,0]]
parent,dis=dijkstra(graph,0)
print(parent)
print(dis)
3.2 所有顶点对之间的最短路径
以每个顶点作为源点,执行Dijkstra算法
Floyd算法
设有
n
n
n个顶点,定义
n
n
n个矩阵序列
A
0
,
A
1
,
.
.
.
,
A
n
A_0,A_1,...,A_n
A0,A1,...,An,其中
A
k
[
i
−
1
]
[
j
−
1
]
A_k[i-1][j-1]
Ak[i−1][j−1]表示从
i
i
i到
j
j
j的、中间点序号不大于
k
k
k的最短路径长度
(
1
≤
k
≤
n
)
(1\leq k\leq n)
(1≤k≤n)。特别地,
A
0
A_0
A0等于图
G
G
G的邻接矩阵,
A
n
[
i
−
1
]
[
j
−
1
]
A_n[i-1][j-1]
An[i−1][j−1]表示从
i
i
i到
j
j
j的的最短路径长度。Floyd算法的基本思想就是:从
A
0
A_0
A0开始,递推地生成矩阵序列
A
1
,
A
2
,
.
.
.
,
A
n
A_1,A_2,...,A_n
A1,A2,...,An。由
A
k
A_k
Ak到
A
k
+
1
A_{k+1}
Ak+1的递推公式为,
A
k
+
1
[
i
]
[
j
]
=
m
i
n
{
A
k
[
i
]
[
j
]
,
A
k
[
i
]
[
k
]
+
A
k
[
k
]
[
j
]
}
A_{k+1}[i][j]=min\{A_k[i][j],A_k[i][k]+A_k[k][j]\}
Ak+1[i][j]=min{Ak[i][j],Ak[i][k]+Ak[k][j]}
为了记录最短路径,还要定义路径矩阵 p a t h path path,第 k + 1 k+1 k+1次迭代中求得的 p a t h [ i ] [ j ] path[i][j] path[i][j]是从 i i i到 j j j的中间点序号不大于 k + 1 k+1 k+1的最短路径上顶点 i i i的后继顶点。
- floyd复杂度比较高,一般不太常用。