最短路问题
最短路问题可以分为两大类,单源最短路,和多源汇最短路问题。
源点就是起点的意思。
单源最短路又分为存在负权边和不存在负权边的两种情况。
若n为点数,m为边数。
前言
学习笔记,记录一下所学的内容。
一、(朴素)Dijkstra算法
这个算法一般用于稠密图(边多,接近点的个数的图) ,一般使用邻接矩阵存图。作为解决单源最短路问题的算法,其作用或者叫实现的功能就是找到从一个点s出发,到n的最短路。
因此,我们需要一个dis数组存放从s到每个点的最短距离,还需要一个二维数组(以邻接矩阵的方法)来存图,g[i][j]中存放i->j的距离。
因此这个算法的核心就是这两行代码:
t是之前路径最短的点
for(int i=1;i<=n;i++)
dis[i]=min(dis[i],dis[t]+g[t][i]);
用这个简单的图来讲,g[1][2]=2 (图中点1到点2的距离) g[2][3]=1…
dis[1]=0 (dis由g[i][j]+dis之前的值更新而来) dis[2]=2 dis[3]=4
代码中min(dis[3],dis[2]+g[2][3]) 结果为3
在图上就表示 从1->3 直接路径 比间接从1->2->3 的路径要长
所以我们选择更短的路径,这两句代码就这个意思。
例:
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=510;
int n,m;
int g[N][N];//存图
int dis[N];//每个点最短距离
bool st[N];//确定到源点最小的点
int dijkstra()
{
memset(dis,0x3f,sizeof dis);//初始化,各个点的距离定为无穷大
dis[1]=0;//从第一个点开始 第一个点到第一个点距离为0
for(int i=0;i<n;i++)
{
int t=0;//储存当前访问的点
//因为我们从1开始访问,所以t可以初始化成不在1->n内的任何数
for(int j=1;j<=n;j++)//在st为false的情况下 找到dis最小的点
if(!st[j]&&(t==0||dis[t]>dis[j]))//找未遍历的点中路径最短的点
t=j;
st[t]=true;//标记求得最短路径的点
for(int j=1;j<=n;j++)
dis[j]=min(dis[j],dis[t]+g[t][j]);//核心思路
}
if(dis[n]==0x3f3f3f3f) return -1;//若路径为无穷大,代表不能到n
else return dis[n];//dis[n]代表从1到n的最短路径
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b]=min(c,g[a][b]);//这种写法可以防止自环和重边 只留距离最短的边即可
}
cout<<dijkstra()<<endl;
}
这个算法的时间复杂度是O(n^2);
代码很短,分开来看也就只有三部分。
首先 初始化,距离都为正无穷,源点为0。
然后 用t记录当前最短距离,在枚举所有的点,与当前的最短路径进行比较。最后 走一遍循环,看看是否能进行松弛操作。
二、(堆优化)dijkstra算法
由上一部分可以知道,dijkstra算法思想就是,先找到最值,然后更新数组。
而上面我们在搜索时用的是爆搜,时间复杂度比较高。那我们思考下能否优化下我们的搜索操作,我们需要找最小值,然后更新当前点的距离数组。
因此我们可以用堆来进行优化。只需要用小根堆,最小值在根结点,可以直接取,复杂度就能从O(n^2)降低为O(mlogn)。
而堆的实现可以用c++的STL,也可以手写堆。这里我们直接用STL里的优先队列(能用现成的为什么要手写 方便些 )。
我们可以建立一个映射,pair<int,int> 前一个int表示距离,后一个表示点。
这个映射相当于有两个数据类型的结构体。
和上面的dijkstra算法对比,初始化多了一点,需要初始化我们定义的小根堆(优先队列),有这个小根堆后,上面搜索最短距离的点,这个过程就被优化了,只需在堆中取出队头元素即可。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int,int>PII;
const int N=100010;
int n,m;
int h[N],e[N],ne[N],idx;//数组模拟邻接表 h:表头 e:结点的值 ne:指针 idx:结点编号
int w[N],dis[N];//w表示权重,dis表示距离
bool st[N];
void add(int a,int b,int c)//邻接表的插入方法,和链表的头插法差不多
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dijkstra()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;
priority_queue<PII,vector<PII>,greater<PII>>q;//小根堆的定义方法
q.push({0,1});//最开始是距离为0,在第1个点
while(q.size())
{
auto t=q.top();//auto自动识别类型 q是什么类型t就是什么类型
q.pop();//每次记录根节点,然后将其出队
int d=t.first,v=t.second;
if(st[v]) continue;//如果是遍历过的点 直接continue
st[v]=true;
for(int i=h[v];i!=-1;i=ne[i])//邻接表的遍历
{
int j=e[i];
if(dis[j]>d+w[i])//若干j结点的dis>小根堆队头的最短路径+权重
{
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);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
cout<<dijkstra()<<endl;
return 0;
}
这个算法的时间复杂度是O(nm) 适用于稀疏图(边不多的情况)
堆优化版和朴素版差别不大,堆优化版只是在函数中搜索最短路径的时候是在堆中搜索。
三、bellman-ford算法
bellman_ford算法 用于有边数限制的带负权的最短路问题。
例:
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
核心代码是这几行
for(int j=0;j<m;j++)
dis[b]=min(dis[b],p[a]+w);
p数组就是dis数组的备份,为了不改变dis数组中的数据。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=510,M=20010;
int n,m,k;
int dis[N],p[N];
struct edge{
int a,b,w;//a为出点,b为入点,w为权重
}e[M];
void bellman_ford()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;
for(int i=0;i<k;i++)
{
memcpy(p,dis,sizeof dis);//备份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],p[a]+w);
}
}
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++)
{
int a,b,w;
cin>>a>>b>>w;
e[i]={a,b,w};
}
bellman_ford();
if(dis[n]>0x3f3f3f3f/2) puts("impossible");//遍历的时候存在负边权,在松弛操作时,可能会+上负值,不一定等于0x3f3f3f3f,可能稍微小一点
else cout<<dis[n]<<endl;
return 0;
}
四、SPFA算法
SPFA算法适用于无边数限制的最短路问题,它相当于是bellman-ford算法用队列做优化,具体是做什么优化呢,就是我们的bellman-ford算法在遍历的时候会遍历所有的边,但有很多边都没有遍历的必要,我们只需要遍历那些离源点距离变小的边就可以了,当某个点的前驱更新时,这个点若是距离变小,我们就创建队列,让这个点入队。
例:
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。
#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],e[N],ne[N],idx;
int w[N],dis[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++;
}
void spfa()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;
queue<int>q;
q.push(1);
st[1]=true;
while(q.size())//也可以写!q.empty() ,只要队列中不为空就一直循环
{
int t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i])//邻接表的遍历
{
int j=e[i];
if(dis[j]>dis[t]+w[i])
{
dis[j]=dis[t]+w[i];
if(!st[j])
{
q.push(j);
st[j]=true;
}
}
}
}
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
spfa();
if(dis[n]==0x3f3f3f3f) cout<<"impossible"<<endl;
else cout<<dis[n]<<endl;
return 0;
}
此算法时间复杂度一般为O(m)。
五、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]);
意思就是 对于i->j的距离,如果有另一个点k,能使得i->k->j的距离小于i直接到j的距离,就更新d。需要注意的是,k的循环必须在最外层。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=210,INF=1e9;
int n,m,q;
int 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][k]+d[k][j],d[i][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;
}
while(m--)
{
int a,b,w;
cin>>a>>b>>w;
d[a][b]=min(d[a][b],w);
}
floyd();
while(q--)
{
int a,b;
cin>>a>>b;
if(d[a][b]>INF/2) puts("impossible");//如果存在负权边,不存在通路的时候 边的值就接近正无穷 或者稍微小一点
else cout<<d[a][b];
}
return 0;
}
此算法时间复杂度是O(n^3)。
总结
这是以上单源最短路算法的核心代码,可以发现,其算法思想都大同小异,无非就是先找到一个当前最短距离然后枚举与它相连的边看看最短距离和所枚举的边谁更短,更新为最短距离。但是有存在着些区别。
如dijkstra算法的st判断的是到源点最小距离的点,spfa算法中st判断的是当前点是否已在队列中。
bellman-ford算法要遍历所有的边和点,遍历时,把入度的点的距离更新为最小。而spfa算法使用邻接表,只用到了有效的点,直到每个点都是最短距离,每条边最多只遍历一次。
结语
感谢观看,如果有错误欢迎指正。
有借鉴于acwing,这更像我学完后的总结。