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