目录
图的基本概念
图的存储
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=" ")