最短路问题 - 模板总结(Dijkstra + Bellman-Ford + SPFA + Floyd)
本文参考原网站https://www.it610.com/article/1282949950665146368.htm
*1、Dijkstra算法(正边权单源最短路问题) *
1-1、朴素Dijkstra算法-O(n2)
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
思路:每次把到起点的最短距离的点加入点集
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 510;
int n,m;
int g[maxn][maxn],d[maxn];
bool vis[maxn];
int dijkstra(){
memset(d,0x3f,sizeof d);//初始距离不可达
d[1] = 0;
for(int i = 0;i < n;i++){//遍历n次
int t = -1;
for(int j = 1;j <= n;j++){
if(!vis[j] && (t == -1 || d[t] > d[j])) t = j;//找点集外初始最短边
}
vis[t] = true;//加入点集
for(int j = 1;j <= n;j++){
if(!vis[j] && d[t] + g[t][j] < d[j]) d[j] = d[t] + g[t][j];//松弛操作
}
}
if(d[n] == 0x3f3f3f3f) return -1;//终点不可达
return d[n];
}
int main(){
scanf("%d%d",&n,&m);
int a,b,c;
memset(g,0x3f,sizeof g);
while(m--){
scanf("%d%d%d",&a,&b,&c);
g[a][b] = min(g[a][b],c);
}
cout<<dijkstra()<<endl;
return 0;
}
1-2.堆优化的Dijkstra算法-O(mlogn)
给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出-1。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出-1。
数据范围
1≤n,m≤1.5×105,
图中涉及边长均不小于0,且不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
思路:这题的数据范围要1e5+,所以用邻接矩阵存图肯定是不行的,我们采用邻接表的链式前向星
简单介绍下链式前向星存图:
void add(int x,int y,int z) {
e[cnt] = y; //e[]代表临边
w[cnt] = z; //w[]代表权重
ne[cnt] = h[x]; //ne[] 指向下一条边,若ne[x]!=-1,则x有多条边
h[x] = cnt++; //定位第一条边
}
for(int i = h[k]; i ~; i = ne[i]) {
//遍历
}
ac代码
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int>pll;
const int maxn = 1e6+5;
int w[maxn],e[maxn],h[maxn],nxt[maxn];
int dis[maxn],cnt,x,y,z,n,m;
bool vis[maxn];
priority_queue<pll,vector<pll>,greater<pll> >q; // 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时
// 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
void add(int x,int y,int z) {
e[cnt] = y;
w[cnt] = z;
nxt[cnt] = h[x];
h[x] = cnt++;
}
int dijstra(int n){
memset(dis,0x3f,sizeof dis);
q.push({0,1});
while(q.size()){
pll k = q.top(); q.pop();
int v = k.second; int d = k.first;
if(vis[v]) continue;
vis[v] = true;
for(int i = h[v];i != -1; i = nxt[i]) {
int j = e[i];
if(dis[j] > d + w[i]) {
dis[j] = d + w[i];
q.push({dis[j],j});
}
}
}
if(dis[n] == 0x3f3f3f3f) return -1;
else return dis[n];
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
for(int i = 0;i < m ; ++i){
cin >> x >> y >> z;
add(x,y,z);
}
cout << dijstra(n) << endl;
return 0;
}
2.Bellman-Ford算法(带负权且经过的边的数量有限制)-O(nm)
给定一个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。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
思路: 有负权回路,最短路未必存在(起点到终点的路径上未经过负权环,此时仍然是有最短路的)。
共 迭 代 k 次 , 每 次 遍 历 所 有 的 边 , 更 新 所 有 点 到 起 点 的 最 短 距 离 。
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn = 10010;
struct node{
int a,b,w;
}e[maxn];
int back[maxn],dis[maxn];
int n,m,k;
int bf() {
memset(dis,0x3f,sizeof dis);
dis[1] = 0;
for(int i = 0;i < k ;++i) {
memcpy(back,dis,sizeof dis);
for(int j = 0;j < m ; ++j) {
int a = e[j].a,b = e[j].b , w = e[j].w;
dis[b] = min(dis[b],back[a] + w);
}
}
if(dis[n] > (0x3f3f3f3f) / 2) return -1;
else return dis[n];
}
int main(){
cin >> n >> m >> k;
for(int i = 0; i < m; ++i) {
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
e[i] = {a,b,w};
}
int t = bf();
if(t == -1) printf("impossible\n");
else printf("%d\n",t);
return 0;
}
3.SPFA算法(带负权的最短路)-O(m)~O(nm)
3-1、SPFA求最短路
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible。
数据保证不存在负权回路。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
输出一个整数,表示1号点到n号点的最短距离。
如果路径不存在,则输出”impossible”。
数据范围
1≤n,m≤105
图中涉及边长绝对值均不超过10000。
输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
思路:
S P F A 算 法 是 B e l l m a n − F o r d 算 法 的 优 化 , 我 们 发 现 B F 算 法 每 次 迭 代 均 要 遍 历 所 有 的 边 , 但 事 实 上 , 并 不 是 所 有 边 均 会 被 更 新 得 更 小 。 转 移 方 程 : d i s [ b ] = m i n ( d i s [ b ] , d i s [ a ] + w ) , 可 见 , 只 有 当 d i s [ a ] 减 小 , d i s [ w ] 才 会 变 小 。 因 此 , 只 有 当 某 个 点 到 起 点 的 距 离 变 小 , 经 过 该 节 点 的 其 他 点 到 起 点 的 距 离 才 可 能 变 小。
我 们 采 用 B F S 的 思 想 来 对 B F 算 法 进 行 优 化 , 用 一 个 队 列 来 存 储 到 起 点 距 离 变 短 的 点 , 每 次 出 队 后 用 该 点 更 新 与 其 相 邻 节 点 到 起 点 的 距 离 , 再 将 更 新 过 的 到 起 点 距 离 变 短 的 点 继 续 入 队 。需 要 注 意 , 由 于 存 在 负 权 边 , 因 此 我 们 初 始 化 的 + ∞ 可 能 会 略 微 的 减 小 。 因 此 我 们 认 为 若 某 点 到 起 点 的 距 离 大 于 + ∞ 2 , 则 说 明 从 起 点 无 法 到 达 该 点
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int maxn = 1e5 + 5;
int dis[maxn],e[maxn],w[maxn],h[maxn],ne[maxn];
int x,y,z,cnt,n,m;
bool vis[maxn];
void add(int x,int y,int z) {
e[cnt] = y; //e[]代表临边
w[cnt] = z; //w[]代表权重
ne[cnt] = h[x]; //ne[] 指向下一条边,若ne[x]!=-1,则x有多条边
h[x] = cnt++; //定位第一条边
}
int spfa(int n){
memset(dis,0x3f,sizeof dis);
queue<int>q;
q.push(1);
while(q.size()){
int k = q.front(); q.pop();
dis[1] = 0;
vis[k] = false;
for(int i = h[k]; i != -1; i = ne[i]) { //遍历与k相连的每一条边
int j = e[i];
if(dis[j] > dis[k] + w[i]) {
dis[j] = dis[k] + w[i];
if(!vis[j]) {
vis[j] = true;
q.push(j);
}
}
}
}
if(dis[n] == 0x3f3f3f3f) return -1;
else return dis[n];
}
int main(){
cin >> n >> m ;
memset(h,-1,sizeof h);
while(m--) {
cin >> x >> y >> z;
add(x,y,z);
}
if(spfa(n) == -1) printf("impossible\n");
else cout << dis[n] << endl;
return 0;
}
3-2、SPFA判断负环
给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数n和m。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
输出格式
如果图中存在负权回路,则输出“Yes”,否则输出“No”。
数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过10000。
输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes
思路:何为负环?即成环且环的总权值为负数。与 求 最 短 路 的 思 路 类 似 , 需 额 外 增 加 数 组 c n t [ x ] , 记 录 起 点 到 x 点 经 过 的 边 数 。
若 c n t [ x ] > = n , 表 示 从 起 点 到 x 点 经 过 了 n 条 边 , 意 味 着 经 过 了 n + 1 个 点 , 由 抽 屉 原 理 , 必 然 有 某 个 点 经 过 了 2 次 。 这 就 说 明 了 图 中 存 在 一 个 负 环 。
与 最 短 路 不 同 的 是 , 这 次 我 们 要 先 将 所 有 点 入 队 。 因 为 负 环 可 能 从 某 个 起 点 出 发 未 必 能 经 过 , 需 要 将 所 有 点 都 当 作 起 点 跑 S P F A 。
Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool spfa()
{
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
st[i] = true;
q.push(i);
}
while (q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
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;
}
4、Floyd(多源最短路)-O(n3)
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出“impossible”。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数n,m,k
接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。
接下来k行,每行包含两个整数x,y,表示询问点x到点y的最短距离。
输出格式
共k行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出“impossible”。
数据范围
1≤n≤200,
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过10000。
输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1
思路:
思 路 十 分 简 单 , 枚 举 中 间 点 k , 更 新 所 有 从 i 到 j 且 经 过 k 的 路 径 的 最 短 距 离 。
同 样 的 , 由 于 存 在 负 权 边 , 若 距 离 仍 大 于 + ∞ 2 , 则 认 为 两 点 之 间 不 连 通 。
#include<iostream>
#include<queue>
#include<cstring>
#define inf 0x3f3f3f3f
using namespace std;
const int N=210;
int n,m,q,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()
{
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;
for(int i=0;i<m;i++)
{
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;
}
对比4种算法
结点n,边m | 边权值 | 选用算法 |
---|---|---|
n<200 | 允许有负 | floyd |
n*m<1e7 | 允许有负 | bellman-ford |
更大 | 有负 | spfa |
更大 | 无负 | dijkstra |