【算法笔记·图论·Dijkstra】最短路径,包含路径还原

导论:

Dijkstra算法是图论中基础的处理最短路径方法之一。而使用了队列优化的Dijkstra算法的时间复杂度从原来不使用队列优化的O( N 2 N^2 N2)到O( ∣ E ∣ l o g ∣ V ∣ |E|log|V| ElogV)。 1

[1]注解:时间复杂度的中的V代表图中输入的顶点,E代表图中的边。不适用队列优化:使用邻接矩阵实现的算法复杂度是O( V 2 V^2 V2)。使用邻接表的话更新最短距离只需要访问每一条边就可以了,这部分的复杂是O(E),但是每次要枚举所有的顶点来查找下一个使用的顶点,所有最终还是O( V 2 V ^ 2 V2)。

详解算法:

本详细解法为了篇幅,不再使用图例来全流程地模拟过程。您可以使用web上的测试样例,自行去模拟一遍过程。您也可以使用web看别人的博客,来模拟这个过程。

算法核心思想:2

定义概念:
已确认的顶点:就是说从起点到这个点的最短距离就是Dis数组中保存的这个数值。

算法分析:
(1)找到最短距离已经确认的顶点,从它出发更新相邻顶点的最短距离。
(2)未确定的顶点有了距离,这个距离是由最短距离已经确定的顶点转移而来的,此后不需要再次关系“1”中的“最短距离已经确认的顶点”。

在(1)和(2)提到的“最短距离已经确认的顶点”要怎么得到是问题的关键。在最开始的时候,只有其实的起点是确定的最短距离点。而在尚未使用过的顶点中,Dis[i]3数组中距离最小的就是下一个将要被确定下来的顶点。这是因为这张图中所有的边都是正数,所以不太可能在以后的更新中会变得更小。(因为这个点是从已经确认的顶点转移而来的。而未确认的顶点Dis数组中的值都比他大,而且边都是正数。)

[2]注解:本部分参考自秋叶拓哉的《挑战程序设计(第二版)》。
[3]注解:Dis数组中存放的是到这个顶点的最短距离。例如Dis[5]的意思是到5号节点的距离(不一定是正确的,因为5号节点可能并没有被确定,只有确定的顶点Dis数组中存放的才是正确的距离。)

Dijkstra算法的参考程序(C++):4

int cost[MAX_V][MAX_V];//cost[u][v]表示边e=(u,v)的权值(不存在设为INF)
int d[MAX_V];//顶点s出发的最短距离
bool used[MAX_V];//已经确认的顶点
int v;//顶点数目

//求从起点s出发到各个顶点的最短距离
void dijkstra(int s) {
    fill(d, d + V, INF);
    fill(used, used + V, false);
    d[s] = 0;

    while (true) {
        int v = -1;
        //从尚未使用过的顶点中选择一个距离最小的顶点
        for (int u = 0; u < V; u++) {
            if (!used[u] && (v == -1 || d[u] < d[v]))
                v = u;
        }
        if (v == -1)
            break;
        used[v] = true;
        for (int u = 0; u < v; u++) {
            d[u] = min(d[u], d[u] + cost[v][u]);
        }
    }
}

[4]注解:本部分参考自秋叶拓哉的《挑战程序设计(第二版)》。

注意:

Dijkstra不能处理边为负数的情况,但是Bellman算法可以,Bellman不能处理负权回路的问题。

优化程序:

思考:

∣ E ∣ |E| E比较小的时候,大部分的时间都
(1)c.end()函数返回的是指向”最后一个元素+1“的位置,是一个NULL。
(2)reserve的使用需要包含头文件algorithm。且函数是传入的两个参数是左闭右开的,即reserve(a,b)处理的范围是[a,b)。不过正是由于end()函数返回的是最后一个元素的下一个位置,所以下文的代码没有错误。(sort函数同样也是左闭右开)
(3)程序的队列中很可能会存在残留的数据。if (Used[priQueTop.D_x]) continue; 这就是为什么需要这句话的原因。

关键点:

最关键的地方其实就是第(3)点。对于if (Used[priQueTop.D_x]) continue; 的理解请看下图。您最好先翻到本文后面,先把代码阅读一遍,然后再来理解这句话。
在这里插入图片描述
因为有了这句priQue.push(D_N((*ite).vDis, Dis[(*ite).vDis]));。所以对于上图的4号点,如果从<1,4>开始,队列加入了<4号点,距离3>。Dis[4]=3。然后是从<2,4>,队列又加入了<4号点,距离2>。现在队列总共有<4号点,距离2><4号点,距离3>。更新Dis[4]=2。
最终是<3,4>。最终Dis[4]=1。但是优先队列里面却有<4号点,距离1><4号点,距离2><4号点,距离3>其中,优先队列里面只有队头是最新的4号点的状态。后面两个<4号点,距离2><4号点,距离3>是之前残留在队列里面的,不应该被计算。所以当我们计算过一次4号点,那么后面的两个就根本不需要再计算。所以加上if (Used[priQueTop.D_x]) continue;这句话就可以避免使用队列里面的残留数据。

队列优化和路径还原的参考程序:

// Dis队列优化算法
#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>
#define Max_N 10002
#define INF 9999999
using namespace std;
int N, E, Dis[Max_N] = {0}, Used[Max_N] = {0}, Pre[Max_N] = {0};
class P {
public:
    int vDis, vDisSum;
    P(int a, int b) : vDis(a), vDisSum(b){};
    bool operator<(const P& cmp) { return vDis > cmp.vDis; }
};
class D_N {  //维护Dis数组,和Dis数组同步更新
public:
    int D_x, Num;
    D_N(int a, int b) : D_x(a), Num(b){};
    friend bool operator<(D_N a, D_N b) { return a.Num > b.Num; }
};
vector<P> mPoint[Max_N];
void Disj(int Start) {
    fill(Dis, Dis + N + 1, INF);
    priority_queue<D_N> priQue;
    priQue.push(D_N(Start, 0));
    Dis[Start] = 0;
    while (!priQue.empty()) {
        D_N priQueTop = priQue.top();
        priQue.pop();
        if (Used[priQueTop.D_x])
            continue;
        Used[priQueTop.D_x] = 1;
        vector<P>::iterator ite;
        for (ite = mPoint[priQueTop.D_x].begin(); ite != mPoint[priQueTop.D_x].end(); ite++) {
            if (Dis[(*ite).vDis] > Dis[priQueTop.D_x] + (*ite).vDisSum) {
                Dis[(*ite).vDis] = Dis[priQueTop.D_x] + (*ite).vDisSum;
                priQue.push(D_N((*ite).vDis, Dis[(*ite).vDis]));
                Pre[(*ite).vDis] = priQueTop.D_x;  //做了一个路径的还原,保存前驱节点
            }
        }
    }
}
void PrintRoad(int X) {  //路径还原
    vector<int> temp;
    vector<int>::iterator ite;
    while (1) {
        temp.push_back(X);
        if (X == 1)
            break;
        X = Pre[X];
    }
    reverse(temp.begin(), temp.end());
    for (ite = temp.begin(); ite != temp.end(); ite++)
        cout << *ite << " ";
}
int main() {
    printf("这个程序默认从点1开始,输出到所有点的最短距离\n输入顶点和边\n");
    scanf("%d%d", &N, &E);
    printf("输入边的信息,按照u v和d的顺序输入,d是u和v之间的距离:\n");
    for (int i = 0; i < E; i++) {
        int u, v, d;
        scanf("%d %d %d", &u, &v, &d);
        mPoint[u].push_back(P(v, d));
        mPoint[v].push_back(P(u, d));
    }
    Disj(1);
    for (int i = 1; i <= N; i++)
        if (Dis[i] == INF)
            printf("不可达 ");
        else
            printf("%d ", Dis[i]);
    printf("\n现在Dis数组输出完毕,如果想查询1到某个顶点的路径请输入这个顶点,然后按下回车。\n");
    int X;
    scanf("%d", &X);
    PrintRoad(X);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值