搜索与图论

搜索与图论 来源acwing

1. 深度优先搜索

  • 搜索的过程也是逻辑上的,回溯的过程并且要恢复现场(人脑想的话,就是标记,然后知道下一行执行什么),执行下一行代码的过程,一定要有一个整体上的认识
  • DFS 空间 Oh
  • BFS空间 O 2 h 2^h 2h 最优最短路的特性
  • 两种搜索方法 搜索顺序并不一样
  1. 全排列例题说明

全排列例题,理解搜索顺序
在这里插入图片描述

  • 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()

搜索树
在这里插入图片描述

结合搜索树来理解递归中的 回溯和恢复现场这两个概念

  1. 皇后例题说明
  • 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.宽度优先搜索

宽搜的基本框架

宽搜的基本框架
在这里插入图片描述

  • 一层一层去搜,当边的权重是一,具有最短路的特性

bfs例题
在这里插入图片描述

  • 一层一层去搜索,类似于雷达一样一圈一圈往外走,说一具备最短路的特性
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是一个级别
  1. 朴素Dikstra基本算法流程(dist数组表示点到起点的举例)

Dikstra算法流程
在这里插入图片描述

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()

  1. 堆优化的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()

权重w的理解方式
在这里插入图片描述

  1. Bellman-Ford算法
  • 这个算法可以不用邻接矩阵,也可以不用邻接表,直接利用结构体来存储边

  • 算法流程如下

belman 算法流程
在这里插入图片描述

  • 如果迭代了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)

bellman算法用备份数组原因
在这里插入图片描述

避免出现串联现象

  • 负权边的一些注意事项
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)
  1. SPFA算法 对bellamn_ford算法的优化

bellman_ford算法很傻,傻在哪里,每次迭代的时候,遍历所有边来进行更新,但是每次迭代的时候 不一定真的会把 dist[b]变小

  • spfa做的优化就是每次迭代的时候看一下,如果dist[b]想变小,那么一定是它的前驱dist[a]变小了,才有可能变小
  • 基于这个思想,针对变小的前驱,更新它的所有出边,只有前驱变小了,后继才有可能变小,用宽搜来进行优化,队列里面存储的是变小的a
  • 我更新过谁,我再拿谁来更新别人。一个点如果没有被更新的话,那么拿他来更新是没有效果的

题目描述

spfa题目描述
在这里插入图片描述

算法流程

spfa算法流程

在这里插入图片描述

代码如下:和堆优化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模拟案例
在这里插入图片描述

  1. 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算法求解负环的原理:根据循环的实际意义来进行判断的

题目描述:

spfa判断负环题目
在这里插入图片描述

  1. floyed算法求最短路动态规划思想

题目描述

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])
            
        
    
        

算法思想 邻接矩阵来存储

算法流程

floyed算法流程
在这里插入图片描述

#感觉就像是暴力,枚举第一个点,将第一点作为中间点
#在枚举所有的起点,终点

#枚举第二个点,将第二个点作为中间点,
#在美剧所有的起点,终点
#三重循环

8.最小生成树

大纲如下

大纲
在这里插入图片描述

  1. 朴树版本Prim算法 这个算法和dijstra虽然非常类似,但是还是有一些微小的差异

prim算法流程
在这里插入图片描述

用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算法是找到一个不在集合中的距离集合最近的点,然后利用这个点来更新其他点到集合的距离
  1. 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)
  • 这种递归之类的题目一定要想清楚整体框架,整体框架想好了,递归就很好实现了
  • 一定有思路框架,对于每一个图论问题,要想好思路框架,再落实到代码上
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值