前言
最短路是图论中的一种常见题目,通常用邻接表和邻接矩阵来储存,用邻接矩阵存可能会因为空间超大而不可取,但更加方便,具体选择要根据题目要求来
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;
}