最短路
本文主要介绍最短路相关问题的基本介绍,较为基础。本篇将以几道模板题,尝试说明这几个算法。
常见的最短路问题
1. 单源最短路
求一个点到其他点的最短距离
(1)所有边权都为正数
m 如果和 n^2一个级别的叫稠密图
m如果和 n 一个级别的就叫 稀疏图
(N为一个图中的点数)
- 朴素Dijkstra算法 时间复杂度:O(N ^ 2)
- 堆优化版的Dijkstra算法O(m logN )
(2)存在负权边
- Bellman-Ford算法O(n m)
- SPFA算法,一般: O(m)最坏: O(n m)
(最短路经过的边数<= k 就不能用该算法)
2.多源汇最短路
(源点: 起点; 汇点:终点)
可能会有很多询问,任选一个点到另一个点的最短距离。(起点、终点不确定)
Floyd算法 O(n ^ 3)
难点:建图,转化成最短路问题
Dijkstra求最短路 I
基本思路
- Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T
- 初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
- 然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中
- 然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点
- 举例如果目前离 v1顶点最近的是 v3顶点,因为这个图所有的边都是正数,那么肯定不可能通过第三个顶点中转,使得 v1顶点到 v3顶点的路程进一步缩短了。因为 v1顶点到其它顶点的路程肯定没有 v1到 v3顶点短.
- 稠密图:我们用邻接矩阵来存
问题描述
题目链接
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n≤500,
1≤m≤10^5,
图中涉及边长均不超过10000。
代码
#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 510;
int n, m;
int dis[N], g[N][N];
//dis存储1号点到其他点的当前最短距离
int st[N]; //当前每个点的最短路是否确定
int dijkstra(){
memset(dis, 0x3f, sizeof(dis));
dis[1] = 0;
for(int i = 1; i <= n; i ++){//n个点要进行n次迭代
int t = -1; //存储当前访问的点
for(int j = 1; j <= n; j ++){
if(st[j] == 0 && (t == -1 || dis[t] > dis[j])) t = j;
}
st[t] = 1;
for(int j = 1; j <= n; j ++) dis[j] = min(dis[j], dis[t] + g[t][j]);
}
if(dis[n] == 0x3f3f3f3f) return -1;
return dis[n];
}
int main(void){
cin >> n >> m;
memset(g, 0x3f, sizeof(g));
while(m --){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c);
}
cout << dijkstra() << endl;
return 0;
}
Dijkstra求最短路 II
题目描述
题目链接
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n,m≤1.5×10^5,
图中涉及边长均不小于0,且不超过10000。
分析:
-
本题的数据范围更大,且m 与 n是一个级别的,我们采用优化版的Dijkstra算法解决
-
为稀疏图,我们用邻接表的形式存储
-
用数据结构 堆 来找一个最小值,时间复杂度为 O(1), 更新一个点logn
堆的相关内容可参考本人的这篇博客
博客链接 -
选用优先队列,我们需要两个参数,可用pair存储,第一个参数是距离,第二个参数是 节点编号。
-
我们在这里选择优先队列的写法,会造成冗余,我们需要设置一个标记数组,判断是否已经判断过这个点。
代码
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef pair <int, int> PII;
const int N = 1e6 + 10;
int h[N], w[N], e[N], ne[N];
int n, m, idx;
int dis[N], st[N];
void add(int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a];
h[a] = idx ++;
}
int dijkstra(){
memset(dis, 0x3f, sizeof(dis));
dis[1] = 0;
priority_queue <PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1});
while(heap.size()){
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if(st[ver]) continue;
st[ver] = 1;
for(int i = h[ver]; i != -1; i = ne[i]){
int j = e[i];
if(dis[j] > distance + w[i]){
dis[j] = distance + w[i];
heap.push({dis[j], j});
}
}
}
if(dis[n] == 0x3f3f3f3f) return -1;
return dis[n];
}
int main(void){
cin >> n >> m;
memset(h, -1, sizeof(dis));
while(m --){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
cout << dijkstra() << endl;
return 0;
}
Bellman-Ford
简单说明
- 存储:开个结构体即可,(随便存,只要保证遍历到所有边即可)
- 迭代n次;
每次都要:遍历所有边,dis[b] = min(dis[b], dis[a] + w). “松弛操作” - 两重循环执行完毕后,会满足一个三角不等式:
dis[b] <= dis[a] + w
- 如果有负权回路存在,最短路可能就不存在了
例题:有边数限制的最短路
问题描述
题目链接
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出impossible。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数n,m,k。
接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。
如果不存在满足条件的路径,则输出“impossible”。
1≤n,k≤500,
1≤m≤10000,
任意边长的绝对值不超过10000。
注意
- 我们规定了最多经过k条边,我们有负环也我所谓,可以求出最短路
- 因为有了不能超过k条边的限制,我们在迭代每次之前都得拷贝一个数组,记录上一次的结果。举例说明: a 到 b已更新,拿着更新的b去更新c,顺次往下进行,而有可能a不能更新到b(达不到满足k条边、最短路的要求)
那么下边更新的也就错了,结合代码更易理解 - 我们比较时,用的是 0x3f3f3f3f / 2, 因为我们存在负权边。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 510, M = 10010;
int n, m, k;
int dis[N], backup[N];
struct edge{
int a, b, w;
}edges[M];
int bellman_ford(){
memset(dis, 0x3f, sizeof(dis));
dis[1] = 0;
for(int i = 0; i < k; i ++){
memcpy(backup, dis, sizeof(dis));
for(int j = 0; j < m; j ++){
int a = edges[j].a, b = edges[j].b;
int w = edges[j].w;
dis[b] = min(dis[b], backup[a] + w);
}
}
if(dis[n] > 0x3f3f3f3f / 2) return -1;
return dis[n];
}
int main(void){
scanf("%d%d%d", &n, &m, &k);
for(int i = 0; i < m; i ++){
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
int t = bellman_ford();
if(t == -1) puts("impossible");
else printf("%d\n", t);
return 0;
}
spfa求最短路
简单介绍
- SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化,它是一种十分高效的最短路算法。
- 实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
- st[]数组,记录哪个点已经进入队列,防止队列中有重复点。
问题描述
题目链接
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible。
数据保证不存在负权回路。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出”impossible”。
数据范围
1≤n,m≤10^5,
图中涉及边长绝对值均不超过10000。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef pair <int, int> PII;
const int N = 1e5 + 10;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dis[N], st[N];
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] =h[a];
h[a] = idx ++;
}
int spfa(){
memset(dis, 0x3f, sizeof(dis));
dis[1] = 0;
queue <int> q;
q.push(1);
st[1] = 1;
while(q.size()){
int t = q.front();
q.pop();
st[t] = 0;
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(dis[j] > dis[t] + w[i]){
dis[j] = dis[t] + w[i];
if(st[j] == 0){
q.push(j);
st[j] = 1;
}
}
}
}
if(dis[n] == 0x3f3f3f3f) return -1;
return dis[n];
}
int main(void){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof(h));
while(m --){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t = spfa();
if(t == -1) printf("impossible\n");
else printf("%d\n", t);
return 0;
}
spfa判断负环
题目描述
题目链接
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
如果图中存在负权回路,则输出“Yes”,否则输出“No”。
数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过10000。
思路
- 我们dis[]数组,记录的是1号点到其他点的最短距离
- 设置一个cnt[]数组,最短路的一个边数,当某个cnt[x] > n时,说明有负环。
cnt[x] = cnt[t] + 1
- 注意:判断是否存在负环,并不是说只是从1开始的负环。一开始我们要把所有点放入队列里。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef pair <int, int> PII;
const int N = 1e5 + 10;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dis[N], st[N], cnt[N];
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] =h[a];
h[a] = idx ++;
}
int spfa(){
dis[1] = 0;
queue <int> q;
for (int i = 1; i <= n; i++) {
q.push(i);
st[i] = true;
}
while(q.size()){
int t = q.front();
q.pop();
st[t] = 0;
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(dis[j] > dis[t] + w[i]){
dis[j] = dis[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(st[j] == 0){
q.push(j);
st[j] = 1;
}
}
}
}
return false;
}
int main(void){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof(h));
while(m --){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
if(spfa()) puts("Yes");
else puts("No");
return 0;
}
Floyd求最短路
基本知识
- 邻接表存储
dis[i][j] 存储所有边
- 三个循环完成。
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]);
}
}
}
结束后的表示含义:dis[i][j]
就表示i, j 的最短距离
循环含义: 从 i 出发 只经过 1- k 中间点到达 j 的最短距离
代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, q;
int d[N][N];
void floyd(){
for(int k = 1; k <= n; k ++){
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
}
}
}
int main(void){
cin >> n >> m >> q;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
if(i == j) d[i][j] = 0;
else d[i][j] = INF;
}
}
while(m --){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
d[a][b] = min(d[a][b], c);
}
floyd();
while(q --){
int a, b;
scanf("%d%d", &a, &b);
if(d[a][b] > INF / 2) puts("impossible");
else printf("%d\n", d[a][b]);
}
return 0;
}