最短路径——Dijkstra堆优化算法
最短路径——Dijkstra算法
转自:https://www.cnblogs.com/fusiwei/p/11390537.html
DIJ算法的堆优化
DIJ算法的时间复杂度是O(n2)的,在一些题目中,这个复杂度显然不满足要求。所以我们需要继续探讨DIJ算法的优化方式。
堆优化的原理
堆优化,顾名思义,就是用堆进行优化。我们通过学习朴素DIJ算法,明白DIJ算法的实现需要从头到尾扫一遍点找出最小的点然后进行松弛。这个扫描操作就是坑害朴素DIJ算法时间复杂度的罪魁祸首。所以我们使用小根堆,用优先队列来维护这个“最小的点”。从而大大减少DIJ算法的时间复杂度。
堆优化的代码实现
说起来容易,做起来难。
我们明白了上述的大体思路之后,就可以动手写这个代码,但是我们发现这个代码有很多细节问题没有处理。
首先,我们需要往优先队列中push最短路长度,但是它一旦入队,就会被优先队列自动维护离开原来的位置,换言之,我们无法再把它与它原来的点对应上,也就是说没有办法形成点的编号到点权的映射。
我们用pair解决这个问题。
pair是C++自带的二元组。我们可以把它理解成一个有两个元素的结构体。更刺激的是,这个二元组有自带的排序方式:以第一关键字为关键字,再以第二关键字为关键字进行排序。所以,我们用二元组的first位存距离,second位存编号即可。
然后我们发现裸的优先队列其实是大根堆,我们如何让它变成小根堆呢?
有两种方法,第一种是把第一关键字取相反数,取出来的时候再取相反数。第二种是重新定义优先队列:
priority_queue<int,vector<int>,greater<int> >q;
解决了这些问题,我们愉快地继续往下写,后来我们发现,写到松弛的时候,我们很显然要把松弛后的新值也压入优先队列中去,这样的话,我们又发现一个问题:优先队列中已经存在一个同样编号的二元组(即第二关键字相同),我们没有办法删去它,也没有办法更新它。那么在我们的队列和程序运行的时候,一定会出现bug。
怎么办呢??
我们在进入循环的时候就开始判断:如果有和堆顶重复的二元组,就直接pop掉,成功维护了优先队列元素的不重复。
所以我们得到了堆优化的代码:
priority_queue<pair<int,int> >q;
void dijkstra(int start)
{
memset(dist,0x3f,sizeof(dist));
memset(v,0,sizeof(v));
dist[start]=0;
q.push(make_pair(0,start));
while(!q.empty())
{
while(!q.empty() && (-q.top().first)>dist[q.top().second])
q.pop();
if(!q.empty())
return;
int x=q.top().second;
q.pop();
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(dist[y]>dist[x]+val[i])
{
dist[y]=dist[x]+val[i];
q.push(make_pair(-dist[y],y));
}
}
}
}
UPD:2020.10.28
现在又回头来看这个模板,还是觉得很麻烦的。至少很多东西实现的时候很是繁琐。
其实我们完全可以使用标记数组来避免重复关键字的多次更新。
所以我们得到了新的Dijkstra模板。
void dijkstra()
{
memset(dist,127,sizeof(dist));
dist[1]=0;
q.push(make_pair(0,1));
while(!q.empty())
{
int x=q.top().second;
q.pop();
if(v[x])
continue;
v[x]=1;
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(dist[y]>dist[x]+val[i])
{
dist[y]=dist[x]+val[i];
q.push(make_pair(-dist[y],y));
}
}
}
}
思考
以下为笔者自己的思考
参考《挑战程序设计竞赛》中的代码,写出代码如下,在优先级队列中没有使用pair而是定义了结构体Point,并重载了小于号
const int MAXN = 101;
const int MAXM = 3010;
const int INF = 1e5+10;
struct Edge{
int to;
int length;
Edge(int t, int l): to(t), length(l) {}
};
vector<Edge> graph[MAXN*MAXN];
struct Point{
int num; //顶点编号v
int length; //dist[v]
Point(int n, int l): num(n), length(l) {}
bool operator < (const Point& p) const{
return length > p.length;
}
};
int dist[MAXN];
void Dijkstra(int start){
//初始化
fill(dist, dist + MAXN, INF);
dist[start] = 0;
//默认是大根堆,由于Point结构体重载了小于号,定义dist小的优先级高
//因此,dist小的会被调整在堆顶
priority_queue<Point> q;
q.push(Point(start, dist[start]));
while(!q.empty()){
Point p = q.top(); //获得堆顶元素
q.pop();
int u = p.num; //u为堆顶元素的结点编号
//!!!注1
if(dist[u] < p.length){
continue;
}
//遍历u的所有临界边
for(int i = 0; i < graph[u].size(); ++i){
Edge e = graph[u][i];
//松弛操作
if(dist[u] + e.length < dist[e.to]){
dist[e.to] = dist[u] + e.length;
q.push(Point(e.to, dist[e.to]));
}
}
}
}
注1:
在初读时,不能理解算法下面这条if语句的作用
if(dist[u] < p.length){
continue;
}
直到读到了上面这位大佬的博客
写到松弛的时候,我们很显然要把松弛后的新值也压入优先队列中去,这样的话,我们又发现一个问题:优先队列中已经存在一个同样编号的二元组(即第二关键字相同)
优先级队列中记录的顶点的编号及其在dist中的值,对于一个顶点,在进行疏松操作之后,要将其压入到优先级队列中,但是在优先级队列中可能存在该顶点此次疏松操作之前的记录。在仅仅只求最短路径长度的情况下,不考虑这一点貌似也不会出现问题。但是当求最短路径的条数时,如果忽略这点可能会出现问题,用下面的列子来理解一下。
根据堆优化的最短路径Dijkstra算法,添加num数组即可求得源点到各顶点最短路径的条数。
初始时num[start] = 1,num[v] = 0 (v = {V - start})
若dist[u] + w(u,v) < dist[v],则num[v] = num[u]
若dist[u] + w(u,v) = dist[v],则num[v] += num[u]
代码如下
const int MAXN = 101;
const int MAXM = 3010;
const int INF = 1e5+10;
struct Edge{
int to;
int length;
Edge(int t, int l): to(t), length(l) {}
};
vector<Edge> graph[MAXN*MAXN];
struct Point{
int num; //顶点编号v
int length; //dist[v]
Point(int n, int l): num(n), length(l) {}
bool operator < (const Point& p) const{
return length > p.length;
}
};
int dist[MAXN];
int num[MAXN];
void Dijkstra(int start){
//初始化
fill(dist, dist + MAXN, INF);
memset(num, 0, sizeof(num));
dist[start] = 0;
num[start] = 1;
priority_queue<Point> q;
q.push(Point(start, dist[start]));
while(!q.empty()){
Point p = q.top();
q.pop();
int u = p.num;
//注1
/*if(dist[u] < p.length){
continue;
}*/
for(int i = 0; i < graph[u].size(); ++i){
Edge e = graph[u][i];
if(dist[u] + e.length < dist[e.to]){
dist[e.to] = dist[u] + e.length;
q.push(Point(e.to, dist[e.to]));
num[e.to] = num[u];
}
else if(dist[u] + e.length == dist[e.to]){
num[e.to] += num[u];
}
}
}
}
当没有考虑上述问题时(即注释掉注1
的代码),现在我们考虑下面的输入样例
4 4
1 2 10
1 3 20
2 3 5
3 4 15
1 | 2 | 3 | 4 | |
---|---|---|---|---|
初始 | (0,1) | (inf,0) | (inf,0) | (inf,0) |
松弛1 | (10,1) | (20,1) | (inf,0) | |
松弛2 | (15,1) | (inf,0) | ||
松弛3(15,1) | (30,1) | |||
松弛3(20,1) | (30,2) |
当进行松弛3
的操作时,优先级队列中有两条顶点3的记录
- 松弛顶点1时,push进优先级队列的顶点3记录(20,1)
- 松弛顶点2时,push进优先级队列的顶点3记录(15,1)
因此,在松弛顶点3时,会进行两次松弛操作,第一次确定dist[4]=30,num[4]=1,第二次会导致num[4]=2,显然出现了错误。
解决方法即添加注1
代码
if(dist[u] < p.length){
continue;
}
每当从优先级队列中选取堆顶元素之后进行判断,若取出来的堆顶记录中dist[v](即p.length)大于当前dist[v],则此顶点是重复出现的顶点,直接continue即可
实战
https://acm.ecnu.edu.cn/problem/1818/
在最短路径长度的基础上输出最短路径的条数
若没有考虑注1
,提交代码后第二个测试点错误
可以运行第二个测试点,输出最短路径条数为3,而正确答案应该是1
考虑到注1
,添加if条件,再次提交,AC