数据结构-图

本文详细介绍了图的概念,包括无向图、有向图以及连通性。接着讨论了图的遍历方法,如深度优先遍历和广度优先遍历。接着深入探讨了最短路径问题,包括Dijkstra算法和Bellman-Ford算法,以及它们在处理负权边的区别。此外,还提到了SPFA算法作为负权边的优化。最后,简要概述了最小生成树问题,提到了Prim和Kruskal算法。
摘要由CSDN通过智能技术生成

图(Graph)

图:由顶点的有穷非空集合 V ( G ) V ( G ) V(G) 和顶点之间边的集合 E ( G ) E ( G ) E(G) 组成,通常表示为: G = ( V , E ) G = ( V , E ) G=(V,E),其中, G G G表示个图, V V V 是图 G G G 中顶点的集合, E E E 是图 G G G 中边的集合。若 V = v 1 , v 2 , . . . , v n V = { v_1 , v_2 , . . . , v_n } V=v1,v2,...,vn, 则用 ∣ V ∣ ∣ V ∣ V表示图 G G G 中顶点的个数,也称图 G G G 的阶,E = { ( u , v ) ∣ u ∈ V , v ∈ V } ,用 ∣ E ∣ ∣ E ∣ E表示图 G G G 中边的条数。

有向图:
在这里插入图片描述
无向图:
在这里插入图片描述
连通、连通图和连通分量:

强连通图、强连通分量:
在这里插入图片描述
生成树、生成森林:
在这里插入图片描述
顶点的度、入度和出度:
在这里插入图片描述
路径、路径长度和回路:
顶点 v p v_p vp到顶点 v q v_q vq 之间的一条路径是指顶点序列 v p , v i 1 , v i 2 , . . . , v i m v_p,v_{i_1},v_{i_2},...,v_{i_m} vp,vi1,vi2,...,vim,当然关联的边也可以理解为路径的构成要素。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路。若一个图有 n n n 个顶点,并且有大于 n − 1 n − 1 n1 条边,则此图一定有环。

1. 图的表示

1.1 邻接矩阵

在这里插入图片描述

n,m = map(int,input().split())
g = [[0]*(n+1) for i in range(n+1)]

for i in range(m):
	u,v = map(int,input().split())
	g[u][v] = 1
	
	# g[v][u] = 1 # 无向图加上
	
# 图的遍历
for i in range(1,n+1):
	for j in range(1,n+1):
		if i!=j and g[i][j]:
			print(i,j)

优点: 比较好写
缺点: 二维数组对顶点数目有限制( 1 0 3 10^3 103 左右),否则内存会超;对于稀疏图来说,数组利用率太低
空间耗费: O ( N 2 ) O(N^2) O(N2)

1.2 邻接表

当一个图为稀疏图时(边数相对顶点较少),使用邻接矩阵法显然要浪费大量的存储空间;
在这里插入图片描述

n,m = map(int,input().split())
g = [[] for i in range(n+1)]

for i in range(m):
	a,b = map(int,input().split())
	g[a].append(b)
	# g[b].append(a) # 无向图加上这句

for i in range(1,n+1):
	for j in g[i]:
		print(i,j)

利用邻接表储存图:
很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。

2. 图的遍历

图的遍历:我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次, 这一过程就叫做图的遍历(Traversing Graph)
对于图的遍历来,通常有两种遍历次序方案: 深度优先遍历广度优先遍历

2.1 深度优先遍历

这种搜索算法所遵循的搜索策略是尽可能“深”地搜索一个图。
它的基本思想如下:

  1. 首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点 w 1 w_1 w1,再访问与 w 1 w_1 w1 邻接且未被访问的任一顶点…
  2. 重复上述过程。
  3. 当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
    一般情况下,其递归形式的算法十分简洁,算法过程如下:
def dfs(graph,v):
    visit[v] = 1
    print(v,end=' ')
    for next_ in graph[v]:
        if not visit[next_]:
            dfs(graph,next_)
            
def TraverseGraph(graph,n):
	visit = [False]*(n+1)
	for i in range(1,n+1):
		if not visit[i]:
			dfs(graph,i)

2.2 广度优先遍历

广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。
以下是广度优先遍历的代码:

def bfs(graph,v0):
    print(v0,end=' ')
    from collections import deque
    que = deque()
    que.append(graph[v0])
    while que:
        edges = que.popleft()
        for v in edges:
            if not visit[v]:
                print(v,end=' ')
                que.append(graph[v])
                visit[v] = 1

3. 最短路问题

在网图和非网图中,最短路径的含义是不同的。由于非网图它没有边上的权值,所谓的最短路径,其实就是指两顶点之间经过的边数最少的路径;而对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点

3.1 单源最短路(Dijkstra)

即询问的是从单起点 S S S ,到其他点的最短路径。
基本步骤:
1. 对图 g g g 设置集合 S S S,存放已被访问的节点。
2. 每次从集合 V − S V-S VS(就是所有未被访问的节点)中选择与起点 s s s 距离最短的节点 u u u,访问并加入 S S S
3. 以 u u u 为中介点,优化起点 s s s 与所有从 u u u 能到达的顶点 v v v 之间的最短距离(这个操作称为松弛)。
4. 这样的操作执行 n n n 次,直到所有节点均被访问。(每次操作都能确定一个点的最短路径)

基础版:
时间复杂度: O ( E 2 ) O(E^2) O(E2)

def Dijkstra(start,end):
	# 维护一个由起点到其它所有点的距离和判断是否访问过的数组
	dist = [float('inf)]*(n+1)
	visited = [False]*(n+1)
	
	dist[start] = 0
	visited[stat] = True
	# 直到找到终点停止
	while not visited[end]:
		minv.midx = float('inf'),0
		# 找出离起点最近且没被访问过的点
		for i in range(n):
			if not visited[i] and dist[i]<minv:
				minv = dist[i]
				midx = i
		visited[midx] = True
		# 寻找目标点能到达的其它点 并更新为最短距离
		for next_,w in g[midx]:
			if dist[next_] > minv+w:
				dist[next_] = minv+w
	return dist

堆优化 Dijkstra:

  • 优化了循环中找最小 d i s t [ u ] dist[u] dist[u] 的过程:使用最小堆存储{ d i s t [ u ] , u dist[u],u dist[u],u} ,保证堆顶直接就是最小的 d i s t [ u ] dist[u] dist[u]
  • 优化了以 u u u 为中介点时,循环遍历所有节点的过程:采用邻接矩阵存储,只遍历实际存在的边
def dijkstra(s):
    # 单源最短路 无负边
    #维护一个由起点到其它点的距离和判断是否访问过的列表
    dist = [float('inf')]*(n+1)
    visited = [False]*(n+1)
    dist[s] = 0
    que = [(0,s)]
    while que:
        # 找出离起点最近且没被访问过的点
        curDis,curNode = heapq.heappop(que)
        if visited[curNode]:
            continue
        visited[curNode] = True

        for idx,w in g[curNode]:  # 遍历从curNode可以到达的边 都变为最短距离
            if dist[idx] > curDis+w:
                dist[idx] = curDis+w
                heapq.heappush(que, (dist[idx], idx))
    return dist

时间复杂度: O ( E l o g V ) O(ElogV) O(ElogV),最坏的情况下就是每条边( E E E )都成功松弛,加入最小堆( l o g V logV logV

Dijkstra 无法处理有负权边的情况:根源是如果存在负权边,那么就无法保证每次找出的 u u u d i s t [ u ] dist[u] dist[u]是起点到 u u u 的所有路径中最小的。

  • 无负权边的情况下,如果当次循环中 d i s t [ u ] dist[u] dist[u] 最小,就可以保证此时 d i s t [ u ] dist[u] dist[u] 就是全局最小,之后不可能找出使 d i s t [ u ] dist[u] dist[u] 更小的情况
    反证:如果当次循环 d i s t [ u ] dist[u] dist[u] 最小,但假设在之后的循环中 d i s t [ u ] dist[u] dist[u] 还能被优化,那么应该选择另一个顶点 u ′ u' u( d i s t [ u ′ ] > d i s t [ u ] dist[u'] > dist[u] dist[u]>dist[u]),之后松弛只可能使
    在这里插入图片描述
    与松弛条件 n e w d i s t [ u ] < d i s t [ u ] new_dist[u] < dist[u] newdist[u]<dist[u]矛盾,证毕。

  • 而有负权边的情况下,可能 g [ u ′ − > u ] < 0 g[u'->u] < 0 g[u>u]<0,所以此时 d i s t [ u ] dist[u] dist[u]不一定就是最终的最小情况
    在这里插入图片描述
    上图中设置 A A A 为源点

    • u = A , d i s [ A ] = 0 , 松弛 d i s t [ B ] = − 1 , d i s t [ C ] = 1 u = A,dis[A] = 0,松弛dist[B]= -1,dist[C]=1 u=A,dis[A]=0,松弛dist[B]=1,dist[C]=1
    • u = B , d i s [ B ] = − 1 , 此时 B 加入集合 S ,代表已被访问,并且松弛 d i s t [ C ] = − 6 u = B,dis[B] = -1,此时 B 加入集合 S ,代表已被访问,并且松弛dist[C]=-6 u=B,dis[B]=1,此时B加入集合S,代表已被访问,并且松弛dist[C]=6
    • u = C , 其他点都已经被访问 , 结束算法 u = C,其他点都已经被访问,结束算法 u=C,其他点都已经被访问,结束算法

    但实际上如果一开始从 A − > C − > B , d i s t [ B ] 会更小 A->C->B,dist[B]会更小 A>C>B,dist[B]会更小

3.2 单源最短路(负边,负环)

3.2.1 Bellman-ford

基本步骤:

  1. 假设图中含有 V V V 个顶点,且起点为 s s s,初始化 d i s t [ n ] dist[n] dist[n] 数组,其中 d i s t [ s ] = 0 dist[s]=0 dist[s]=0 ,其他顶点都为无穷大
  2. 以任意顺序松弛所有边,重复 V − 1 V-1 V1 轮。
    在这里插入图片描述
    在这里插入图片描述
    那又为什么不像 Dijkstra 算法那样,会被负权边影响?
    为 Dijkstra 算法中有一个控制访问的 v i s i s t e d [ n ] visisted[n] visisted[n]数组,保证每个节点只能被访问一次,不能被二次优化,但 Bellman-ford 不是,它更加通用,每条边、(每个节点)都可能被多次访问、被多次优化。
def bellman_ford(start):
    # 存放边[u,v,w] : u->v 的长度为 w
    edges = [
        [0,1,-3],
        [1,2,2],
        [2,3,3],
        [3,4,2],
        [0,4,5]
    ]

    # 单源最短路 解决负边问题
    dist = [float('inf')]*(n+1)
    dist[start] = 0
    # 循环 n-1 次
    for i in range(n-1):
        # 从起点最多通过 i+1 条边时的最短路径
        for f,t,w in edges:
            if dist[t]>dist[f]+w:
                dist[t] = dist[f]+w
    
    flag = False
    # 判断负权环
    for f,t,w in edges:
        if dist[t]>dist[f]+w:
            # 如果还存在边需要松弛则存在负权环
            flag = True
            break

    return dist

时间复杂度: O ( V E ) O(VE) O(VE) V − 1 V-1 V1 轮迭代所有边( E E E),适合稀疏图。

3.2.2 SPFA

SPFA 是对 Bellman-ford 的优化,因为在 Bellman-ford 算法中,每轮都需要对 所有 边进行松弛,很多边的遍历都是无效的(即不满足松弛条件)。

而满足松弛条件有一个前提:只有当某一个节点 u u u d i s t [ u ] dist[u] dist[u] 值在上一轮被改变了,那这一轮从 u u u 出发的边的终点 v v v d i s t [ v ] dist[v] dist[v] 值才有可能被改变。(涟漪)(引理:最短路径的子路径都是最短路径)

所以,SPFA 在 Bellman-ford 算法的基础上,引入了一个队列,如果在某轮迭代中,从 u u u 出发的边 u − > v u->v u>v满足松弛条件,那么就将 v v v 加入队列,下一次迭代中从 v v v 出发的边可能可以被松弛。

如何判断负环?

  1. 统计每个点入队的次数,如果某个点入队 n n n 次,则说明存在负环(每次入队,num[v]++ );
  2. 统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于 n n n ,则也说明存在负环(每次入队,num[v] = num[u] + 1 )
def SPFA(start,end,g):
    from collections import deque
    dist = [float('inf')]*(n+1) # 初始化距离
    visited = [False]*(n+1) # 初始化访问列表
    cnt = [0]*(n+1) # 记录起点到每点的最短距离包含点的个数
    
    que = deque([start]) # 存放待优化的点
	dist[start] = 0
    visited[start] = True
    cnt[start] = 1

    while que:
        curNode = que.popleft()
        visited[curNode] = False # 被弹出则不在访问列表中
        # 遍历 curNode 能到达的点
        for idx,w in g[curNode]:
            if dist[idx]>dist[curNode]+w:
                dist[idx] = dist[curNode]+w
                cnt[idx] = cnt[curNode]+1
				# 包含点的个数超过 n 说明存在负权环
                if cnt[idx]>n:
                    print("存在负权环")
                    return
                if not visited[idx]:
                    que.append(idx)
                    visited[idx] = True
    return dist[end]

时间复杂度 : O ( k E ) O(kE) O(kE) 其中 k k k 为常数。但如果图中有从源点可达的负环,复杂度会退化到 O ( V E ) O(VE) O(VE) ,适合稀疏图。

3.3 多源最短路

动态规划的方法:

  • f [ i , j , k ] f[i,j,k] f[i,j,k]表示从 i i i j j j 的最短路径上有 1   k 1~k 1 k 中的某个点作为中介点,那么有:
    f [ i , j , k ] = m i n ( f [ i , j k − 1 ] , f [ i , k , k − 1 ] + f [ k , j , k − 1 ] ) f[i,j,k] = min(f[i,jk-1],f[i,k,k-1]+f[k,j,k-1]) f[i,j,k]=min(f[i,jk1],f[i,k,k1]+f[k,j,k1])

    解释:
    1. 不用 k k k 做中介点( f [ i , j , k − 1 ] f[i,j,k-1] f[i,j,k1] );
    2. 用 k k k 做中介点( f [ i , k , k − 1 ] + f [ k , j , k − 1 ] f[i,k,k-1]+f[k,j,k-1] f[i,k,k1]+f[k,j,k1]

  • 因为在计算第 k k k 层的 f [ i , j ] f[i,j] f[i,j] 的时候必须先将第 k − 1 k-1 k1 层的所有状态计算出来,所以需要把 k k k 放在最外层。

# 多源最短路(n^3)
def floyd():
    # 遍历每个点为中转点
    for k in range(n):
        # 以 k 为中转点 遍历整个图更新路径
        for i in range(n):
            for j in range(n):
                # 判断 i 是否能够到达 k 再到达 j
                if g[i][k]==float('inf') or g[k][j]==float('inf'):
                    continue
                g[i][j] = min(g[i][j],g[i][k]+g[k][j])

n = 4
g = [
    [0,2,float('inf'),6],
    [2,0,3,2],
    [float('inf'),3,0,2],
    [6,2,2,0]
]   

时间复杂度: O ( N 3 ) O(N^3) O(N3),适合稠密图

4. 最小生成树

生成树(极小连通子图): 能连通无向图的所有( N 个)顶点而又不产生回路的任何子图(N-1 条边)。
最小生成树: 所有生成树中带权路径和最小的那棵树。

4.1 Prim

与 Dijkstra 算法的思路基本一致,设定一个已访问的顶点集合 S S S ,每次从未访问的节点中,选择一个与 集合 S S S 距离最小的节点(Dijkstra 算法中是选择一个与起点距离最小的节点),对它的邻边进行优化。
如何理解与 集合 S S S 距离最小?设 v v v 是一个未访问的点,则 v v v 与集合 S S S 的距离为:
在这里插入图片描述
每次需要找到这个具有最小的 L L L v v v
在这里插入图片描述
在这里插入图片描述


时间复杂度: 和 Dijkstra 算法一样,也可以用最小堆进行优化。

4.2 Kruskal

算法步骤:

  1. 将所有边按权重从小打大进行排序。
  2. 按边权重从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块中,则把这条边加入当前最小生成树中;否则,将其舍弃。
  3. 执行步骤 2,直到最小生成树的边数等于总顶点数减 1,或是所有边都遍历完毕。
    在这里插入图片描述
    判断两个节点是否在一个连通块中,需要使用并查集
#最小生成树(利用并查集来寻找代表元)
def find(x):
    r=x
    while r!=pre[r]:
        r=pre[r]
    while x!=r:     #压缩路径
        j=pre[x]
        pre[x]=r
        x=j
    return r
    
def join(x,y):
    fx,fy=find(x),find(y)
    if fx!=fy:
        pre[fx]=fy
        return True
    return False
    
n,m = map(int,input().split())
mst,edges = [],[]     # 最小生成树的序列, 存储边的列表
pre = [i for i in range(n+1)]    # 每个顶点的代表元
for i in range(m):
    x,y,w = map(int,input().split())
    edges.append((w,x,y))
    
length = 0
edges.sort()    # 将边以权值从小到大排序

for w,x,y in edges:
    if join(x,y):      # 用于判断两顶点是否为不同连通分量
        mst.append(((x,y),w))
        length += w
        if len(mst) == n-1: # 直至图中加入n-1条边
            print(length)
            break
else:
    print("orz")

5. 拓扑排序

拓扑排序是将有向无环图 的所有顶点排成一个线性序列,使得对图 中的任意两个顶点 ,如果存在边 ,那么在序列中 一定在 前面。但同一层节点的先后关系无严格限制。
在这里插入图片描述
算法步骤:

  1. 定义一个队列 Q Q Q ,并把所有入度 为 0 的节点加入队列。(因为同一层节点先后顺序不要求,所以用栈也可以)
  2. 取队首节点 u u u ,输出(或记录入某个数组)。然后遍历所有从它出发的边,将这些边的目的顶点 v v v 的入度减 1 (即删去这些边)。如果某个顶点的入度变成了 0 ,就将其加入队列。
  3. 反复执行 ② 操作,直到队列为空。
  4. 当队列为空时,如果输出或记录的节点数正好为 N N N ,就说明拓扑排序成功,并且 G G G 为有向无环图。否则失败, G G G中有环。
def topoSort(g,in_):
    import collections
    que = collections.deque()
    for i in range(1,n+1):
        if in_[i]==0:
            que.append(i)

    ans = []
    while que:
        cur = que.popleft()
        ans.append(cur)

        for next in g[cur]:
            in_[next]-=1
            if in_[next] == 0:
                que.append(next)
    return ans

习题

#Dijkstra
P4779 【模板】单源最短路径(标准版)
P1576 最小花费
P1629 邮递员送信
P1529 [USACO2.4]回家 Bessie Come Home
P1744 采购特价商品
P1821 [USACO07FEB] Cow Party S

n,m,x = map(int,input().split())
g1 = [[] for i in range(n+1)]
g2 = [[] for i in range(n+1)]

# 有向图
#求 一个单源最短路 + 一个单终点最短路 的最大值。

'''
    反向建图,直接把单终点最短路转为单源最短路,只需要跑两次最短路算法
'''

for i in range(m):
    u,v,w = map(int,input().split())
    g1[u].append((w,v))
    g2[v].append((w,u))

import heapq
def dijkstra(s,g):
    dist = [float('inf')]*(n+1)
    visited = [0]*(n+1)
    que = [(0,s)]
    dist[s] = 0

    while que:
        curDis,curNode = heapq.heappop(que)
        if visited[curNode]:
            continue
        visited[curNode] = 1
        for w,idx in g[curNode]:
            if dist[idx]>curDis+w:
                dist[idx] = curDis+w
                heapq.heappush(que,(dist[idx],idx))
    return dist



dis1 = dijkstra(x,g1)
dis2 = dijkstra(x,g2)
ans = [0]*(n+1)
for i in range(1,n+1):
    ans[i] = dis1[i]+dis2[i]
print(max(ans))



P2299 Mzc和体委的争夺战
P2951 [USACO09OPEN]Hide and Seek S
P2984 [USACO10FEB]Chocolate Giving S
P1339 [USACO09OCT]Heat Wave G
P6770 [USACO05MAR]Checking an Alibi 不在场的证明

#Spfa
P1938 [USACO09NOV]Job Hunt S

'''
这道题完美地需要我们将点权转成边权:

贝西需要去这么多城市,而每座城市他都要赚 D 美元。那我们为什么不直接将每条普通边的边权就设置成 D 呢?这两者之间是等价的呀·····

那对于飞行边来说,同理,飞行边需要花费 T 美元,而到一个城市又可以赚 D 美元,那我们就可以将飞行边权设为 D-T。

这样,存图的事就解决了

2. 只要你读懂题目,就会发现这题压根不是最短路,是最长路······
做最长路主要有两种做法:
    2.1 做最短路是我们是现将 dis 数组设为无穷大,再每次更新最小值。反过来,最长路就可以将 dis 数组都设为 0 ,再每次更新最大值。
    2.2 将所有边权都取反后,再求最短路
'''

D,P,C,F,S = map(int,input().split())

g = [[] for i in range(C+1)]

for i in range(P):
    a,b = map(int,input().split())
    g[a].append((D,b))

for i in range(F):
    a,b,w = map(int,input().split())
    g[a].append((D-w,b))


from collections import deque
def SPFA(s,g):
    dist = [0]*(C+1)
    visited = [0]*(C+1)
    cnt = [0]*(C+1)
    que = deque([s])
    visited[s] = 1
    cnt[s] = 1
    dist[s] = D

    while que:
        curNode = que.popleft()
        visited[curNode] = 0
        for w,idx in g[curNode]:
            if dist[idx]<w+dist[curNode]:
                dist[idx] = w+dist[curNode]
                cnt[idx] = cnt[curNode]+1
                if cnt[idx]>C:
                    return []
                if not visited[idx]:
                    que.append(idx)
                    visited[idx] = 1
    return dist

dist = SPFA(S,g)
if len(dist)==0:
    print(-1)
else:
    print(max(dist))

P3906 Geodetic集合

n,m = map(int,input().split())

'''
先用 spfa 单源最短路径算出 v 点到其他点的最短距离,用dis1数组存储。
然后再用 spfa 算出 u 点到其他点的最短距离,用 dis2 数组存储。
算完最短距离后开始遍历每个点,
如果这个点到 v 的最短路径(dis1[i])加上这个点到 u 的最短路径(dis2[i])恰好等于 v 到 u 的最短距离(dis1[u]),那么就输出这个点的编号。
'''
g = [[] for i in range(n+1)]
for i in range(m):
    u,v = map(int,input().split())
    g[u].append((1,v))
    g[v].append((1,u))

k = int(input())
from collections import deque
def SPFA(s,g):
    dist = [float('inf')]*(n+1)
    visited = [0]*(n+1)
    que = deque([s])
    visited[s] = 1
    dist[s] = 0

    while que:
        curNode = que.popleft()
        visited[curNode] = 0
        for w,idx in g[curNode]:
            if dist[idx]>w+dist[curNode]:
                dist[idx] = w+dist[curNode]
                if not visited[idx]:
                    que.append(idx)
                    visited[idx] = 1
    return dist

for i in range(k):
    s,e = map(int,input().split())
    dis1 = SPFA(s,g)
    dis2 = SPFA(e,g)
    ans = []
    for i in range(1,n+1):
        if dis1[i]+dis2[i]==dis1[e]:
            ans.append(i)
    print(*ans)

P1144 最短路计数

n,m = map(int,input().split())

g = [[] for i in range(n+1)]
edges = []
for i in range(m):
    a,b = map(int,input().split())
    g[a].append((1,b))
    g[b].append((1,a))
    edges.append((a,b))
from collections import deque

ans = [0]*(n+1)
ans[1] = 1

def SPFA(s,g):
    dist = [float('inf')]*(n+1)
    visited = [0]*(n+1)
    cnt = [0]*(n+1)
    que = deque([s])
    cnt[s] = 1
    visited[s] = 1
    dist[s] = 0
    while que:
        curNode = que.popleft()
        visited[curNode] = 0
        for w,idx in g[curNode]:
            if dist[idx]>w+dist[curNode]:
                dist[idx] = w+dist[curNode]
                ans[idx] = ans[curNode] # idx 和 curNode 处于同一条最短路上
                cnt[idx] = cnt[curNode]+1
                if cnt[idx]>n:
                    return 
                if not visited[idx]:
                    que.append(idx)
                    visited[idx] = 1
            elif dist[idx]==w+dist[curNode]: # 该点最短路个数为 idx 最短路个数+ curNode最短路个数
                ans[idx] = (ans[idx]+ans[curNode])%100003
    return 

dis1 = SPFA(1,g)
for i in range(1,n+1):
    print(ans[i])

P2850 [USACO06DEC]Wormholes G

t = int(input())

'''
此题就是求一个图有没有负权回路。

虫洞就是边权为负的边。

用spfa判负权回路:从随便一个点出发,当某个点松弛了超过n次后,便可以说明有负环
'''
from collections import deque
def SPFA(s,g):
    n = len(g)
    dist = [float('inf')]*(n)
    visited = [0]*n
    cnt = [0]*n
    que = deque([s])
    dist[s] = 0
    cnt[s] = 1
    visited[s] = 1

    while que:
        u = que.popleft()
        visited[u] = 0
        for w,v in g[u]:
            if dist[v]>w+dist[u]:
                dist[v] = w+dist[u]
                cnt[v] = cnt[u]+1
                if cnt[v]>n-1:
                    return True
                if not visited[v]:
                    que.append(v)
                    visited[v] = 1
    return False
for s in range(t):
    n,m,w = map(int,input().split())
    g = [[] for i in range(n+1)]
    for i in range(m):
        u,v,p = map(int,input().split())
        g[u].append((p,v))
        g[v].append((p,u))
        
    for i in range(w):
        u,v,p = map(int,input().split())
        g[u].append((-p,v))
    if SPFA(1,g):
        print('YES')
    else:
        print('NO')

#Floyd
力扣1334. 阈值距离内邻居最少的城市
P2910 [USACO08OPEN]Clear And Present Danger S
P1119 灾后重建
Floyd这个算法的主要思路,就是通过其他的点进行中转来求的两点之间的最短路。

for k in range(n):
	for i in range(n):
        for j in range(n):
            if graph[i][k]==float('inf') or graph[k][j]==float('inf'):
                continue
            graph[i][j] = min(graph[i][j],graph[i][k]+graph[k][j])

这段代码的基本思想就是:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从i号顶点到j号顶点只经过前k号点的最短路程。

而我们再回头看题意:

所有的边全部给出,按照时间顺序更新每一个可用的点(即修建好村庄),对于每个时间点进行两点之间询问,求对于目前建设的所有村庄来说任意两点之间的最短路

不正好就是Floyd算法中使用前k个节点更新最短路的思维吗?

n,m = map(int,input().split())
ls_t = list(map(int,input().split()))

graph = [[float('inf')]*n for i in range(n)]
for i in range(n):
    graph[i][i] = 0

for i in range(m):
    a,b,w = map(int,input().split())
    graph[a][b] = w
    graph[b][a] = w

q = int(input())
visited = [0]*(n+1)

for i in range(q):
    x,y,t = map(int,input().split())
    for k in range(n):
        if ls_t[k]<=t and not visited[k]:
            visited[k]=1
            for i in range(n):
                for j in range(n):
                    if graph[i][k]==float('inf') or graph[k][j]==float('inf') or i==j:continue
                    graph[i][j] = min(graph[i][j],graph[i][k]+graph[k][j])
    
    if ls_t[x]<=t and ls_t[y]<=t and graph[x][y]!=float('inf'):
        print(graph[x][y])
    else:
        print(-1)

#最小生成树
重点在图的建立

P1547 [USACO05MAR]Out of Hay S
P1991 无线通讯网
P2916 [USACO08NOV]Cheering up the Cow G

'''与奶牛谈话也需要时间,而且每条路都需要来回走两遍。

所以每条边的权值为:起点权值+终点权值+路的长度*2.

然后就能直接跑最小生成树了
'''
n,m = map(int,input().split())

min_=float('inf')
cs = []
for i in range(n):
    s = int(input())
    min_=min(min_,s)
    cs.append(s)

edges = []

for i in range(m):
    a,b,w = map(int,input().split())
    edges.append((2*w+cs[a-1]+cs[b-1],a,b))

ans = 0

pre = [i for i in range(n)]
def find(x):
    r=x 
    while r!=pre[r]:
        r=pre[r]
    while x!=r:
        pre[x],x=r,pre[x]
    return x

edges.sort()
cnt=0
for w,a,b in edges:
    fa,fb = find(a-1),find(b-1)
    if fa!=fb:
        pre[fa]=fb
        cnt+=1
        ans+=w
        if cnt==n-1:
            print(ans+min_) #加劝说出发点的奶牛 
            break



P2330 [SCOI2005]繁忙的都市
P2820 局域网
P4047 [JSOI2010]部落划分

# 一共有k个部落,我们只需要找n-k条边得到k个连通分量,第n-k+1条边就是答案。
n,k = map(int,input().split())

locs = []

for i in range(n):
    x,y = map(int,input().split())
    locs.append((x,y))

edges = []
import math
for i in range(n):
    x,y = locs[i]
    for j in range(i+1,n):
        x2,y2 = locs[j]
        w = math.sqrt((x-x2)**2+(y-y2)**2)
        edges.append((w,i,j))

edges.sort()

max_ = -1
pre = [i for i in range(n)]

def find(x):
    r = x
    while r!=pre[r]:r=pre[r]
    while x!=r:pre[x],x=r,pre[x]
    return x

cnt = 0
for i in range(len(edges)):
    w,a,b = edges[i]
    fa,fb = find(a),find(b)
    if fa!=fb:
        pre[fa]=fb
        cnt+=1
        if cnt==n-k+1:
            print('{:.2f}'.format(edges[i][0]))
            break

#并查集
P1551 亲戚
P2078 朋友
P1111 修复公路
P8654 [蓝桥杯 2017 国 C] 合根植物
P1536 村村通
P2256 一中校运会之百米跑

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值