搜索与图论 来源acwing
1. 深度优先搜索
- 搜索的过程也是逻辑上的,回溯的过程并且要恢复现场(人脑想的话,就是标记,然后知道下一行执行什么),执行下一行代码的过程,一定要有一个整体上的认识
- DFS 空间 Oh
- BFS空间 O 2 h 2^h 2h 最优最短路的特性
- 两种搜索方法 搜索顺序并不一样
- 全排列例题说明
- DFS最重要的就是 考虑 顺序 到底按照怎样的顺序搜索所有的方案
- 全排列的搜索顺序就是从前往后一个一个去搜
N=10
path=[0]*N
st=[0]*N
def dfs(u):
if u==n:
for i in range(n):
print("%d"%path[i],end=" ")
return
for i in range(1,n+1):
if not st[i]:
path[u]=i
st[i]=1
dfs(u+1)
st[i]=0
def main():
n=int(input())
dfs(0)
main()
结合搜索树来理解递归中的 回溯和恢复现场这两个概念
- 皇后例题说明
- n皇后有很多搜索方式,明白搜索方式对我们理解代码很有帮助
- 第一种顺序和全排列的搜索顺序类似,全排列问题是从前往后搜索每一个数,皇后问题可以是从前往后搜索每一行,因为每一行只能放一个皇后 就是全排列问题 搜索顺序完全和全排列 的搜索方式一样
- 但是这种搜索顺序,要剪枝
- 皇后问题利用到了状态压缩,将主对角线和反对角线 (一条直线上的点具有什么共同点截距是相同的)因此可以用截距来表示
N=20
n=0
g=[[0]*N for i in range(N)]
col=[0]*N
dg=[0]*N
udg=[0]*N
def dfs(u):
if u==n:
for i in range(n):
print(g[i])
return
#枚举所有列
for i in range(n):
if not col[i] and not !dg[u+i] and not udg[n-u+i]:
g[u][i]="Q"
col[i]=dg[u+i]=udg[n-u+i]=1
#递归到下一=层
dfs(u+1)
col[i]=dg[u+i]=udg[n-u+i]=0
g[u][i]="."
def main():
n=int(input())
g=[]
for i in range(n):
g.append(list(input().split()))
dfs(0)
main()
- 还有一种更原始的方式 的搜索顺序 枚举每一个格子,前面的一种方法是经过分析提取出来压缩之后的方法,原始的搜索状态还是非常可以的
N=20
row=[0]*N
col=[0]*N
dg=[0]*N
udg=[0]*N
# x,y表示二维坐标 s表示当前几个皇后
def dfs(x,y,s):
if y==n:
y=0
x+=1
if x==n:
if s==n:
for i in range(n):
print(g[i])
return
#不放皇后
dfs(x,y+1,s)
#放皇后 既然要放皇后的话,就要判断当前这个位置能否放皇后
if not row[x] and not col[y] and not dg[x+y] and not udg[x-y+n]:
g[x][y]="Q"
row[x]=col[y]=dg[x+y]=udg[x-y+n]=1
dfs(x,y+1,s+1)
row[x]=col[y]=dg[x+y]=udg[x-y+n]=0
g[x][y]="."
def main():
n=int(input())
g=[]
for i in range(n):
g.append(list(input().split())
dfs(0,0,0)
main()
2.宽度优先搜索
宽搜的基本框架
- 一层一层去搜,当边的权重是一,具有最短路的特性
- 一层一层去搜索,类似于雷达一样一圈一圈往外走,说一具备最短路的特性
N=110
n,m=0,0
#初始化距离数组
d=[[0]*N for _ in range(N)]
#手写一个队列,队列里面存储的是点的坐标
q=[0]*(N*N)
def bfs():
#一般的话,tt都是从-1开始
#但是这里的话,由于插入了一个数,
#所以tt从0开始
hh=0,tt=0
q[0]=[0,0]
global d
d=[[-1]*N for _ in range(N)]
#初始化(0,0)点的距离为0
d[0][0]=0
dx=[-1,0,1,0]
dy=[0,1,0,-1]
#队列非空
while hh<=tt:
#取出队首元素
t=q[hh]
hh+=1
for i in range(4):
x=t[0]+dx[i]
y=t[1]+dy[i]
if x>=0 and x<n and y>=0 and y<m and g[x][y]==0 and d[x][y]==-1:
#设置d[x][y]=-1是为了寻找最短路,如果不等于-1,表示已经走过,不是最短路
d[x][y]=d[t[0]][t[1]]+1
tt+=1
q[tt]=[x,y]
return d[n-1][m-1]
def main():
n,m=map(int,input().split())
g=[]
for i in range(n):
g.append(input().split())
print(bfs())
main()
**如果想要增加输出路径这个过程,很简单,开一个数组就好了,新增一个prev二维数组,表示每个点由前面哪一个点到达的,所以这个二维数组 里面的值也是一个点的坐标形式
- 对于迷宫问题,要增加路径输出,因为迷宫问题的路径是二维的形式,所以需要开一个二维数组,然后记录路径
prev=[[0]*N for _ in range(N)]
#记录当前这个(x,y)是有上一个点转移过来
prev[x][y]=t
x,y=n-1,m-1
while x || y:
print(x,y)
#得到上一个点的信息
t=prev[x][y]
x,y=t[0],t[1]
#如果 想要顺序输出路径的话,python实现就比较简单
way=[]
while x||y:
way.append((x,y))
t=pre[x][y]
x,y=t[0],t[1]
way.reverse()
for i in range way:
print(i,end=" ")
3.树与图的存储
图的存储方式:
N=100010,M=n*2
h=[0]*N
e=[0]*M
ne=[0]*M
idx=0
st=[0]*N
# 终于搞清楚了邻接表的存储方式
# idx 只是表述这些数组的下标罢了
#e[idx] 获取当前数组下标的节点编号(在图中对应)
#ne[idx] #获取下一节点 再数组的编号
def add(a,b):
#修改了idx的值
#所以要声名全局变量
global idx
e[idx]=b
ne[idx]=h[a]
h[a]=idx
idx+=1
def dfs(u):
st[u]=1 #标记一下,表示已经被搜过了
# 获取邻接点的数组下标
i=h[u]
while i!=-1:
j=e[i]
if not st[j]:
dfs(j)
#注意这里是 ne[i]
i=ne[i]
#输出从节点开始的所有 路径
def dfs(u):
st[u]=1 #标记一下,表示已经被搜过
path.append(u) #将当前节点添加到路径中
end_node=True #用于检测u是否为叶子节点
i=h[u]
while i!=-1:
j=e[j]
if not st[j]:
dfs(j,path.copy()) #如果没有被访问,递归调用dfs
end_node=False
i=ne[i]
if end_node: #如果u是叶子节点,打印路径
print("->".join((map(str,path))))
#宽搜输出路径--队列实现
#队列实现的框架
#初始化队列 队列添加起点和路径
#进行循环判断 只要当前队列不空,取出队首元素,检查其所有邻接点
#对于每个临界点,如果他还没有被访问,则将它和他的路径添加到队列的末尾
#如果一个节点是叶子节点,打印它的路径
from collections import deque
def bfs(u):
queue=deque([u,[u]])
st[u]=1
while queue:
node,path=queue.popleft() #相当于队首出队
is_leaf=True
i=h[node]
while i!=-1:
j=e[i]
if not st[j]:
st[j]=1
queue.append(j,path+[j])
is_leaf=False
i=ne[i]
if is_leaf:
print("->".join(map(str,path)))
一定要注意,idx表示当前用到了哪个节点,比如说7–3,6–3都要进行开辟相应的空间 为3,6 所以 e和ne这两个数组的大小应该和边的大小保持一致 这种存储方式就是在单链表的存储方式 进行扩展的
-
用深度优选搜索可以实现输出路径,用宽搜如何实现输出路径,宽搜的话,就比较复杂一些,但是也是
-
上一道题目搜索的是图中的编号,而不是idx,idx在这道题目 表示边了
-
这个题目无论从哪个点开始搜索,都是没有问题的,因为这是一个无向图,从哪一个边开始都是ok的
图邻接表 新建节点 一般都是在头节点插入的
一道例题说明:对于这道题目而言,我们只需要对于每一个点,删除之后,求出连通块中点的最大值,然后在这些最大值中取一个较小的,递归的话,就是要知道题目的整体流程就好,整体框架是怎么样的
- 那么如何快速的求出,就需要使用到深度优先遍历了
N=100010,M=N*2
h=[0]*N
e=[0]*M
ne=[0]*M
idx=0
st=[0]*N
ans=N
def add(a,b):
e[idx]=b
ne[idx]=h[a]
h[a]=idx
idx+=1
def dfs(u):
#以u为根的子树中点的数量
st[u]=1
sums,res=1,0
i=h[u]
while i!=-1:
#虽然是不同的idx,但是里面存储的值可能相同,都是节点编号,途中节点编号
j=e[i]
if not st[j]:
s=dfs(j)
res=max(res,s)
sum+=s
#求到最大值之后,再与之前的作比较
res=max(res,n-sum)
ans=min(ans,res)
return sums
4.树与图的深度优先遍历
如何完整的将这样的深度优先遍历的路径输出出来,用邻接表的形式 自己好像不太会 ,宽搜与深搜的路径已经实现
5.树与图的宽度优先遍历
宽度优先遍历的题目 刚刚讲了迷宫问题,之前哪个迷宫问题是二维数组的形式,这个是邻接表的形式,二者还是有细微的差别
N=100010
n,m=0,0
h=[0]*N
e=[0]*N
ne=[0]*N
#上一个题目 e和ne不是存的N而是M,该怎么做
idx=0
d=[0]*N
q=[0]*N
def add(a,b):
e[idx]=b
ne[idx]=h[a]
h[a]=idx
idx+=1
def bfs():
hh,tt=0,0
#初始化队列,表示图中编号为1的店
q[0]=1
d=[-1]*N
d[1]=0
while hh<=tt:
t=q[hh]
hh+=1
#这里扩展队头元素不一样,之前是四个方向
#现在是指针的形式
i=h[t]
while i!=-1:
j=e[i]
if d[j]==-1:
d[j]=d[t]+1
tt+=1
q[tt]=j
return d[n]
def main():
n,m=map(int,input().split())
global h
h=[-1]*N
for i in range(m):
a,b=map(int,input().split())
add(a,b)
print(bfs())
main()
6.拓扑排序
宽搜的一个经典应用 拓扑排序
什么叫做有向图的拓扑排序?:就是所有边都是从前往后的
- 可以证明一个有向无环图一定存在拓扑排序
- 要根据出度入度来做这道题
- 入度为0表示当前没有任何点指向在我的前面,所以这个点可以放在队列里面
N=100010
n,m=0,0
h=[0]*N
e=[0]*N
ne=[0]*N
idx=0
q=[0]*N
d=[0]*N
def add(a,b):
e[idx]=b
ne[idx]=h[a]
h[a]=idx
idx+=1
def topsort():
hh,tt=0,-1
for i in range(1,n+1):
if not d[i]:
tt+=1
q[tt]=i
while hh<=tt:
t=q[hh]
hh+=1
i=h[t]
while i!=-1:
j=e[i]
d[j]-=1
if d[j]==0:
tt+=1
q[tt]=j
#判断所有的点是否 都入队了
return tt=n-1
def main():
n,m=map(int,input().split())
h=[-1]*N
for i in range(m):
a,b=map(int,input().split())
add(a,b)
#入度数组,数组下标就表示图中节点编号
d[b]+=1
if topsort():
#此时队列里面的顺序恰好就是我们的拓扑序
#因为队列里面只是指针发生了变化
for i in range(n):
print("%d"%q[i])
else:
print("-1")
main()
这个有向图的拓扑排序并不是唯一的,输出任何一个顺序都是可以的
7.最短路问题
总体框架
- 朴素版本Dikstra算法时间复杂度和我们的边数是没有关系,只与节点个数有关,所以Dijkstra这个算法比较适合稠密图
- 如果是稀疏图,m和n是一个级别的都是10的五次方,就要使用堆优化的Dijkstra算法
- 稠密图:m和n方是一个级别
- 稀疏图:m和n是一个级别
- 朴素Dikstra基本算法流程(dist数组表示点到起点的举例)
N = 510
inf = int(1e9) # 用一个较大的数字表示无穷大
n, m = 0, 0
g = [[0] * N for _ in range(N)]
dist = [0] * N
st = [0] * N
def dijkstra():
global dist
dist = [inf] * N
dist[1] = 0 # 将第1个点到起点的距离设为0
for i in range(n):
t = -1
# 在未确定最短路径的点中,找到目前距离最近的点
for j in range(1, n+1):
if not st[j] and (t == -1 or dist[t] > dist[j]):
t = j
if t == n:
break
st[t] = 1
# 更新与t点相邻的其他点的距离
for j in range(1, n+1):
dist[j] = min(dist[j], dist[t] + g[t][j])
if dist[n] == inf:
return -1
return dist[n]
def main():
global n, m, g
n, m = map(int, input().split())
g = [[inf] * N for _ in range(N)]
for _ in range(m):
a, b, c = map(int, input().split())
g[a][b] = min(g[a][b], c)
t = dijkstra()
print("%d" % (t))
main()
- 堆优化的dijkstra
- 可以手写堆,时时刻刻保持堆里面有n个数
- 也可以调用函数,但是这个不支持修改元素,那怎么进行相关操作呢?每一次修改就往堆里面插入一个新的数,会导致冗余问题,导致堆里面可能会有m个数
- 比如说1号点,存了一个距离10,又存了一个距离15,由于有冗余的存在,当前找到的最小值,有可能之前确定过的,用st数组判断一下,如果之前确定过了,就continue
- 还是有点抽象,还是要转换成逻辑上面的思考才行,符合正常思维才行
from heapq import heapify, heappush, heappop
N = 100010
inf = int(1e9) # 定义无穷大的值
# 邻接表初始化
h = [-1] * N # 每个点的头指针,初始化为-1表示没有邻接的点
w = [0] * N # 每条边的权重
e = [0] * N # 每条边连接到的点
ne = [0] * N # 下一条边的索引
idx = 0 # 当前边的索引
dist = [0] * N # 存储每个点到起点的最短距离
st = [0] * N # 标记数组,用来记录每个点是否已经确定了最短距离
def add(a, b, c):
global idx
e[idx] = b # 当前边连接到的点是b
w[idx] = c # 当前边的权重是c
ne[idx] = h[a] # 当前边的下一条边是之前a点的头指针指向的边
h[a] = idx # 更新a点的头指针为当前边
idx += 1 # 索引加1,移动到下一条边
def dijkstra():
global dist
dist = [inf] * N # 初始化所有点到起点的距离为无穷大
dist[1] = 0 # 起点到自己的距离为0
heap = []
heapify(heap)
heappush(heap, [0, 1]) # 将起点放入堆中,距离为0
while len(heap): # 当堆不为空时,继续处理
t = heappop(heap)
ver, distance = t[1], t[0] # 从堆中取出一个距离最近的点和它到起点的距离
if st[ver]: # 如果这个点的最短距离已经确定,就跳过
continue
st[ver] = 1 # 标记这个点的最短距离已经确定
i = h[ver]
while i != -1: # 遍历这个点的所有边
j = e[i]
# 如果通过这个点到j点的距离更短,就更新j点的距离
if dist[j] > distance + w[i]:
dist[j] = distance + w[i]
heappush(heap, [dist[j], j]) # 将j点放入堆中
i = ne[i]
if dist[n] == inf: # 如果终点的距离是无穷大,说明没有到达终点的路径
return -1
return dist[n]
这里的w[i]还不是很厉理解,还是要手动模拟一边才可以
w【i】到底表示那个边长,以及权重
根据图像举一个例子,更加深刻理解才行
- 当然这个dijkstra算法不仅仅是只能求取1号点到n号点的最短距离,还可以求取2号点到其他任意号点的距离
- 只需要稍微修改一下代码就可以实现这种功
#如果要求二号点到n号点的最短路径长度
#初始化距离的时候,应该是第二个点设置为0
def dijkstra():
global dist
dist = [inf] * N
dist[2] = 0 # 将第2个点到起点的距离设为0
for i in range(n):
t = -1
# 在未确定最短路径的点中,找到目前距离最近的点
for j in range(1, n+1):
if not st[j] and (t == -1 or dist[t] > dist[j]):
t = j
if t == n:
break
st[t] = 1
# 更新与t点相邻的其他点的距离
for j in range(1, n+1):
dist[j] = min(dist[j], dist[t] + g[t][j])
if dist[n] == inf:
return -1
return dist[n]
#如果要求2号点到n-1个点的最短距离
#最终返回距离return dist[n-1]
#还是迭代n次,一步一步局部最优,到n-1最优,才能到n最优
#如果要记录最短的路径的
N = 510
inf = int(1e9) # 用一个较大的数字表示无穷大
n, m = 0, 0
g = [[0] * N for _ in range(N)]
dist = [0] * N
st = [0] * N
pre = [-1] * N # 增加一个pre数组
def dijkstra():
global dist
dist = [inf] * N
dist[1] = 0
for i in range(n):
t = -1
for j in range(1, n+1):
if not st[j] and (t == -1 or dist[t] > dist[j]):
t = j
if t == n:
break
st[t] = 1
for j in range(1, n+1):
if dist[j] > dist[t] + g[t][j]:
dist[j] = dist[t] + g[t][j]
pre[j] = t # 更新前驱
if dist[n] == inf:
return -1
return dist[n]
def get_path():
path = []
v = n
while v != -1:
path.append(v)
v = pre[v]
return path[::-1] # 反转,使其从起点开始
def main():
global n, m, g
n, m = map(int, input().split())
g = [[inf] * N for _ in range(N)]
for _ in range(m):
a, b, c = map(int, input().split())
g[a][b] = min(g[a][b], c)
t = dijkstra()
path = get_path()
print("最短路径长度: %d" % (t))
print("路径:", ' -> '.join(map(str, path)))
main()
- Bellman-Ford算法
-
这个算法可以不用邻接矩阵,也可以不用邻接表,直接利用结构体来存储边
-
算法流程如下
- 如果迭代了n次,第n次还有更新的话,说明存在边数为n的最短路径,那么就有n+1个点,根据抽屉原理,只有n个点,那么存在环
- 第一层的循环是有实际意义的,迭代k次,最多不经过k条边
- bellman-ford算法可以用来找负环
- 时间复杂度O(n*m)两重循环,非常傻算法
- 存在负权回路最短路径不一定存在,如果限制了经过边的条数,那么存在负环也无所谓了
N=510,M=10010
dist=[0]*N
n,m,k=0,0,0
#备份数组
backup=[0]*N
inf=int(1e20)
def bellman_ford():
global dist,backup
dist=[inf]*N
dist[1]=0
for i in range(k):
backup=dist[:] #进行深拷贝,备份数组
#枚举每一条边
for j in range(m):
a=edges[j][0]
b=edges[j][1]
w=edges[j][2]
#用备份数组进行更新,避免出现串联现象
dist[b]=min(dist[b],backup[a]+w)
#w表示的是b到a边,就是和之前是一样的
if dist[n]>(inf//2):
return -1
return dist[n]
def main():
global n,m,k
n,m,k=map(int,input().split())
#存储边的列表
edges=[]
for i in range(m):
edges.append(list(map(int,input().split())))
t=bellman_ford()
if t==-1:
print("impossible")
else:
print("%d"%t)
避免出现串联现象
- 负权边的一些注意事项
def bellman_ford():
global dist
dist = [inf] * N
dist[1] = 0
#这个i确实是有实际意义的,迭代k次,表示从起点经过不超过k条边 走到每个点的最短距离
#由这个实际意义可以得出,当迭代n次的时候,表示从起点经过不超过n条边 走到每个点的最短距离,如果n次松弛成功了,说明存在一个长度为n的边,那么就存在负权边,要不然不会更新成功
for i in range(n-1): # 进行n-1次松弛
backup = dist.copy() # 深拷贝
# 枚举每一条边
for j in range(m):
a, b, w = edges[j]
dist[b] = min(dist[b], backup[a] + w)
# 检测负权环 如果还可以
for j in range(m):
a, b, w = edges[j]
if dist[b] > dist[a] + w: # 如果在n-1轮后仍然可以松弛某条边
return -2 # 存在负权环
if dist[n] > (inf // 2): # 检查是否无法到达终点
return -1
return dist[n]
- 但是我们一般不适用bellman-fold算法来寻找最短边,一般使用spfa算法来进行判断有无最短边
- 但是有些题目只能使用bellman-fold算法,有边数限制的题目
途中表示5号点和n号点都不能到达n号点,但是5号这个点能把n号点更新,这也是为什么对应代码中的内容
- 还存在python深拷贝浅拷贝的问题
a=[1,2,3]
b=a
b[0]=5
print(a)
print(b)
像这种的话,并没有创建一个新的空间,只是新建了一个引用一旦修改b中元素的值,也会影响a中的值,因为a,b都是指向同一个空间==
*如何避免这种问题,就是想要进行深拷贝操作
- 使用python中的copy模块
import copy
a = [1, 2, 3]
b = copy.copy(a)
- 使用列表的[:]切片,切片就是创建了新的空间
a = [1, 2, 3]
b = a[:]
- 使用list()函数
a = [1, 2, 3]
b = list(a)
- SPFA算法 对bellamn_ford算法的优化
bellman_ford算法很傻,傻在哪里,每次迭代的时候,遍历所有边来进行更新,但是每次迭代的时候 不一定真的会把 dist[b]变小
- spfa做的优化就是每次迭代的时候看一下,如果dist[b]想变小,那么一定是它的前驱dist[a]变小了,才有可能变小
- 基于这个思想,针对变小的前驱,更新它的所有出边,只有前驱变小了,后继才有可能变小,用宽搜来进行优化,队列里面存储的是变小的a
- 我更新过谁,我再拿谁来更新别人。一个点如果没有被更新的话,那么拿他来更新是没有效果的
题目描述
算法流程
代码如下:和堆优化dijkstra算法代码非常类似
N=10010
n,m=0,0
h=[0]*N
w=[0]*N
e=[0]*N
ne=[0]*N
idx=0
dist=[0]*N
st=[0]*N
inf=int(1e20)
q=[0]*N #数组模拟队列
def add(a,b,c):
e[idx]=b
w[idx]=c
ne[idx]=h[a]
h[a]=idx
idx+=1
def spfa():
global dist
dist=[inf]*N
dist[1]=0
hh=0,tt=0
q[0]=1
st[1]=1
while hh<=tt:
#取得队首元素
t=q[hh]
hh+=1
st[t]=0
i=h[t]
while i!=-1:
j=e[i]
if dist[j]>dist[t]+w[i]:
dist[j]=dist[t]+w[i]
if not st[j]:
tt+=1
q[tt]=j
st[j]=1
i=ne[i]
if dist[n]==inf:
return -1
return dist[n]
def main():
n.m=map(int,input().split())
global h
h=[-1]*N
while m:
a,b,c=map(int,input().split())
add(a,b,c)
t=spfa()
if t==-1:
print("impossible")
else:
print("%d"%t)
main()
模拟案例
- spfa算法求解负环
N=10010
n,m=0,0
h=[0]*N
w=[0]*N
e=[0]*N
ne=[0]*N
idx=0
dist=[0]*N
cnt=[0]*N
st=[0]*N
inf=int(1e20)
q=[0]*N #数组模拟队列
def add(a,b,c):
e[idx]=b
w[idx]=c
ne[idx]=h[a]
h[a]=idx
idx+=1
def spfa():
global dist
dist=[inf]*N
dist[1]=0
hh,tt=0,-1
for i in range(1,n+1):
st[i]=1
tt+=1
q[tt]=i
while hh<=tt:
#取得队首元素
t=q[hh]
hh+=1
st[t]=0
i=h[t]
while i!=-1:
j=e[i]
if dist[j]>dist[t]+w[i]:
dist[j]=dist[t]+w[i]
cnt[j]=cnt[t]+1
if cnt[j]>=n:
return True
if not st[j]:
tt+=1
q[tt]=j
st[j]=1
i=ne[i]
return False
def main():
n.m=map(int,input().split())
global h
h=[-1]*N
while m:
a,b,c=map(int,input().split())
add(a,b,c)
if spfa():
print("Yes")
else:
print("No")
main()
- spfa算法求解负环的原理是什么也是利用抽屉原理
#就是spfa除了开了一个dist数组之外还开了一个count数组用来做记录,如果某一过程cnt[x]>=n说明至少经过了n+1个点,说明存在负权回路
#这个是判断负环,并不是问是否从1开始的负环,所以最开始的时候,要把所有的点放入
- bellman-fold算法求解负环的原理:根据循环的实际意义来进行判断的
题目描述:
- floyed算法求最短路动态规划思想
题目描述
N=210
INF=int(1e9)
n,m,Q=0,0,0
d=[[0]*N for i in range(N)]
def floyed():
for k in range(1,k+1):
for i in range(1,n+1):
for j in range(1,n+1):
d[i][j]=min(d[i][j],d[i][k]+d[k][j])
def main():
n,m,q=map(int,input().split())
for i in range(1,n+1):
for j in range(1,n+1):
if i==k:
d[i][j]=0
else:
d[i][j]=INF
while m:
a,b,w=map(int,input().split())
d[a][b]=min(d[a][b],w)
m-=1
floyed()
while q:
a,b=map(int,input().split())
if d[a][b]>INF/2:
print("impossible")
else:
print("%d"%d[a][b])
算法思想 邻接矩阵来存储
算法流程
#感觉就像是暴力,枚举第一个点,将第一点作为中间点
#在枚举所有的起点,终点
#枚举第二个点,将第二个点作为中间点,
#在美剧所有的起点,终点
#三重循环
8.最小生成树
大纲如下
- 朴树版本Prim算法 这个算法和dijstra虽然非常类似,但是还是有一些微小的差异
用t更新其他点到集合的距离,就是看一下其他点有没有一条边可以连上集合内部,然后取距离最小的一天边
- 这个dist数组和dijkstra算法中的dist数组的含义并不太一样
N=510
dist=[0]*N
def prim():
global dist
dist=[0x3f]*N
res=0
for i in range(n):
t=-1
for j in range(1,n+1):
if not st[j] and t==-1||dist[t]>dist[j]:
t=j
if i&&dist[t]==INF:
return INF
#只要不是第一个点,最小生成树就要加上这个边
if i:
res+=dist[t]
#用t这个点来更新到集合的最小距离
for j in range(1,n+1):
dist[j]=min(dist[j],g[t][j])
st[t]=1
return res
def main():
n,m=map(int,input().split())
g=[[0x3f]*N for _ in range(N)]
while m:
#解决重边问题,保留最小边
a,b,c=map(int,input().split())
g[a][b]=g[b][a]=min(g[a][b],c)
m-=1
t=prim()
if t==INF:
print("impossible")
else:
print("%d",t)
- 与dijkstra算法的差别
- 首先dijkstra算法首先初始化了一个点的距离,而prim算法没有更新
- 还有一个差别就是dijstra算法是:找到一个不在集合中的距离原点最小的点,用这个点来更新它的出边到远点的据咯;而prim算法是找到一个不在集合中的距离集合最近的点,然后利用这个点来更新其他点到集合的距离
- kruskal算法 这个就相当于是并查集的简单应用
N=100010
n,m=0,0
p=[0]*N
def find(x):
if x!=p[x]:
return p[x]=find(p[x])
return p[x]
def main():
n,m=map(int,input().split())
edges=[]
for i in range(m):
temp=list(map(int,input().split()))
edges.append(temp)
edges.sort(key=x:lambda x[2])
for i in range(1,n+1):
p[i]=i
res,cnt=0,0
for i in range(m):
a=edges[i][0]
b=edges[i][1]
w=edges[i][2]
a=find(a)
b=find(b)
if a!=b:
p[a]=b
res+=w
cnt+=1
if cnt<n-1:
print("impossible")
else:
print("%d"%res)
main()
9.二分图
一个图是二分图当且仅当图中不含奇数环
- 使用染色法交替染色,假设第一个点染色1号,那么与他想通的点就染色2号,那么再与之相邻的点就染成1号,
N=100010
M=200010
n,m=0,0
h=[0]*N
e=[0]*M
ne=[0]*M
idx=0
color=[0]*N
def add(a,b):
e[idx]=b
ne[idx]=h[a]
h[a]=idx
idx+=1
def dfs(u,c):
color(u)=c
i=h[u]
#遍历一下当前这个点的所有邻点
while i!=-1:
#j来获取当前这个邻点的编号
j=e[i]
if not color[j]:
if not dfs(j,3-c):
return False
elif color[j]==c:
return False
#
i=ne[i]
return True
def main():
n,m=map(int,input().split())
global h
h=[-1]*N
while m:
a,b=map(int,input().split())
add(a,b)
add(b,a)
flag=True
for i in range(1,n+1):
if not color[i]:
if not flag(i,1):
flag=False
break
if flag:
print("Yes")
else:
print("No")
10.匈牙利算法
求解最大匹配数目 找到最多的匹配成功的边,就是不存在两条边公用了一个点的
- st表示对于男生而言,当前这些妹子都还没有考虑过
- match存储
- 终于搞清楚了邻接表存储的图的含义了 图中节点的编号和idx的含义是不一样的,有很大的差别的
N=510
M=100010
n1,n2,m=0,0,0
h=[0]*N
e=[0]*M
ne=[0]*M
idx=0
#match数组表示当前女生所匹配的男生
match=[0]*N
#表示当前男生考虑的
st=[0]*N
def add(a,b):
e[idx]=b
ne[idx]=h[a]
h[a]=idx
odx+=1
#写这个递归函数一定要有整体的思路
#find函数 是为男生x找到匹配的女生
def find(x):
#枚举x的所有邻接点 i对应的是idx
i=h[x]
while i!=-1:
j=e[j]
#当前这个女生男生还没有考虑过
if not st[j]:
st[j]=1
#当前这个女生单身
if match[j]==0 || find(match[j]):
match[j]=x
return True
i=ne[i]
return False
def main():
n1,n2,m=map(int,input().split())
h=[-1]*N
while(m):
a,b=map(int,input().split())
add(a,b)
res=0
for i in range(1,n1+1):
#对于每一个男生开始选的时候
#都要进行重置,表示没有选
st=[0]*N
if find(i):
res+=1
print("%d"%res)
- 这种递归之类的题目一定要想清楚整体框架,整体框架想好了,递归就很好实现了
- 一定有思路框架,对于每一个图论问题,要想好思路框架,再落实到代码上