目录
序
本蒟蒻今天刚学Dijkstra算法,写一篇文章理理思路。
概念
Dijkstra算法用于求单源最短路(给定一个起点,输出其到多个结点的最短路径)。
像这种带边权的题目,用BFS是不行的,因为BFS搜的是路径最短,如果遇到如下图的情况,那么就会输出错误的结果。
如图,求的是结点A到结点B的最短路径,如果我们用BFS,则它第一次就扩展到了B点,直接选择了边权为10的路径,那么就不对了,还有一条更短的路明摆着呢!
这时候可能有人会说:“这不合常理啊,边长都一样,路也应该一样长啊!”确实,如果我们按照正常思维来讲,当然是不对的,小学生都学过“三角形的两边之和大于第三边”嘛
(我可以骄傲地这样说,是因为我马上就是个初中生了😜)这里引用一下我编程老师的话:“其实我不赞成把边权叫做‘长度’,我觉得应该叫做‘花费’。”所以,如果将边权叫做“花费”,那么就是可以成立的。就类似于这条路很“火”,想在这条路上“游玩”要花的钱就会比较多。
假设有负权边,那么就会出现问题。当存在负权边时,将新节点加入最短路集合的贪心
操作不成立,从而出现错误。这时候就要用到SPFA算法。(还没学,请多指教)
核心步骤
Dijkstra算法的核心思路是如下:
1.在所有未经过向外扩展的结点中找到dis最小点x。
2.对于每条从x出发的边(x,y),对终点y进行“松弛”操作。
“松弛”操作
设dis[v]为起点s到结点v的最短路,对于一条边起点u,终点v,路径长度为w。若dis[u]+w<dis[v],则更新dis[v]=dis[u]+w。操作原理如下图。
如图,可以看出原本直接从s到v的路径较长,然而使用“中转站”会更短。于是将dis[v]更新为设立“中转站”的距离(1+2=3),这本质上是一种贪心的操作。
存图的方法
我将讲解的代码有两个“版本”,一是“原版”,不加任何优化,时间复杂度约为O(n²),为普及组的难度;二是优化版,运用优先队列优化,时间复杂度约为O(mlogn),是提高组的难度。
链式前向星
要存图,最方便的三种方法莫过于邻接矩阵、vector和链式前向星了。下面分析三种方法的利弊。
1.邻接矩阵方法:若由结点u到结点v有一条权重为w的边,那么记edge[u][v]=w。特别地,在无向图中,若结点u与结点v之间有边,则edge[u][v]和edge[v][u]都需要记录,因为双向都有边。
邻接矩阵在遇到稀疏图(边的数目远远小于点的数目的平方的图)时,会造成很多空间的浪费,比较适合存储结点数较少的稠密图。
.
如上图,第一个为无向图,第二个为有向图,可见无向图的邻接矩阵是对称的。
(本图非原创,感谢本图作者,源自百度中搜的图片,原图链接)
2.vector存图:和邻接矩阵类似,只是“随开随用”,不会浪费太多空间。大致思路就是开辟一个一维的vector数组(一维的动态数组等于二维)或套两层动态数组(较麻烦)。例如结点u到结点v有一条边,则执行edge[u].push_back(v)。和邻接矩阵一样,vector在遇到无向图时也需双向建边。
重点来了:vector的缺点:只要边太多,数据稍微比空间大一点点,但是根据vector的内部逻辑,它都会自动给你开双倍空间,导致MLE。各位慎用!
3.链式前向星:据说是由一名山东的老师研究。使用链表的方式,用“头插法”存储每条边。几乎适用于所有场合。这里多来个代码图片哈,链式前向星的一个基础程序如下,代码实现了输入一个有向图,按顺序输出所有边的起点与终点。具体过程可以参考注释,我后面也许会写一篇讲解链式前向星的文章。
#include<bits/stdc++.h>
const int MN=200005;
int n,m,to[MN],head[MN],nxt[MN];
//head[i]:以i点作为起点的最新一条边的编号
//nxt[i]:同起点的i的下一条边的编号
//to[i]:i号边的终点
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
//注意顺序,先连接再改变
nxt[i]=head[x];//创建链表,使最新的边指向同一个起点的边[如果有同一个起点的边,那么插入到前面]
head[x]=i;//以x号起点作为起点的最新一条边为tot号边
// head[x]=i;
to[i]=y;
}
for(int i=1;i<=n;i++){
printf("%d:",i);//以i作为起点
for(int j=head[i];j;j=nxt[j])
printf("%d ",to[j]);
printf("\n");
}
return 0;
}
题目讲解-方法一
头文件及变量定义
#include<bits/stdc++.h>
#define M 500005
int n,m,u,v,w,s,head[10005],to[M],nxt[M],val[M],dis[10005];
bool vis[10005];
定义执行必需的链式前向星数组和dis数组,并用vis数组记录每个店是否“松弛”过。
主函数部分
输入、存图
一个标准的链式前向星存图。
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++){//输入m条边
scanf("%d%d%d",&u,&v,&w);
to[i]=v;//当前i号边尾节点
val[i]=w;//边权
nxt[i]=head[u];//头插法 插向同一结点的最新一条边
head[u]=i;//当前i号边记录为最新一条边
}
初始化
鉴于题目数据数据范围:,我们不能将dis数组初始化为0x3f3f3f3f了,因为还不够大。这里介绍一个简单的方法,不用手打“2147483647”,直接上INT_MAX。
for(int i=0;i<=n;i++)//初始化,题目数据太大,不能赋值为0x3f
dis[i]=INT_MAX;
dis[s]=0;//起点距离自己为0
Dijkstra算法主体
不断取距离的结点进行松弛操作,更新最短距离并标记松弛结束。
while(1){
u=0;//以一个不存在的结点最为最大值
for(int i=1;i<=n;i++)
if(dis[i]<dis[u]&&!vis[i])//没有被扩展过并且最小
u=i;
if(!u)//全部结点都松弛过了,退出
break;
//选出了当前最小距离,以u号结点往外松弛
for(int i=head[u];i;i=nxt[i]){
v=to[i],w=val[i];
if(dis[v]>dis[u]+w)//更新最短距离
dis[v]=dis[u]+w;
}
vis[u]=1;//u号结点松弛结束(所有与它相邻的点)
}
输出
单源最短路就是输出所有结点距离起点的最短距离,这里输出就不用说了吧。
for(int i=1;i<=n;i++)//输出距离起点的距离
printf("%d ",dis[i]);
完整代码
#include<bits/stdc++.h>
#define M 500005
int n,m,u,v,w,s,head[10005],to[M],nxt[M],val[M],dis[10005];
bool vis[10005];
int main(){
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++){//输入m条边
scanf("%d%d%d",&u,&v,&w);
to[i]=v;//当前i号边尾节点
val[i]=w;//边权
nxt[i]=head[u];//头插法 插向同一结点的最新一条边
head[u]=i;//当前i号边记录为最新一条边
}
for(int i=0;i<=n;i++)//初始化,题目数据太大,不能赋值为0x3f
dis[i]=INT_MAX;
dis[s]=0;//起点距离自己为0
while(1){
u=0;//以一个不存在的结点最为最大值
for(int i=1;i<=n;i++)
if(dis[i]<dis[u]&&!vis[i])//没有被扩展过并且最小
u=i;
if(!u)//全部结点都松弛过了,退出
break;
//选出了当前最小距离,以u号结点往外松弛
for(int i=head[u];i;i=nxt[i]){
v=to[i],w=val[i];
if(dis[v]>dis[u]+w)//更新最短距离
dis[v]=dis[u]+w;
}
vis[u]=1;//u号结点松弛结束(所有与它相邻的点)
}
for(int i=1;i<=n;i++)//输出距离起点的距离
printf("%d ",dis[i]);
return 0;
}
题目讲解-方法二
这里我们发现,复杂度主要是由枚举距离最小的结点产生的,因此我们可以在这里用优先队列进行优化,每次压入一个顶点就内部自动排序。代码比较麻烦,需要重载小于运算符。主要改的部分是需要加上结构体,不再需要枚举。代码如下,具体可以参考注释。
#include<bits/stdc++.h>
#define M 500005
using namespace std;
int n,m,u,v,w,s,head[10005],to[M],nxt[M],val[M],dis[10005];
bool vis[10005];
struct node{
int id,dis;//号数,距离
bool operator < (const node &a) const{//重载运算符
return dis>a.dis;
}
};
priority_queue<node>q;//定义优先队列
int main(){
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++){//输入m条边
scanf("%d%d%d",&u,&v,&w);
to[i]=v;//当前i号边尾节点
val[i]=w;//边权
nxt[i]=head[u];//头插法 插向同一结点的最新一条边
head[u]=i;//当前i号边记录为最新一条边
}
for(int i=0;i<=n;i++)//初始化,题目数据太大,不能赋值为0x3f
dis[i]=INT_MAX;
q.push(node{s,0});//压入一个点
dis[s]=0;//起点距离自己为0
while(!q.empty()){
u=q.top().id;//优化,直接用优先队列取最大值
q.pop();
if(vis[u])//已经访问,跳过
continue;
vis[u]=1;//标记已经访问
//选出了当前最小距离,以u号结点往外松弛
for(int i=head[u];i;i=nxt[i]){
v=to[i],w=val[i];
if(dis[v]>dis[u]+w)//更新最短距离
dis[v]=dis[u]+w,q.push(node{v,dis[v]});
}
vis[u]=1;//u号结点松弛结束(所有与它相邻的点)
}
for(int i=1;i<=n;i++)//输出距离起点的距离
printf("%d ",dis[i]);
return 0;
}
总结
感谢大家的观看!有问题可以随时和我反馈,毕竟各位神犇比本蒟蒻厉害多了😁
推荐大家一个临时邮箱吧,比较不错,只是一开始加载较慢:网站
我这里还有挺多好用的资源的,以后每写一篇文章就分享一下吧。
希望各位大神留下一个赞~