一、作用和限制
1.作用
只是单源点算法(只获得了所有节点到初始点的最短路径长度,而非图中各节点之间的最短路径长度)
2.限制
- 所有边都有权值。
- Dijkstra算法适用于边权为正的无向和有向图,不适用于有负边权的图!
二、算法思想
1.思想:贪心
每次循环都选择最小的,这就是贪心所在。如果这个问题有最优子结构,那么可以用最优子结构。
贪心的关键就是松弛定理Relax:
δ
(
u
,
v
)
≤
δ
(
u
,
x
)
+
δ
(
x
,
v
)
\delta(u,v) \leq \delta(u,x)+ \delta(x,v)
δ(u,v)≤δ(u,x)+δ(x,v)
松弛定理的数学证明好理解,那看看为什么有用。
比如v5顶点,我们想要获得v1到v5的最短路径。现在我们不从v1走,而是以v5的角度的出发,到v5肯定要从v5的上一个结点走到吧:要么v2,要么v4。
假如你现在已经知道v2和v5、v4和v5的边的权重以及之前的最短路径长度,那么你只要做一个简单的判断:
- 是从v1到v2的最短路径长度加上v2和v5的边的权重大
- 还是从v1到v4的最短路径长度加上v4和v5的边的权重大
所以,你只要简单的做个数学比较就知道该选择v2还是v4,而这个v2或者v4就是中继点。
【这也是如何输出path路径】:
倒着看的话,这样就把要经过很多节点的最后顶点的大问题分成了倒着选择一个好的中继点的更小的问题。而这个问题分到最小,也就是分到开始顶点处。
2.伪码
- 目的:求从单源点s出发到图G中每个顶点的最短路径,结果放入d数组
- 输入:图G, 各权重w, 单源点s
- 输出:数组d
- 其他:轨迹存放在
Π
数组中。
DIJKSTRA(G, w, s):
// 单源点到自身的最短路径为0
d[s] ← 0
// 到其他顶点的最短路径初始化
for each v ∈ V - s{s}
do d[v] ← ∞
Π[v] ← NIL
// S集合内先是空的
S ← ∅
// Q集合内是所有的顶点
Q ← V
// Q集合空则表示算法结束
while Q ≠ ∅
// u是中继点
do u ← EXTRACT-MIN(Q)
// S集合内添加中继点u
S ← S ∪ {u}
// 松弛,就是更新最短路径
// Adj[u]表示点u的邻接边,即选出u邻接的顶点
for each vertex v ∈ Adj[u]
// d[v]表示原来到v的最短路径长度
// d[u]+w(u,v)表示现在通过中继点的路径长度
do if d[v] > d[u] + w(u, v)
then d[v] ← d[u] + w(u, v)
3.算法流程
节点
- 初始点s
- 中继点
- 已求出最短路径的节点
- 未求出最短路径的节点
符号
d数组:记录最短路径长度的数组
两个集合S和U:存储最短路径长度
- S:记录已求出最短路径的节点到初始点的最短路径长度
- U:记录未求出最短路径的节点到初始点的最短路径长度
过程
(1)初始化:
- 源顶点到自身的最短路径为0(
d[s] = 0
),到其他顶点的为正无穷(d[v] ← ∞
) - S集合为空,U集合有全部的顶点
(2)移动节点和更新中继点
- 从U集合中选出最短路径长度最小的节点,将其从U集合中移入到S集合中
- 中继点更新为此节点
(3)更新U中各顶点的最短路径长度(d数组元素值)
- 计算U中的各节点的
新最短路径长度
=S中的由初始点到中继点的最短路径长度
+中继点到U中某邻接节点的距离d(其实就是邻接边的权重)
- 比较U中各节点的
新最短路径长度
是否小于旧最短路径长度
,若小于,则替换更新成新最短路径长度
。
(4)重复
重复(2)和(3),直到U空,即遍历完所有节点,获得了所有节点到初始点的最短路径长度。
4.算法图示
(1)详细图示
- 初始化
- 第一轮完成:移入A到S中,并以A为中继点更新好U中各顶点的d
- 第二轮完成:移入D到S中,并以D为中继点更新好U中各顶点的d
- 第三轮完成:移入B到S中,并以B为中继点更新好U中各顶点的d
- 第四轮完成:移入C到S中,并以C为中继点更新好U中各顶点的d
- 第五轮完成:移入E到S中,并以E为中继点更新好U中各顶点的d
- U空,结束
(2)简单图示
第一行,直接以A为初始点,其他都是∞。
之后的行,将上一行最小的值的点移入S中,并不再写这列,然后以此点为中继点更新剩下的最短路径。
三、时间复杂度
- 时间复杂度
主要是while部分:
T i m e = θ ( V × T EXTRACT-MIN + E × T DECREASE-KEY ) Time = \theta(V \times T_{\text{\tiny EXTRACT-MIN}}+E \times T_{\text{\tiny DECREASE-KEY}}) Time=θ(V×TEXTRACT-MIN+E×TDECREASE-KEY)
T EXTRACT-MIN T_{\text{\tiny EXTRACT-MIN}} TEXTRACT-MIN是找出中继点的代价,因为有V个结点,所以乘V。
T DECREASE-KEY T_{\text{\tiny DECREASE-KEY}} TDECREASE-KEY是松弛的代价,因为有E个边,所以乘E。
四、c++数组实现
#include <iostream>
#include <string.h>
#include <stack>
#include <windows.h>
using namespace std;
// 图中的顶点个数
#define VERTEX_SIZE 6
// 假设的正无穷大
// 但这个数不可使用INT_MAX,因为松弛操作时,"dist[j] = min(dist[j], dist[relay] + Graph[relay][j]);",需要相加,会越界
#define INF 0x3f3f3f3f
// 寻找当前未求出最短路径的顶点集合U中的所有结点中路径最短的顶点下标
// -1表示有向图中无法继续走时的情况(因为可能某个顶点只指向别人的,没有指向其自身的)
int EXTRACT_MIN(int dist[], int isShortest[])
{
// U集合中结点最短的路径长度,初始化为正无穷
int minDist = INF;
// 路径最短的结点的下标
int minDist_index = -1;
// 虽然还是遍历图中的结点,但通过isShortest数组来实现只计算U中的结点
for (int i = 0; i < VERTEX_SIZE; i++)
{
// isShortest数组用来记录结点在S还是在U中,1则在S中,0则在U中
// 选择在U中的顶点
if (isShortest[i] == 0)
{
// 更新更小的路径长度,同时更新路径最短的结点的下标
if (dist[i] < minDist)
{
minDist = dist[i];
minDist_index = i;
}
}
}
return minDist_index;
}
// 输出路径path
void printPath(int source, int destination, int path[VERTEX_SIZE])
{
stack<int> myStack;
/******* 倒着找到中继点 *******/
// 从自身开始
int pre = destination;
while (pre != source)
{
myStack.push(pre);
// 更新为存在到下标pre的顶点的中继点
pre = path[pre];
}
// 可有可无,从源点出发
myStack.push(source);
// 输出路径
while (!myStack.empty())
{
cout << myStack.top() << " ";
myStack.pop();
}
cout << endl;
}
void Djkstra(int Graph[VERTEX_SIZE][VERTEX_SIZE], int dist[VERTEX_SIZE], int path[VERTEX_SIZE], int source)
{
/******** 初始化dist ********/
// 所有不与源点相连的设置为正无穷
fill(dist, dist + VERTEX_SIZE, INF);
// 源点到自身的最短路径为0
dist[0] = 0;
/******** 初始化isShortest ********/
// isShortest数组用来记录顶点在S还是在U中,1则在S中,0则在U中
// 初始化为全0,表示S集合为空,U集合有全部的顶点
int isShortest[VERTEX_SIZE] = {};
/******** 搞定U集合中的顶点,共VERTEX_SIZE个 ********/
for (int i = 0; i < VERTEX_SIZE; i++)
{
// 在U集合中找到中继点,放入S集合
int relay = EXTRACT_MIN(dist, isShortest);
// 当有向图中无法继续走时(因为可能某个顶点只指向别人的,没有指向其自身的)
// 无向图自然不会出现此情况
if (relay == -1)
{
break;
}
// 中继点的最短路径标志设为1,因为要放入S集合
isShortest[relay] = 1;
// 将中继点放入S集合后,更新U集合内的顶点的最短距离
for (int j = 0; j < VERTEX_SIZE; j++)
{
// 选择在U中的顶点
if (isShortest[j] == 0)
{
// 选择中继点的邻接点,不是就是跳过
if (Graph[relay][j] == INF)
{
continue;
}
// 松弛操作:选择更近的路径长度,前者是原来的路径长度,后者是通过中继点的路径长度
if (dist[j] > dist[relay] + Graph[relay][j])
{
dist[j] = dist[relay] + Graph[relay][j];
// 记录到达此顶点的中继点
path[j] = relay;
}
}
}
}
}
// 赋予从startP顶点到endP的权值
void addEdge(int startP, int endP, int weight, int Graph[VERTEX_SIZE][VERTEX_SIZE])
{
Graph[startP][endP] = weight;
// 这个加上就是无向图,不加这个就是有向图
// Graph[endP][startP] = weight;
}
int main()
{
// Graph[i][j]表示顶点i到顶点j的权重
int Graph[VERTEX_SIZE][VERTEX_SIZE];
// 相连的顶点之间是权值,不相连的顶点之间是正无穷大
fill(&Graph[0][0], &Graph[VERTEX_SIZE][VERTEX_SIZE], INF);
// 初始化顶点到自身的为0
for (int i = 0; i < VERTEX_SIZE; i++)
{
Graph[i][i] = 0;
}
// 赋权值
addEdge(0, 1, 10, Graph);
addEdge(0, 5, 3, Graph);
addEdge(1, 2, 7, Graph);
addEdge(1, 3, 5, Graph);
addEdge(3, 0, 3, Graph);
addEdge(3, 2, 4, Graph);
addEdge(3, 4, 7, Graph);
addEdge(5, 1, 2, Graph);
addEdge(5, 3, 6, Graph);
addEdge(5, 4, 1, Graph);
// 输出图
for (int i = 0; i < VERTEX_SIZE; i++)
{
for (int j = 0; j < VERTEX_SIZE; j++)
{
printf("%13d", Graph[i][j]);
}
cout << endl;
}
cout << endl;
// dist[i]表示源顶点到其他顶点的最短距离
int dist[VERTEX_SIZE];
// 最短路径过程
int path[VERTEX_SIZE];
// 源顶点在图中的下标,可以是[0,VERTEX-1],这里我们假定为0
int source = 0;
Djkstra(Graph, dist, path, source);
// 输出最短路径长度
cout << "dist = \n";
for (int i = 0; i < VERTEX_SIZE; i++)
{
cout << dist[i] << endl;
}
cout << endl;
// 输出路径
cout << "path = \n";
for (int i = 0; i < VERTEX_SIZE; i++)
{
printPath(source, i, path);
}
cout << endl;
system("pause");
return 0;
}
/*
0 10 1061109567 1061109567 1061109567 3
1061109567 0 7 5 1061109567 1061109567
1061109567 1061109567 0 1061109567 1061109567 1061109567
3 1061109567 4 0 7 1061109567
1061109567 1061109567 1061109567 1061109567 0 1061109567
1061109567 2 1061109567 6 1 0
dist =
0
5
12
9
4
3
path =
0
0 5 1
0 5 1 2
0 5 3
0 5 4
0 5
*/
Reference
Dijkstra(单源最短路径)
Djkstra最短路径算法的c++代码实现
DijkStra最短路径的C++实现与输出路径