最短路总结--模板及例题

本文总结了图论中的四种经典最短路算法:Floyd、Dijkstra、Bellman-Ford和SPFA。Floyd适用于解决任意两点间最短路,Dijkstra适合单源点最短路,但无法处理负权;Bellman-Ford能处理负权,但可能有负环问题;SPFA是Bellman-Ford的优化,但比赛中易被卡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

最短路是图论中的一种常见题目,通常用邻接表邻接矩阵来储存,用邻接矩阵存可能会因为空间超大而不可取,但更加方便,具体选择要根据题目要求来

1.Floyd算法

弗洛伊德(floyd)算法是一种用来解决任意两点之间的最短路的算法,以动态规划的思路来进行遍历

我们定义一个数组dp[k][i][j]来表示第i个点到第j个点的小于等于点k标号的中转点时的最短路,如果从i到k再从k到j的两条路之和比目前从i到j的最短路更短,那么就更新dp[k][i][j]

在这里插入图片描述

	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++){
				if(G[k][i][j]>G[k-1][i][k]+G[k-1][k][j])
				{
					G[k][i][j]=G[k-1][i][k]+G[k-1][k][j];
				}
			}
		}
	}

同时,因为第k个点总是由k-1个点的来,所以第一位可以省略变为如下代码

	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++){
				if(G[i][j]>G[i][k]+G[k][j])
				{
					G[i][j]=G[i][k]+G[k][j];
				}
			}
		}
	}

在输出路径时,可以用一个数组来记录当前点的前驱,最后递归输出即可
例题
因为数据较小,所以用 f l o y d floyd floyd也可以解决

#include<cstdio>
#include<iostream>
using namespace std;
int G[505][505],prev[505][505],n,m,s,e;
void print(int x)//递归输出
{
	if (prev[s][x]==0) {//如果已经枚举到了s点
		printf("%d",s);//输出s
		return ;//退出
	}
	print(prev[s][x]);//否则就递归
	printf(" %d",x);//输出上一个点的前驱
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
	    {
	    	G[i][j]=0x3f3f3f3f;//把没有连接的两个点的距离赋为无穷大
		}
		G[i][i]=0;
		prev[i][i]=0;
	}
	for(int i=1;i<=m;i++)
	{
		int x,y,w;
		scanf("%d%d%d",&x,&y,&w);
		G[x][y]=w;	
		prev[x][y]=x;//y的前驱为x
   }
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++){
				if(G[i][j]>G[i][k]+G[k][j])//如果有更短的路
				{
					G[i][j]=G[i][k]+G[k][j];//更新路的长度
					prev[i][j]=prev[k][j];//更新前驱,注意,这里不能直接赋为k,因为k是枚举1到n任意的一个点,可能不是j的前驱
				}
			}
		}
	}
	scanf("%d%d",&s,&e);
	printf("%d\n",G[s][e]);//输出最短路
	print(e);//输出路径
	return 0;
}

赠加:无向图的最小环问题
题目大意:给定一个无向图,寻找最小环的权值之和
从题目中可以发现一看到n<=100直接无脑floyd,我们枚举到k点时,前面的k-1个点的最小值都已经更新,那么从前面的k-1个点中枚举两个点i,j来更新最小环的权值(长度最小的环由k和与之相连的i,j组成,要找更长的环,其实就是i或j的最短路加上那个点到i或j的最短路,所以只用枚举i,j即可)
然后再更新一下最短路就行了

#include<cstdio>
#include<iostream>
using namespace std;
const int inf=0x3f3f3f3f;
int n,m,ans=inf,dis[150][150],a[150][150];
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			dis[i][j]=0x3f3f3f3f;
			a[i][j]=0x3f3f3f3f;
		}
		a[i][i]=0;
		dis[i][i]=0;
	}
	for(int i=1;i<=m;i++)
	{
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		dis[u][v]=min(dis[u][v],w);//判断重环
		dis[v][u]=min(dis[v][u],w);//判断重环
		a[u][v]=min(a[u][v],w);//判断重环
		a[v][u]=min(a[v][u],w);//判断重环
	}
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<k;i++)
		{
			for(int j=i+1;j<k;j++)
			{
				ans=min(ans,dis[i][j]+a[i][k]+a[k][j]);//更新最小环的权值和
			}
		}
		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[j][i]=dis[i][j];
			}
		}
	}
	if(ans==inf)
	{
		printf("No solution.");
		return 0;
	}
	printf("%d\n",ans);
	return 0;
}

2.dijkstra算法

迪杰斯特拉(dijkstra)算法是求指定起点(单源点)的最短路问题的常用办法,思想有点类似于bfs
dijstra算法的原理是先把所有点的距离赋为最大值,然后通过从当前起点开始,暴力枚举与其相邻且未被访问过的结点,算出这些边中的最小值,将起点赋为那个结点,标记为已访问,再进行操作,直到找到终点到起点的最短路,在四种算法中最常用,但是无法跑负权图

其中,一种被称为”松弛操作“的技巧常用于dijkstra算法当中,思想就是:如果从一个点的前驱走到此时我们找到的点再走到这个点,离源点的距离比目前这个点的最短路更小,就把它的值修改成上述情况(就像把一根绳子拉的更松,距离更短)

	for(int j=1;j<=n;j++)
		{
		dis[j]=min(dis[j],dis[k]+w[k][j]);//k为目前离源点最近的结点
		}

例题

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
int n,m,dis[2505],w[2505][2505],s,e;
bool vis[2505];
void dijkstra()
{ 
    memset(vis,0,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
	for(int i=1;i<=n-1;i++)//枚举所有的结点
    {
    	int k=0;
    	for(int j=1;j<=n;j++)
    	{
            if(!vis[j]&&(k==0||dis[j]<dis[k]))//如果没有被访问过并且距离更短
            {
            	k=j;//更新为结点j
			}
		}
		if(!k)//如果已经被访问过
		{
			continue;
		}
		vis[k]=1;//k标记为已走
		for(int j=1;j<=n;j++)
		{
		dis[j]=min(dis[j],dis[k]+w[k][j]);//“松弛”操作
		}
	}
}
int main()
{
	scanf("%d%d%d%d",&n,&m,&s,&e);
	memset(w,0x3f,sizeof(w));
	for(int i=1;i<=n;i++)
	{
		w[i][i]=0; 
	}
	for(int i=1;i<=m;i++)
	{
		int x,y,zhi;
		scanf("%d%d%d",&x,&y,&zhi);
	    w[x][y]=min(w[x][y],zhi);//x到y的距离
		w[y][x]=w[x][y];//无向图是双向边
	}
    dijkstra();
    printf("%d\n",dis[e]);
    return 0;
}

优先队列优化版

#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;

const int MAXN = 2505;
int n, m, s, t;
int vis[MAXN], dis[MAXN];

struct edge {
    int v;
    int w;
    edge() {}
    edge(int V, int W) { v = V, w = W; }
};

struct node {
    int u;
    int dis;
    node() {}
    node(int U, int D) { u = U, dis = D; }
    friend bool operator < (node x, node y) { return x.dis > y.dis; }
};

vector<edge> G[MAXN];

int dijkstra(int S, int T) {
    memset(dis, 0x3f, sizeof dis);
    priority_queue < node > Q;
    dis[S] = 0;
    Q.push(node(S, 0));
    while (!Q.empty()) {
        int u = Q.top().u;
        Q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (int i = 0, v, w; i < G[u].size(); i++) {
            v = G[u][i].v, w = G[u][i].w;
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                Q.push(node(v, dis[v]));
            }
        }
    }
    return dis[T];
}

int main() {
    scanf("%d %d %d %d", &n, &m, &s, &t);
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d %d %d", &u, &v, &w);
        G[u].push_back(edge(v, w));
        G[v].push_back(edge(u, w));
    }
    printf("%d", dijkstra(s, t));
    return 0;
}

为什么dijkstra算法不能跑负权值呢?
如下面这张图
在这里插入图片描述

按照我们的思路,结果会先从上方遍历1-2-3的结点,总长为1+2=3.但是,从下面走的路径是3-100=-97!明显比3更小,所以当出现负权值时,dijkstra算法是处理不了的。否则还要Bellman和SPFA干什么

Bellman-Ford算法

贝尔曼福特(Bellman-Ford)是另一种用来解决单源点最短路的算法,思想是对所有点进行“松弛操作”,可以用在出现负权的图中,但是如果存在负环,环中的边权会被松弛地越来越小,所以再用一层循环来判断是否有边仍然不满足三角形不等式(用来松弛的两边大于备松弛的边),如果存在,那么就有负环
路径输出和floyd同理

用两重循环,第一重循环枚举点的个数-1(起点不包括),第二重枚举边的数量,将所有边进行松弛,最后再判断负环。特别的,如果一个点的起点就是源点,它到源点的最短路就是它的边权
例题

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
int n,m,s,t,dis[50005],prev[50005];
struct ren{
	int u,v,w;
}a[50005];
void print(int x)//路径输出
{
	if(!prev[x])//如果已经到了原点
	{
		printf("%d",s);
		return;
	}
	print(prev[x]);
	printf(" %d",x);
}
bool bellmanford(int s)
{	
    memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=m;i++)
	{
		if(a[i].u==s)
		{
			dis[a[i].v]=a[i].w;
		}
	}
	for(int i=1;i<=n-1;i++)//对全部边进行松弛
	{
		for(int j=1;j<=m;j++)
		{
	        if(dis[a[j].v]>dis[a[j].u]+a[j].w)
	        {
	        	dis[a[j].v]=dis[a[j].u]+a[j].w;
	        	prev[a[j].v]=a[j].u;//改变前驱
			}
		}
	}
		for(int j=1;j<=m;j++)//判断负环
		{
			if(dis[a[j].v]>dis[a[j].u]+a[j].w)
			{
				return false;
			    break;
			}
		}
		return true;
}
int main()
{

	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&a[i].u,&a[i].v,&a[i].w);
		prev[a[i].v]=a[i].u;
	}
	scanf("%d%d",&s,&t);
	prev[s]=0;
	dis[s]=0;
	if(!bellmanford(s))//有负环
	{
	    printf("No Solution\n");
	}
	else{
		printf("%d\n",dis[t]);
		print(t); 
	}
	return 0;
}

但是bellman有个很大的问题在于它有可能去松弛的边很多都是无用的,如果输入顺序是从终点到起点,那么bellman需要跑两次m才能松弛完所有边,并且有的边可能根本和终点没有关系,大大地损耗了时间,所以才会诞生SPFA算法

SPFA算法

SPFA其实就是贝尔曼福特算法的队列优化,每次只取出与这个点相邻接的点进行松弛,判断负环的方法就是计算每个点入队的次数,一个点在无负环的情况下最多与所有其他点相连,最多入队n次,如果次数大于了n,说明它在负环内,返回false

请注意,SPFA在很多比赛中容易被卡,若非有负权尽量用dijkstra,但是跟bellman比起来要更优一些

#include<cstdio>
#include<iostream>
#include<queue>
#include<vector>
#include<cstring>
using namespace std;
int n,m,dis[100005],cnt[100005];
bool vis[100005];
struct ren{
	int v,w;
	ren(){};
	ren(int V,int W)
	{
		v=V;
		w=W;
	}
};
queue<int> q;
vector<ren> G[100005];
void add(int x,int y,int z)//加边操作
{
	G[x].push_back(ren(y,z));
}
bool SPFA(int S, int T) {
	memset(dis, 0x3f, sizeof dis);
	q.push(S);
	dis[S] = 0, vis[S] = 0, cnt[S] = 1;
	while (!q.empty()) {
	int u=q.front(); q.pop();
	vis[u]=0;
	for (int i = 0, v, w; i < G[u].size(); i++) {
	v=G[u][i].v,w=G[u][i].w;
	if (dis[v] > dis[u] + w)//松弛
	{
	dis[v] = dis[u] + w;
	if (!vis[v]) 
	{
		vis[v] = 1, cnt[v]++, q.push(v);
	}
	if(cnt[v] > n) //如果入队次数大于n
	{
	return false;	
	}
	}
  }
}
return true;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int x,y,w;
		scanf("%d%d%d",&x,&y,&w);
		add(x,y,w);//无向图
		add(y,x,w);
	}
	SPFA(1,n);
	if(!SPFA)
	{
		printf("No Solution\n");
	}
	else{
		printf("%d\n",dis[n]);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值