最短路问题详解

最短路梳理

在这里插入图片描述

dijkstra

如果是初学者,建议仔细看一下动画

模板题练手

在这里插入图片描述
输入 5 5
1 2 20
2 3 30
3 4 20
4 5 20
1 5 100
输出
90

【数据规模和约定】 1≤n≤2000 1≤m≤10000 0≤c≤10000

dijkstra+链式前向星 (附注释)

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<iomanip>
#include<stack>
#include<map>
#define TIE ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define N 2010
#define M 10010
#define INF 0x3f3f3f3f
using namespace std;
int n,m,s,vis[N],dis[N];
int head[N],to[M*2],nxt[M*2],val[M*2],idx;
void add(int u,int v,int w){
	to[idx]=v,val[idx]=w,nxt[idx]=head[u],head[u]=idx++;
} 
int dijkstra(){
	memset(dis,0x3f,sizeof dis);//一定不要忘记!否则一直爆0 
	dis[1]=0;//起点到自己距离为0 
	for(int i=1;i<n;++i){
		int t=-1;
		for(int j=1;j<=n;++j) if(!vis[j]&&(t==-1||dis[t]>dis[j])) t=j;//t先被赋值为1(下标),之后在去寻找离自己最近的 
		vis[t]=1;
		for(int j=head[t];~j;j=nxt[j]){
			int y=to[j];
			dis[y]=min(dis[y],dis[t]+val[j]);
		}
	}
	return dis[n];
}
int main(){
	memset(head,-1,sizeof head);
	TIE;cin>>n>>m;
	for(int i=1;i<=m;++i){
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
	}
	int ans=dijkstra();
	if(ans==INF) cout<<"-1"<<"\n";
	else cout<<ans<<"\n";
	return 0;
}


上点难度,嘿嘿~~~

在这里插入图片描述
输入
4 8 2
1 2 4
1 3 2
1 4 7
2 1 1
2 3 5
3 1 2
3 4 4
4 2 3
输出
10

冷静分析:作为一个优秀的蒟蒻,我第一次想跑一遍dijkstra然后把答案*2不就行了 可是这是有向图。。。所以我建了个反图(就是把所有有向边起点和终点调换一下) 那对于每个点,它的距离是back(i)+go(i)
在原图上跑一遍起点为k的最短路dis相当于back(i) 在反图上跑一遍起点为k的最短路dis2相当于go(i)

AC code~~~
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<iomanip>
#include<stack>
#include<map>
#define TIE ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define N 1010
#define INF 0x3f3f3f3f
using namespace std;
int n,m,k,g[N][N],dis[N],vis[N],g1[N][N],dis2[N];
void dijkstra(int g[][N],int x,int dis[]){//传参传数组类似引用即可以改变数组在主函数的值,
	memset(vis,0,sizeof vis);//但除一维可以省略外,其他都得填上范围 
	dis[x]=0;
	for(int i=1;i<n;++i){
		int t=-1;
		for(int j=1;j<=n;++j) if(!vis[j]&&(t==-1||dis[t]>dis[j])) t=j;
		vis[t]=1;
		for(int j=1;j<=n;++j){
			if(g[t][j]){
				dis[j]=min(dis[j],dis[t]+g[t][j]);
			}
		}
	}
}
int main(){
	memset(g,0x3f,sizeof g);
	memset(g1,0x3f,sizeof g1);
	memset(dis,0x3f,sizeof dis);
	memset(dis2,0x3f,sizeof dis2);
	TIE;cin>>n>>m>>k;
	for(int i=1;i<=m;++i){
		int u,v,w;
		cin>>u>>v>>w;
		g[u][v]=min(g[u][v],w);
		g1[v][u]=min(g1[v][u],w);
	}
	dijkstra(g,k,dis);
	dijkstra(g1,k,dis2);
	int ans=0;
	for(int i=1;i<=n;++i){
		ans=max(ans,dis[i]+dis2[i]);
	}
	cout<<ans<<"\n";
	return 0;
}
//在原图上跑一遍起点为k的最短路dis相当于back
//在反图上跑一遍起点为k的最短路dis2相当于go

Bellman_ford


看这样一张图,用dijkstra模拟一下。因为每一轮都会选择距离1最近的点来更新距离,然后dis[2]就光荣的变成了2并被打上了标记,实际上到dis[2]的距离是-4,这就是为什么dijkstra解决不了负权图的原因

!那这根难啃的kun骨头就由bellman_ford来啃!

老规矩先过一遍动画

算法逻辑:

  1. 初始化源点s到各个点v的路径dis[v] = ∞,dis[s] = 0。

  2. 进行n - 1次遍历,每次遍历对所有边进行松弛操作,满足则将权值更新。

    松弛操作(这个东西很有名):以u为起点,v为终点,uv边长度为w为例。dis[u]代表源点s到u点的路径长度,dis[v]代表源点s到v点的路径长度。如果满足下面的式子则将dis[v]更新为dis[u] + w。
    dis[v] > dis[u] + w

  3. 遍历都结束后,若再进行一次遍历,还能得到s到某些节点更短的路径的话,则说明存在负环路
    还有一种方法判断,我们进行松弛操作的时候这个点从INF一定会**不变或变小**,但最多最多也不会超过INF/2,所以一个点的值超过就存在负环

模板题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<iomanip>
#include<stack>
#include<map>
#include<queue>
#define TIE ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define N 510
#define M 10010
#define INF 0x3f3f3f
#define ll long long
using namespace std;
struct node{
    int x,y,z;
}a[M];
int n,m,k,dis[N],cpy[N];
int bellman_ford(){
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    for(int i=1;i<=k;++i){
        memcpy(cpy,dis,sizeof dis);
        for(int j=1;j<=m;++j){
            int x=a[j].x,y=a[j].y,z=a[j].z;
            dis[y]=min(dis[y],cpy[x]+z);
        }
    }
    if(dis[n]>INF/2) return -1;
    else return dis[n];
}
int main(){
    TIE;cin>>n>>m>>k;
    for(int i=1;i<=m;++i) cin>>a[i].x>>a[i].y>>a[i].z;
    int ans=bellman_ford();
    if(ans==-1) puts("impossible");
    else cout<<ans<<"\n";
    return 0;
}

tips:cpy数组备份了一下dis数组的原因是防止发生“串联”(控制迭代次数)

SPFA

对任意一条边 u->v,只有 d[u]值改变时,v 的 d[v]值才有可能被改变,所以用一个queue把值改变的点存起来,就万事大吉了😆

void spfa(){
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	queue<int>q;
	q.push(1);
	st[1]=true;
	while(!q.empty()){
		int t=q.front();
		q.pop();
		st[t]=false;
		for(int i=head[t];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>dist[t]+w[i]){
				dist[j]=dist[t]+w[i];
				if(!st[j]){
					q.push(j);
					st[j]=true;
				}
			} 
		}
	}
}

我们知道如果一个图中不存在负环回路,那么求得的最短路一定是不包含环的简单路径,所
以最短路径边数最多为 n-1 条。我们在 SPFA 的过程中可依据此规律判断图中是否存在负环:

  • 定义 cnt[y] 记录当前从 root 到达 y 节点的最短路的边数
  • 初始每个点的 cnt 为 0
  • 当某条(x,y,z)边可以更新 d[y]的值时,此时的 cnt[y]被更新为 cnt[x]+1,即到达 y 的最短路径条数+1,如果此时的 cnt[y] >= n,则说明该图中一定存在负环:

(由抽屉原理,在 n 个点的条件下,从 1 到 y 至少经过 n 条边时,则说明该路径中至少有 n +1 个点,那么一定有点是重复使用的。这就说明该图中一定存在负环)

bool SPFA(){
    queue<int> q;
    for(int i=1;i<=n;++i){
        q.push(i);
        vis[i]=1;
    }
    while(!q.empty()){
        int x=q.front();
        q.pop();
        vis[x]=0;
        for(int i=head[x];~i;i=nxt[i]){
            int v=to[i];
            if(dis[v]>dis[x]+val[i]){
                dis[v]=dis[x]+val[i];
                cnt[v]=cnt[x]+1;
                if(cnt[v]>=n) return true;
                if(!vis[v]){
                    vis[v]=1;
                    q.push(v);
                }
            }
        }
    }
    return false;
}

然而,SPFA在遇见菊花图那种全是负权回路的毒瘤图就直接凉凉 SPFA已死

Floyd

前面我们学习的算法主要用途都是解决单源最短路问题 如果要求解任意两点之间的
最短路,可以枚举所有源点,对所有点执行单源最短路算法,但这样比RE无疑
更方便的,我们可使用 Floyd 算法~~~

Floyd 是一种动态规划算法,稠密图效果最佳(效率要高于执行 n 次 Dijkstra 算法,也高于执行 n 次 SPFA 算法),边权可正可负

缺点:时间复杂度较高,不适合计算大量数据

void Floyd() {
  for (int k = 1; k <= n; ++k) {//一定要先枚举中介点
    for (int i = 1; i <= n; ++i) {
      for (int j = 1; j <= n; ++j) {
        dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);
      }
    }
  }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值