【分类】
1.单源最短路
(1)所有边权都是正数
a.朴素Dijkstra算法 O( n^2 )
b.堆优化版Dijkstra算法 O( mlogn )
(2)存在负边权
a.BellmanFord O( nm )
b.SPFA 一般O( n ),最坏O(nm)
2.多源汇最短路
Floyd O( n^3)
一.单源最短路
【定义】
给定一个带权有向图G=(V,E),其中每条边的权是一个实数。另外,还给定V中的一个顶点,称为源。要计算从源到其他所有各顶点的最短路径长度。这里的长度就是指路上各边权之和。这个问题通常称为单源最短路径问题。
(一)朴素Dijkstra算法
【介绍】
迪杰斯特拉算法(Dijkstra),是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。
【分析】
1.首先初始化各点distance为无穷大,第一个点为0
2.接着for循环,注意第一层循环是从0到n,通过中间点 t 来实现下标和距离的更新
3.还需要一个st数组来标记某点是否到过,记得处理不存在的情况
【代码】
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=510;
int g[N][N];
int d[N];//distance
int n,m;
bool st[N];//标记是否走过
int dijkstra(){
memset(d, 0x3f, sizeof d);
d[1] = 0;//将源点距离初始化为零,其它为无穷大
for (int i = 0; i < n - 1; i ++ ){//从 0 ~ n
int t = -1;//t表示没有确定最短路径的节点中距离源点最近的点
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t==-1||d[t]>d[j]))//当前点没有走过且t没有改变或t点的距离不是最小
t = j;//更新下标
for (int j = 1; j <= n; j ++ )//遍历d数组,找到没有确定最短路径的节点中距离源点最近的点t
d[j] = min(d[j], d[t] + g[t][j]);//更新距离
st[t] = true;//标记
}
if (d[n] == 0x3f3f3f3f) return -1;//不存在
return d[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(g, 0x3f, sizeof g);//记得初始化
while (m -- ){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
g[x][y] = min(g[x][y], z);//选择最短距离
}
printf("%d\n", dijkstra());
return 0;
}
(二)堆优化版Dijkstra算法
【分析】
1.朴素算法的主要耗时的步骤是从d数组中选出:没有确定最短路径的节点中距离源点最近的点 t。但我们只是找个最小值而已,没有必要每次遍历一遍d数组。
在一组数中每次能很快的找到最小值,很容易想到使用小根堆。
核心是需要用到优先队列
2.稀疏图,使用邻接表存储。
3.因为pair中默认根据first排序,所以我们需要将距离置于first
typedef pair<int,int> PII;
priority_queue<PII,vector<PII>,greater<PII> > heap;
【代码】
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int,int> PII;//first为距离,second为点
const int N=1e6+10;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];
void add(int x,int y,int z){
e[idx]=y,w[idx]=z,ne[idx]=h[x],h[x]=idx++;
}//邻接表存储
int dijkstra(){
memset(dist,0x3f,sizeof(dist));
dist[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] = true;
for (int i=h[ver];i!=-1;i= ne[i]){
int j=e[i];
if (dist[j]>dist[ver]+w[i]){
dist[j]=dist[ver]+w[i];//更新较短路
heap.push({dist[j],j});
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;//不存在
return dist[n];
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
while(m--){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
cout<<dijkstra()<<endl;
return 0;
}
(三)Bellman-Ford
【原理】
对图进行V-1次松弛操作,得到所有可能的最短路径。
【分析】
1.首先循环k次(限制数),然后枚举每条边(松弛操作)更新最小值。
三角不等式 dist[b]<=dist[a]+w
2.为什么需要backup数组
为了避免出现如下的串联情况, 在边数限制为一条的情况下,节点3的距离应该是3,但是由于串联情况,利用本轮更新的节点2更新了节点3的距离,所以现在节点3的距离是2。
3.为什么是dist[n]>0x3f3f3f3f/2, 而不是dist[n]>0x3f3f3f3f
5号节点距离起点的距离是无穷大,利用5号节点更新n号节点距离起点的距离,将得到109−2109−2, 虽然小于109109, 但并不存在最短路,(在边数限制在k条的条件下)。
4.关于Bellman-Ford算法为什么不能处理负环
因而如果有负权回路,最短路不一定存在。
【代码】
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int const N=510,M=10010;//点和边
int dist[N],backup[N];//backup为一个备份数组
int n,m,k;
struct Edge{
int a,b,w;
}edge[M];//由a指向b权重为w的边
void bellman_ford(){
memset(dist,0x3f,sizeof(dist));
dist[1]=0;//初始化
for (int i=0;i<k;i++){//外循环为0 ~ k ,因为最多只能经过k条边
memcpy(backup,dist,sizeof(dist));//将dist中的数据复制到backup中
for(int j=0;j<m;j++){
auto e=edge[j];
dist[e.b]=min(dist[e.b],backup[e.a]+e.w);//更新最小值(用备份中a的距离加上到当前点的距离)
}//松弛操作
}
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=0;i<m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
edge[i]={a,b,c};//存入数据
}
bellman_ford();
if(dist[n]>0x3f3f3f3f/2) cout<<"impossible";//如果存在负环,最短路径为-无穷,注意这里表示不存在的方式
else printf("%d ",dist[n]);
return 0;
}
【拓展】
memcpy是一个内存拷贝函数,声明在<cstring>中。
函数的功能是从源内存地址的起始位置开始拷贝若干个字节到目标内存地址中。
函数原型 void *memcpy(void *destin, void *source, unsigned n);
destination,source 那就不解释了,,
unsigned n是要复制的字节数
strcpy和memcpy的区别
1.复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2.复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3.用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy。
(四)SPFA
【Blahblah】
SPFA是一个对Bellman-Ford的优化但是长得像Dijkstra的算法。
Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。
SPFA一般情况复杂度是O(m),最坏情况下复杂度和朴素 Bellman-Ford 相同,为O(nm)。
【Tips】
一个小坑:dist[n]的值有可能为-1,所以,不能在函数中直接return -1,要在主函数中进行判断。
【代码】
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N=1e5+10;
int n, m;
int h[N], w[N], e[N], ne[N], idx=1;
int dist[N];
bool st[N];
void add(int x,int y,int z){
e[idx]=y,w[idx]=z,ne[idx]=h[x],h[x]=idx++;
}
void spfa(){
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
queue <int> q;
q.push(1);
st[1]=true;
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];
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
while(m--){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
spfa();
if(dist[n]==0x3f3f3f3f) cout<<"impossible";
else cout<<dist[n];
return 0;
}
【分析】
1.主体和上面区别不大,但是不再需要初始化dist数组,因为判断负环不需要求距离。
2.在上面的基础上,需要再加一个cnt数组用来统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n(即至少经过n+1个点,但是最多只有n个点,所以一定有重复的点),则说明存在环。
3.若dist[j] > dist[t] + w[i],则表示从t点走到j点能够让权值变少,因此进行对该点j进行更新,并且对应cnt[j] = cnt[t] + 1,往前走一步
【Tips】
该题是判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点。
【代码】
#include <bits/stdc++.h>
using namespace std;
const int N=2005,M=10005;
int e[M],ne[M],w[M],h[N],idx;
int dist[N],cnt[N];
bool st[N];
int n,m;
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()) cout<<"Yes";
else cout<<"No";
return 0;
}
二.多源汇最短路
Only.Floyd
【介绍】
设d[k][i][j]表示经过若干个编号不超过k的节点从i到j的最短距离,则可分为从i到k-1的最短距离加上从k到j的最短距离。k必须在外层循环!
状态转换公式:dist[i][j] = min(dist[i][j],dist[i][k] + dist[k][j])
【分析】
1.初始化d
2.k, i, j 去更新d
【代码】
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=210,INF=1e9;
int d[N][N];
int n,m,q;
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(){
scanf("%d%d%d",&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,w;
scanf("%d%d%d",&a,&b,&w);
d[a][b]=min(d[a][b],w);
}
floyd();
while(q--){
int a,b;
scanf("%d%d",&a,&b);
if(d[a][b]>INF/2) cout<<"impossible"<<endl;
else printf("%d\n",d[a][b]);
}
return 0;
}