Python实现:图的遍历(BFS、DFS)、最小生成树(Kruskal、Prim)、最短路径(Dijkstra)

优先队列示例:数字小,排在前面

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=UET是一棵树(只包含一个顶点且没有边);
  • 检查所有一个端点在集合 U U U里而另一个端点在集合 V − U V-U VU的边,找出其中权最小的边 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=(UET)仍是一棵树;
  • 重复上面步骤直到 U = V U=V U=V(所构造的树已经包含了所有顶点),这时集合 E T ET ET里有 n − 1 n-1 n1条边,子图 T = ( U , E T ) T=(U,ET) T=(UET),就是 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

  1. 定义已经找到最短路径的顶点集合为 S S S,集合 U U U为其余顶点,即 U = V − S U=V-S U=VS:初始时, S S S只包含源点 s s s,即 S = { s } S=\{s\} S{s} U U U包含除 s s s 外的其他顶点;
  2. 定义 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
  3. 从集合 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}
  4. 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)},nodeU
  5. 重复步骤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[i1][j1]表示从 i i i j j j的、中间点序号不大于 k k k的最短路径长度 ( 1 ≤ k ≤ n ) (1\leq k\leq n) (1kn)。特别地, A 0 A_0 A0等于图 G G G的邻接矩阵, A n [ i − 1 ] [ j − 1 ] A_n[i-1][j-1] An[i1][j1]表示从 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复杂度比较高,一般不太常用。

https://www.bilibili.com/video/BV1ts41157Sy

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值