最短路径——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
image-20220128085555362
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,提交代码后第二个测试点错误

image-20220128094238453

可以运行第二个测试点,输出最短路径条数为3,而正确答案应该是1

考虑到注1,添加if条件,再次提交,AC

posted @ 2022-01-28 09:36  dctwan  阅读(747)  评论(0编辑  收藏  举报