图论(学习笔记)

目录

图的基本概念

图的存储

Floyd算法

Dijkstra算法

Bellman-ford

SPFA


1 图的基本概念

:由点(node,或者vertex)和连接点的边(edge)组成。 图是点和边构成的网。

a,b,c,d,e是权值,箭头代表方向

树:特殊的图

 树,即连通无环图 树的结点从根开始,层层扩展子树,是一种层次关系,这种层次关系,保证了树上不会出现环路。 两点之间的路径:有且仅有一条路径。 最近公共祖先。

(1)无向无权图,边没有权值、没有方向;

(2)有向无权图,边有方向、无权值;

(3)加权无向图,边有权值,但没有方向;

(4)加权有向图;

(5)有向无环图(Directed Acyclic Graph,DAG)

邻接矩阵 

二维数组:  graph[NUM ][NUM ]

无向图:graph[i][j]  = graph[j][i]。

有向图:graph[i][j] != graph[j][i]。

权值:graph[i][j]存结点i到j的边的权值。例如graph[1][2] = 3,graph[2][1] = 5等等。用graph[i][j] = INF表示i,j之间无边

邻接表和链式前向星

应用场景:大稀疏图。 优点: 存储效率非常高,存储复杂度O(V+E); 能存储重边。


2 最短路问题 

简单图的最短路径            

树上的路径:任意2点之间只有一条路径            

所有边长都为1的图:用BFS搜最短路径,复杂度O(n+m) 普通图的最短路径            

边长:不一定等于1,而且可能为负数            

算法:Floyd、Dijkstra、SPFA等,各有应用场景,不可互相替代

问题

边权

算法

时间复杂度

一个起点,一个终点

非负数;

无边权(或边权为1)

A*

< O((m+n)logn)

双向广搜

< O((m+n)logn)

贪心最优搜索

< O(m+n)

一个起点到其他所有点

无边权(或边权为1)

BFS

O(m+n)

非负数

Dijkstra(堆优化优先队列)

O((m+n)logn)

允许有负数

SPFA

< O(mn)

所有点对之间

允许有负数

Floyd-Warshall

O(n3)


3 Floyd算法

动态规划:求图上两点i、j之间的最短距离,按“从小图到全图”的步骤,在逐步扩大图的过程中计算和更新最短路。

定义状态:dp[k][i][j],i、j、k是点的编号,范围1 ~ n。状态dp[k][i][j]表示在包含1 ~ k点的子图上,点对i、j之间的最短路。

状态转移方程:从子图1 ~ k-1扩展到子图1 ~ k     dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])

 dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])

虚线圆圈:包含1 ~ k-1点的子图。 dp[k-1][i][j]:虚线子图内的点对i、j的最短路; dp[k-1][i][k] + dp[k-1][k][j]:经过k点的新路径的长度,即这条路径从i出发,先到k,再从k到终点j。

比较:不经过k的最短路径dp[k-1][i][j]和经过k的新路径,较小者就是新的dp[k][i][j]。

用滚动数组简化:  dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]) 

Floyd
for k in range(1,n):
  for i in range(1,n):
    for j in range(1,n):
      dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j])

Floyd算法有个特点:能判断负圈。

负圈:若图中有权值为负的边,某个经过这个负边的环路,所有边长相加的总长度也是负数,这就是负圈。在这个负圈上每绕一圈,总长度就更小,从而陷入在负圈上兜圈子的死循环。

Floyd算法很容易判断负圈,只要在算法运行过程出现任意一个dp[i][i] < 0就说明有负圈。因为dp[i][i]是从i出发,经过其他中转点绕一圈回到自己的最短路径,如果小于零,就存在负圈。

题目:蓝桥公园

题目描述

小明喜欢观景,于是今天他来到了蓝桥公园。

已知公园有 N 个景点,景点和景点之间一共有 M 条道路。小明有 Q 个观景计划,每个计划包含一个起点 st 和一个终点 ed,表示他想从 st 去到 ed。但是小明的体力有限,对于每个计划他想走最少的路完成,你可以帮帮他吗? 

输入描述

输入第一行包含三个正整数 N,M,Q

第 2 到M+1 行每行包含三个正整数 u,v,w,表示 u↔v 之间存在一条距离为 w 的路。

第 M+2 到 M+Q−1 行每行包含两个正整数 st,ed,其含义如题所述。

1≤N≤400,1≤M≤N×(N−1)/2​,Q≤10^3,≤u,v,st,ed≤n,1≤w≤10^9

输出描述

输出共 Q 行,对应输入数据中的查询。

若无法从 st 到达 ed 则输出 −1。

输入输出样例

示例 1

输入

3 3 3
1 2 1
1 3 5
2 3 2
1 2
1 3
2 3

输出

1
3
2
import sys
INF = 0x3f3f3f3f3f3f3f3f
N = 405
dp = [[INF for j in range(N)] for i in range(N)]

def floyd():
    global dp
    for k in range(1, n+1):
        for i in range(1, n+1):
            for j in range(1, n+1):
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j])

n, m, q = map(int, input().split())
for i in range(1, m+1):
    u, v, w = map(int, input().split())
    dp[u][v] = dp[v][u] = min(dp[u][v], w)

floyd()

for i in range(q):
    s, t = map(int, input().split())
    if dp[s][t] == INF:
        print("-1")
    elif s == t:
        print("0")
    else:
        print(dp[s][t])

4 Dijstra 算法

Dijkstra算法算是贪心思想实现的,首先把起点到所有点的距离存下来找个最短的,然后松弛一次再找出最短的,所谓的松弛操作就是,遍历一遍看通过刚刚找到的距离最短的点作为中转站会不会更近,如果更近了就更新距离,这样把所有的点找遍之后就存下了起点到其他所有点的最短距离

Dijkstra算法应用了贪心算法的思想,即“抄近路走,肯定能找到最短路径”。

每次往队列中放新数据时,按从小到大的顺序放,采用小顶堆的方式,复杂度是O(logn),保证最小的数总在最前面; 找最小值,直接取B的第一个数,复杂度是O(1)。

复杂度:用优先队列时,Dijkstra算法的复杂度是O(mlogn),是最高效的最短路算法。

算法实现:

维护两个集合:已确定最短路径的结点集合A、这些结点向外扩散的邻居结点集合B。

(1)把起点s放到A中,把s所有的邻居放到B中。此时,邻居到s的距离就是直连距离。

(2)从B中找出距离起点s最短的结点u,放到A中。

(3)把u所有的新邻居放到B中。显然,u的每一条边都连接了一个邻居,每个新邻居都要加进去。其中u的一个新邻居v,它到s的距离dis(s, v)等于dis(s, u) + dis(u, v)。

(4)重复(2)、(3),直到B为空时,结束。

题目:蓝桥王国

题目描述

小明是蓝桥王国的王子,今天是他登基之日。

在即将成为国王之前,老国王给他出了道题,他想要考验小明是否有能力管理国家。

题目的内容如下:

蓝桥王国一共有 N 个建筑和 M 条单向道路,每条道路都连接着两个建筑,每个建筑都有自己编号,分别为1∼N 。(其中皇宫的编号为 1) 

国王想让小明回答从皇宫到每个建筑的最短路径是多少,但紧张的小明此时已经无法思考,请你编写程序帮助小明回答国王的考核。

输入描述

输入第一行包含三个正整数 N,M。

第 2 到M+1 行每行包含三个正整数 u,v,w,表示 u→v 之间存在一条距离为 w 的路。

1≤N≤3×105,1≤m≤10^6,1≤ui​,vi​≤N,0≤wi​≤10^9。

输出描述

输出仅一行,共 N 个数,分别表示从皇宫到编号为1∼N 建筑的最短距离,两两之间用空格隔开。(如果无法到达则输出 −1)

输入输出样例

示例 1

输入

3 3 
1 2 1
1 3 5
2 3 2

输出

0 1 3

题解:

Python
import heapq

INF = 0x3f3f3f3f3f3f3f3f
N = 300002

class Edge:
    def __init__(self, fr, to, w):
        self.fr = fr
        self.to = to
        self.w = w

class SNode:
    def __init__(self, id, n_dis):
        self.id = id
        self.n_dis = n_dis
    
    def __lt__(self, other):
        return self.n_dis < other.n_dis

def print_path(s, t):
    if s == t:
        print(s, end=" ")
        return
    print_path(s, pre[t])
    print(t, end=" ")

def dijkstra():
    s = 1
    done = [False] * N
    dis = [INF] * N
    pre = [-1] * N
    dis[s] = 0
    pq = []
    heapq.heappush(pq, SNode(s, dis[s]))
    while pq:
        u = heapq.heappop(pq)
        if done[u.id]:
            continue
        done[u.id] = True
        for y in e[u.id]:
            if done[y.to]:
                continue
            if dis[y.to] > y.w + u.n_dis:
                dis[y.to] = y.w + u.n_dis
                heapq.heappush(pq, SNode(y.to, dis[y.to]))
                pre[y.to] = u.id
    for i in range(1, n+1):
        if dis[i] >= INF:
            print("-1", end=" ")
        else:
            print(dis[i], end=" ")

n, m = map(int, input().split())
e = [[] for _ in range(N)]
for i in range(m):
    u, v, w = map(int, input().split())
    e[u].append(Edge(u, v, w))

dijkstra()

5 Bellman-ford算法

单源最短路径问题:给定一个起点s,求它到图中所有n个结点的最短路径。

问题引入:问路

图中每个点上站着一个“警察”。 每个警察问邻居:走你这条路能到s吗?有多远? 反复问多次,最后所有警察都能得到最短路。

第1轮,给所有n个人每人一次机会,问他的邻居,到s的最短距离是多少?     更新每人到s的最短距离。     特别地,在s的直连邻居中,有个t,得到了到s的最短距离。(注意,算法并没有查找是哪个t)

第2轮,重复第1轮的操作。     更新每人到s的最短距离。     特别地,在s和t的直连邻居中,有个v,得到了到s的最短距离。

第3轮,……

关于算法复杂度

一共需要几轮操作?每一轮操作,都至少有一个新的结点得到了到s的最短路径。所以,最多只需要n轮操作,就能完成n个结点。 在每一轮操作中,需要检查所有m个边,更新最短距离。 所以,Bellman-Ford算法的复杂度:O(nm)。


但是这个算法有个致命的缺点:进行了很多重复而且无用的计算

所以我们对这个算法进行了更新

我们计算结点u之后,下一步只计算和调整它的邻居,能加快收敛的过程,可以用队列对这个过程进行计算

改进后的算法称为:

6 SPFA算法

步骤:

(1)起点s入队,计算它所有邻居到s的最短距离。把s出队,状态有更新的邻居入队,没更新的不入队。

(2)现在队列的头部是s的一个邻居u。弹出u,更新它所有邻居的状态,把其中有状态变化的邻居入队列。

(3)继续以上过程,直到队列空。这也意味着,所有结点的状态都不再更新。最后的状态就是到起点s的最短路径。

题目:随机数据下的最短路问题

题目描述

给定 N 个点和 M 条单向道路,每条道路都连接着两个点,每个点都有自己编号,分别为 1∼N 。

问你从 S 点出发,到达每个点的最短路径为多少。

输入描述

输入第一行包含三个正整数N,M,S。

第 2 到 M+1 行每行包含三个正整数 u,v,w,表示 u→v 之间存在一条距离为 w 的路。

1≤N≤5×10^3,1≤M≤5×10^4,1≤ui​,vi​≤N,0≤wi​≤10^9。

本题数据随机生成。

输出描述

输出仅一行,共 N 个数,分别表示从编号 S 到编号为 1∼N 点的最短距离,两两之间用空格隔开。(如果无法到达则输出 −1)

输入输出样例

示例 1

输入

3 3 1
1 2 1
1 3 5
2 3 2

输出

0 1 3

 题解:

INF = 0x3f3f3f3f3f3f3f3f
N = 5010

class Edge:
    def __init__(self, to, w):
        self.to = to
        self.w = w

dist = [INF] * N
inq = [0] * N
e = [[] for _ in range(N)]

def spfa(s):
    global dist
    global inq
    dist = [INF] * N
    dist[s] = 0
    q = []
    heapq.heappush(q, s)
    inq[s] = 1
    while q:
        u = heapq.heappop(q)
        inq[u] = 0
        if dist[u] == INF:
            continue
        for i in range(len(e[u])):
            v = e[u][i].to
            w = e[u][i].w
            if dist[v] > dist[u] + w:
                dist[v] = dist[u] + w
                if inq[v] == 0:
                    heapq.heappush(q, v)
                    inq[v] = 1

n, m, s = map(int, input().split())
for i in range(m):
    u, v, w = map(int, input().split())
    e[u].append(Edge(v, w))
spfa(s)
for i in range(1, n+1):
    if dist[i] == INF:
        print("-1", end=" ")
    else:
        print(dist[i], end=" ")

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值