Dijkstra以及heap优化详解
博客食用更佳bossbaby's blog
迪杰斯特拉算法是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有向图中最短路径问题。迪杰斯特拉算法主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
引自 百度百科
要求
迪杰斯特拉算法的要求是不能存在负权边(而\(SPFA\)可以),可存在环(而\(SPFA\)则不行)
过程
迪杰斯特拉会从出发点开始枚举每个可到达的点,用这个点去松弛这个点未到达过的可到达的点
1.什么是松弛呢
假设现在有点\(u\),\(v\)(正在搜索u,还未搜索过\(v\)).\(u\)到\(v\)的距离为\(d[u][v]\).目前出发点到\(u\),\(v\)的距离分别是\(dis[u]\),\(dis[v]\).那么如果\(dis[u]+d[u][v]<dis[v]\)(\(dis[v]\)被初始化为\(d[start][v]\)(\(start\)为原点)或\(INF\),\(INF\)通常定义为\(INT\_MAX\))那么就把\(dis[v]\)更新为\(dis[u]+d[u][v]\),并继续搜索.
2.具体搜索方法
我们会先枚举离出发点最近(可能是更新后的),也是\(dis\)值最小,且未访问过的一个节点.然后,进行松弛他的所有可以连接的节点.最后所有点的\(dis\)值就是他到\(start\)(原点)的最短距离.
3.为什么松弛的不仅是未访问过的节点
假设现在正在搜索\(u\),有一个访问过的节点\(v\)可以连通到\(u\).首先,\(dis[v]\)是可能会大于\(dis[u]\)的,应为第一次搜索进入\(v\)的边的权值可能非常大.所以再更新一遍比较稳妥.
4.为什么图不能有负权边
感性理解一下就会发现不行
这样就会改变应有的搜索顺序,使得一些本应该先放问的点后面再访问,而一些次优点被先访问.
这个算法其实有贪心的成分在里面,因为是最短,所以每次都从最短的开始搜索.
而负权边会使得路径一直减小.
5.代码
由于这不是最优的解决方案,可以优化,所以直接写的不是特别详细,可以看百度百科
邻接矩阵
#include<bits/stdc++.h>
#define MAXN 100000
using namespace std;
int d[MAXN][MAXN],dis[MAXN];//d为邻接矩阵,dis为每个点到原点的最小距离,d不连通为INF(=INT_MAX或memset为0x3f)
bool vis[MAXN];//vis标记访问过的节点
int n;//点个数
//省去输入输出
void dij(int s/*出发点*/){
memset(vis,0,sizeof(vis));//初始化
vis[s]=1;//原点标记已经搜索过,搜索在下一行
for(int i=1;i<=n;i++)dis[i]=d[s][i];//搜索原点
dis[s]=0;//原点到原点距离为0
for(int i=1;i<n;i++){//注意,进行n-1次
int mindis=MAX_INT;//当前未搜索过的最小节点的离原点的距离
int current;//当前未搜索过的最小节点
for(int j=1;j<=n;j++){
if(!vis[j]&&mindis>dis[j]){
mindis=dis[j];
current=j;//更新
}
}
vis[current]=1;//标记已经搜索过
for(int j=1;j<=n;j++){
dis[j]=min(dis[current]+d[current][s],dis[j]);
}
}
}
//完成
邻接表
//参见上面自行修改...能学dij的应该这个都会吧...
优化
大家想一想,每次都寻找最小的,是不是有点浪费时间,不如每次用堆(\(heap\))维护起来,每次更新一个点后就把它扔进堆里面,每次取最上面的.而假如这个点已经又被更新过了一次,就跳过.如何判断呢?每次把这个点的编号和\(dis\)值扔进去,取出时如果遇见丢进取得\(dis\)与现在的\(dis\)不同,就\(continue\),因为在更新时这个点又被扔进过堆里了.所以不用担心漏过.这样还有一个好处,就是不用存储\(vis\),即是否访问过,所有的都在堆里面了.
堆怎么写
别问我QWQ,我也不会,直接用\(STL\)的\(priority\_queue\)
代码
既然没有什么问题,就上代码了.
//使用的是c++11
//复杂度 O(nlogn)
//比较与SPFA O(m/*边*/)
//更适合稠密图
#include<bits/stdc++.h>
#define INF LLONG_MAX
using namespace std;
using pii=pair<int,long long>;
using ll=long long;
int n;
ll d[1010][1010];
ll dis[1010];
int cnt[1010];//每个点连接的边的个数
pii e[1010][1010];//邻接表
int dij(int s/*出发点*/){
memset(dis,0x3f,sizeof(dis));//初始化dis
priority_queue<pii,vector<pii>,greater<pii>>q;//定义堆
dis[s]=0;//初始化
q.push(make_pair(s,0));
while(!q.empty()){//只要堆里有货就搜索
int u=q.top().first;//取出节点编号
ll d=q.top().second;//取出节点dis值
q.pop(); //弹出
if(dis[u]!=d)continue;//判断有没有被再次更新,如果更新就证明已近搜索过了(为什么是已近搜索过了呢?因为假如被更新了,dis肯定是被更新小了的所以一定会在前面被优先搜索过一次)
for(int v=1;v<=n;v++){//松弛可到达的节点并扔进堆里继续更新
if(dis[u]+d[u][v]<dis[v]){
dis[v]=dis[u]+d[u][v];
q.push(make_pair(v,dis[v]));
}
}
}
//完
}
//读入为邻接矩阵,存储为邻接表
int main(){
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
cin>>d[i][j];
e[i][++cnt[i]]=make_pair(j,d[i][j]);
}
dij(1);//dij搜索
return 0;
}
注意事项
重申一边,不能在有负权边的图里用!!!有负权边请用\(SPFA\)!!!有负权边和环就两个都不能用了QWQ