最短路
回顾:链式前向星(邻接表的数组实现)
struct edge{ int nxt, to, dis; }edge[N];
int head[N];
int cnt = 0;
inline void add_edge(int from, int to, int val){
edge[++cnt] = {head[from], to, val};
head[from] = cnt;
}
inline void input(){
for(int i = 1, x, y, z; i <= m; i ++){
read(x); read(y); read(z);
add_edge(x, y, z);
}
}
inline void run(){
for(int i = 1; i <= n; i ++){
for(int k = head[k]; ~k; k = edge[k].nxt){
//
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# DIJKSTRA
# 概念
迪杰斯特拉算法(Dijkstra)是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。---《百度百科》
# 思想:
- 按照最短路径长度递增的次序,依次求的——源点到其余各点到最短路径
- 先求——最短的那条最短路径
- 再求——第二短的最短路径
- ......
- 依次类推
- ......
- 最终求出所有的最短路径
# 算法变量
Dist[i]表示从起点到i点的最短路总权
Map[i][j]表示从i点到j点的距离权
算法图示 无向图的储存(邻接矩阵)

0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 10 | 30 | 100 | |||
1 | 5 | |||||
2 | 10 | 5 | 50 | |||
3 | 50 | 20 | 10 | |||
4 | 30 | 20 | 60 | |||
5 | 100 | 10 | 60 |
# dijkstra模拟求最短路
终点 | Dist[i] | ||||
---|---|---|---|---|---|
i=1 | i=2 | i=3 | i=4 | i=5 | |
V1 | INF | INF | INF | INF | INF |
V2 | 10 | ||||
V3 | INF | 60 | 50 | ||
V4 | 30 | 30 | |||
V5 | 100 | 100 | 90 | 60 | |
Vj | V2 | V4 | V3 | V5 |
# 注意事项
如果无向图要建立反边 如果多条路要选择最短
# 松弛操作
if(Dis[x] + edge[i].val < Dis[edge[i].to])
Dis[edge[i].to] = Dis[x] + edge[i].val,
pque.push({edge[i].to, Dis[edge[i].to]});
2
3
其实就是看看从这个中转站转一下会不会更近
# 整体程序
int dis[N];
bool vis[N];
struct node {
int id, val;
inline friend bool operator < ( node a, node b ) {
return a.val > b.val;
}
};
inline void pre_Dijkstra () {
for ( int i = 0; i <= n; i ++ ) dis[i] = 0x3f3f3f3f,vis[i] = 0;
priority_queue<node> pque;
dis[1] = 0; pque.push({1, 0});
while ( pque.size() ) {
node cur = pque.top(); pque.pop();
if ( vis[cur.id] ) continue; vis[cur.id] = 1;
for ( int i = head[cur.id]; i; i = edge[i].nxt ) {
int to = edge[i].to;
if ( dis[to] > dis[cur.id] + edge[i].val ) {
dis[to] = dis[cur.id] + edge[i].val;
pque.push({to, dis[to]});
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 补:记录路径
我们想用Dijkstra松弛的时候记录一下路径,可以设计每个点的前驱是哪个点
如果设计的是后继的话,由于一个点可能会更新很多个出发点发射出去的最短路
所以后继不能单独表示从出发点到谁的最短路,所以我们要倒着设置,也就是前驱
当然我们还想求到 的哪一条边是最短路径的边
这个可以在设置一个数组,记录前向星的编号
无向图中由于每次加边都是成对加边,边的编号为奇数 时,它和 表示的是同一条边
int pre[N]; // pre[i]: i的前驱是pre[i]
int short_edge[N]; // short_edge[i]:到i点的边中,那一条边是最短路径的边
int to = edge[i].to;
if ( dis[to] > dis[cur.id] + edge[i].val ) {
dis[to] = dis[cur.id] + edge[i].val; pque.push({to, dis[to]});
pre[to] = cur.id; // cur.id -> to
if ( i & 1 ) short_edge[to] = i; // 记录是哪条边(正边)
else short_edge[to] = i - 1; // 记录是哪条边(正边)
}
2
3
4
5
6
7
8
9
10
# 问题
作为最常用的最短路算法,对这个算法的很多问题进行思考
问题一
是否可以解决负环问题?
Answer
考虑这样一个图
由于我们顺序更新后 均小于
故 入队后,我们继续会走
这样 后面虽然更新过 但是 之前入过队并推导完毕,不会去更新
所以 的 错误为
所以不可以解决负环问题
问题二
是否可以求最长路?
Answer
可以看作将边权取反后跑最短路
负环最短路是错误的,所以正环最长路也是错误的,不能解决
问题三
是否可以解决无环负权图?
Answer
可以,但是首先要认清一个事情
该无环图不能有任何意义下的环,即强连通环和弱连通环
否则在问题一给出的图上使边从左到右即可
那么就是一个树图了,...其实 DFS()
就可以
# FLoyd(插点法,经典DP)
求任意两点的最短路
for(int k = 1; k <= n; k ++) //插入点在外层循环很重要!
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
dis[i][j] = MIN(dis[i][j], dis[i][k] + dis[k][j]);
2
3
4
# 算法思想
从i到j的只经过前k个点的最短路径
# 算法特点
简单、粗暴、易于实现、可以解决负权
且拥有独有的优势:
乍一看可能 的复杂度很难解决很多问题
但如果是动态多源最短路
我们每次加入新点的时候都可以在 的复杂度下完成所有点的更新
# 难点思考
三重循环是否可以任意交换顺序?
如果k放里面的话,我们在 更新中用到的 、 不一定是最优解,因为前面并没有更新过
# Bellman-Ford
//数据存储结构?
for(int k = 1; k < n; k ++){ //n - 1个阶段,每个阶段的效果?
for(int i = 1; i <= m; i ++){ //每个阶段,对所有边尝试松弛
dis[v[i]] = MIN(dis[v[i]], dis[u[i]] + w[i]);
}
}
2
3
4
5
6
# 算法思想
一共 个点,所构成最短一条最多 条边
每次松弛都只能完善 个中的一部分边
要想完善完所有 条边
我们要对所有的边进行 次松弛操作
# 算法特点:
简洁、可以解决负权负环的情况
其实并不一定需要 轮,如果前面的点都没做过松弛操作,那么这一趟松弛操作并不会更新,就不需要继续进行(冒泡排序思想)
# 难点思考
如何检测是否存在负环?
如果在某两点内走会不断减权,那么就有负环
for(int k = 1; k <= n - 1; k ++)
for(int i = 1; i <= m; i ++)
dis[v[i]] = MIN(dis[v[i]], dis[u[i]] + w[i]);
//正常情况下,做了n-1趟松弛操作后整个图的最优解就稳定了,如果还有可以松弛的,就是负环
int flag = 0;
for(int i = 1; i <= m; i ++)
if(dis[v[i]] > dis[u[i]] + w[i]) flag = 1;
if(flag) ... //有负环的情况
2
3
4
5
6
7
8
9
# Bellman_Ford算法队列优化(SPFA)
# 基本思想
每次仅对最短路程发生变化了的点的相邻边执行松弛操作
问题:如何知道当前哪些点的最短路程发生了变化呢?
方案:采用“队列“维护
# 具体操作
先把起点加进队列中,然后松弛和起点相连的所有边,如果松弛成功且该点不在队列中,那么就把这个点入队。然后依次取出队列中每一个点进行松弛,直到队列为空。
inline void SPFA(int u){
que.push(u); vis[u] = 1;
while(que.size()){
int x = que.front(); que.pop(); vis[x] = 0;
for(int i = head[x]; ~i; i = edge[i].nxt){
int y = edge[i].to;
if(dis[x] + edge[i].val < dis[y]){
dis[y] = dis[x] + edge[i].val;
if(!vis[y]) vis[y] = 1, que.push(y);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 优缺点总结
DIJKSTRA
优点:大数据单源最短路求解
缺点:负环不能求
FLoyd
优点:擅长求任意两点最短路
缺点:时间复杂度高
Bellman-Ford(SPFA)
优点:求负环问题
缺点:最坏情况复杂度高