Dijkstra算法整理
一、Dijkstra’s Algorithm for Adjacency Matrix Representation
使用邻接矩阵存储图,其实就是使用一个二维数组存储图,来实施Dijkatra算法
思路
- 创建一个布尔数组sptSet [ ](最短路径树集),用来记录包含在最短路径树中的顶点。如果sptSet[v]值为true,则顶点v包含在SPT中,否则不包含。最初,这个集合是空的。
- 为图中的所有顶点分配一个距离值,它表示起点(人为规定,记为src)到当前点i的距离值。将所有距离值初始化为 INFINITE 无穷大。将起点src的距离值指定为 0,以便最先将其加入到集合sptSet 中。
- 当sptSet [ ] 不包括所有顶点时:
…… a) 选择一个不在sptSet中,且距离值最小的顶点 u。
…… b) 将 u 添加到sptSet中。
…… c) 遍历所有与 u 相邻的顶点,以更新 u 的所有相邻顶点的距离值。对于 u 的每个相邻的顶点v,如果 u 的距离值和边<u,v>的权重之和小于v的距离值,则更新 v 的距离值。
代码示例
// 使用图的邻接矩阵形式来实现Dijkstra算法
#include <iostream>
using namespace std;
#include <limits.h>
// 图的顶点数
#define V 9
// 从最短路径树集未包含的顶点集合中,找到距离值最小的顶点
int minDistance(int dist[], bool sptSet[])
{
// 初始化最小值
int min = INT_MAX, min_index;
for (int v = 0; v < V; v++)
if (sptSet[v] == false && dist[v] <= min)
min = dist[v], min_index = v;
return min_index;
}
//输出构建好的dist数组。该数组记录了每个顶点与源点之间的最短路径值。
void printSolution(int dist[])
{
cout << "Vertex \t Distance from Source" << endl;
for (int i = 0; i < V; i++)
cout << i << " \t\t" << dist[i] << endl;
}
//使用邻接矩阵表示的图,实现Dijkstra的单源最短路径算法
void dijkstra(int graph[V][V], int src)
{
int dist[V]; //输出数组。dist[i]表示src到i的最短距离
bool sptSet[V]; // 如果顶点i包含在最短路径树集中,或者确定了从src到i的最短距离,sptSet[i]的值将为真
// 初始化所有距离为INFINITE, stpSet[]为false
for (int i = 0; i < V; i++)
dist[i] = INT_MAX, sptSet[i] = false;
// 起点到起点的最短距离是0
dist[src] = 0;
// 为所有顶点找到最短路径
for (int count = 0; count < V - 1; count++) {
// 从尚未处理的顶点集合中选取距离最小的顶点。在第一次迭代中,u总是等于src。
int u = minDistance(dist, sptSet);
// 已经被选进最短路径树集的点,做标记
sptSet[u] = true;
// 更新被选中顶点的相邻顶点的距离值。
for (int v = 0; v < V; v++)
if (!sptSet[v] && graph[u][v] && dist[u] != INT_MAX
&& dist[u] + graph[u][v] < dist[v])
dist[v] = dist[u] + graph[u][v];
}
// 输出源点到每个点的最短路径
printSolution(dist);
}
int main()
{
/* Let us create the example graph discussed above */
int graph[V][V] = { { 0, 4, 0, 0, 0, 0, 0, 8, 0 },
{ 4, 0, 8, 0, 0, 0, 0, 11, 0 },
{ 0, 8, 0, 7, 0, 4, 0, 0, 2 },
{ 0, 0, 7, 0, 9, 14, 0, 0, 0 },
{ 0, 0, 0, 9, 0, 10, 0, 0, 0 },
{ 0, 0, 4, 14, 10, 0, 2, 0, 0 },
{ 0, 0, 0, 0, 0, 2, 0, 1, 6 },
{ 8, 11, 0, 0, 0, 0, 1, 0, 7 },
{ 0, 0, 2, 0, 0, 0, 6, 7, 0 } };
dijkstra(graph, 0);
return 0;
}
时间复杂度
上述Dijkstra算法中执行了两个嵌套while循环,每个循环遍历一遍所有图结点。记 V 为图结点个数,则使用邻接矩阵实现的Dijkstra算法的时间复杂度为 O(V^2).
二、Dijkstra’s Algorithm for Adjacency List Representation
图示理解
给定的源点为 0。
最初,源点的距离值为 0,所有其他点的距离值为 INF(无穷大)。因此从最小堆中提取源点,并更新与 0相邻的点(1 和 7)的距离值。MinHeap 包含除点 0 之外 的所有顶点。绿色的点是已确定最小距离且不在 MinHeap 中的点。

由于点 1 的距离值在 Min Heap 中的所有节点中是最小的,因此从 Min Heap 中提取并更新与 1 相邻的顶点的距离值(如果顶点在 Min Heap 中并且通过 1 的距离小于之前的距离)。最小堆包含除顶点 0 和 1 之外的所有顶点。

从最小堆中继续选择距离值最小的顶点。顶点 7 被选中。所以 MinHeap 现在包含除了 0、1 和 7 之外的所有顶点。更新 7 的相邻顶点的距离值。更新顶点 6 和 8 的距离值(分别为 15 和 9)。

继续选择与最小堆距离最小的顶点。顶点 6 被选中。所以 MinHeap现在包含除了0、1、7、6之外的所有顶点。更新6的相邻顶点的距离值。更新顶点5和8的距离值。

重复上述步骤,直到最小堆不为空。最后,我们得到以下最短路径树。

思路
采用邻接表存储图,实施Dijkatra算法。
与第一种方法相同的是,在 Dijkstra 的算法中需要维护两个集合,一组是已包含在在 SPT(最短路径树)中的顶点列表,另一组是尚未包含的顶点。与第一种方法不同的是,这里使用邻接表表示和存储图,这样可以使用BFS广度优先遍历在 O(V+E) 时间内遍历图的所有顶点,从而降低时间复杂度。
使用BFS 遍历图的所有顶点,并使用最小堆来存储尚未包含在 SPT 中的顶点,或尚未确定最短距离的顶点。最小堆的作用:充当优先队列,从一组尚未包含的顶点中获取最小距离顶点。
对于 Min Heap,extract-min 和 reduction-key value 等操作的时间复杂度为 O(LogV)。
代码思路如下:
- 创建一个大小为 V 的最小堆,其中 V 是给定图中的顶点数。最小堆的每个节点都包含顶点数和顶点的距离值。
- 以源顶点(人为规定)为根,初始化最小堆 MinHeap。其中分配给源顶点的距离值为 0,分配给所有其他顶点的距离值是 INF(无穷大)。
- 当 Min Heap 不为空时:
(1)从 Min Heap 中提取具有最小距离值节点的顶点。让提取的顶点为 u。
(2)对于 u 的每个相邻顶点 v,检查 v 是否在最小堆中。如果v在Min Heap中且距离值大于uv的权重加上u的距离值,则更新v的距离值。
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
//邻接点的结构定义
struct AdjListNode
{
int dest;
int weight;
struct AdjListNode* next;
};
//邻接表的结构定义
struct AdjList
{
// 边表头指针,指向表头
struct AdjListNode* head;
};
//图结构定义。一个图是一个邻接表的数组,数组大小为顶点个数V。
struct Graph
{
int V;
struct AdjList* array;
};
// 创建一个新的邻接表节点
struct AdjListNode* newAdjListNode(
int dest, int weight)
{
struct AdjListNode* newNode =
(struct AdjListNode*)
malloc(sizeof(struct AdjListNode));
newNode->dest = dest;
newNode->weight = weight;
newNode->next = NULL;
return newNode;
}
// 创建包含V个结点的图
struct Graph* createGraph(int V)
{
struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));
graph->V = V;
// 创建邻接表,大小为V
graph->array = (struct AdjList*)malloc(V * sizeof(struct AdjList));
// 初始化每个邻接表为空
for (int i = 0; i < V; ++i)
graph->array[i].head = NULL;
return graph;
}
// 向图中加边
void addEdge(struct Graph* graph, int src,
int dest, int weight)
{
struct AdjListNode* newNode =newAdjListNode(dest, weight);
newNode->next = graph->array[src].head;
graph->array[src].head = newNode;
// 如果是无向图,还需要执行以下插入
newNode = newAdjListNode(src, weight);
newNode->next = graph->array[dest].head;
graph->array[dest].head = newNode;
}
// 最小堆的结点的结构定义
struct MinHeapNode
{
int v;
int dist;
};
// 最小堆结构定义
struct MinHeap
{
// 当前存在的堆结点个数
int size;
// 最小堆容量
int capacity;
// This is needed for decreaseKey()
int* pos;
struct MinHeapNode** array;
};
// 创建一个新的最小堆的堆结点
struct MinHeapNode* newMinHeapNode(int v,
int dist)
{
struct MinHeapNode* minHeapNode =(struct MinHeapNode*)malloc(sizeof(struct MinHeapNode));
minHeapNode->v = v;
minHeapNode->dist = dist;
return minHeapNode;
}
// 创建最小堆
struct MinHeap* createMinHeap(int capacity)
{
struct MinHeap* minHeap =(struct MinHeap*)malloc(sizeof(struct MinHeap));
minHeap->pos = (int*)malloc(capacity * sizeof(int));
minHeap->size = 0;
minHeap->capacity = capacity;
minHeap->array =(struct MinHeapNode**)malloc(capacity *sizeof(struct MinHeapNode*));
return minHeap;
}
// 交换最小堆的两个结点
void swapMinHeapNode(struct MinHeapNode** a,
struct MinHeapNode** b)
{
struct MinHeapNode* t = *a;
*a = *b;
*b = t;
}
// A standard function to
// heapify at given idx
// This function also updates
// position of nodes when they are swapped.
// Position is needed for decreaseKey()
void minHeapify(struct MinHeap* minHeap,
int idx)
{
int smallest, left, right;
smallest = idx;
left = 2 * idx + 1;
right = 2 * idx + 2;
if (left < minHeap->size &&
minHeap->array[left]->dist <
minHeap->array[smallest]->dist)
smallest = left;
if (right < minHeap->size &&
minHeap->array[right]->dist <
minHeap->array[smallest]->dist)
smallest = right;
if (smallest != idx)
{
// The nodes to be swapped in min heap
MinHeapNode* smallestNode =minHeap->array[smallest];
MinHeapNode* idxNode =minHeap->array[idx];
// Swap positions
minHeap->pos[smallestNode->v] = idx;
minHeap->pos[idxNode->v] = smallest;
// Swap nodes
swapMinHeapNode(&minHeap->array[smallest],&minHeap->array[idx]);
minHeapify(minHeap, smallest);
}
}
// 检查给定的minHeap是否为空
int isEmpty(struct MinHeap* minHeap)
{
return minHeap->size == 0;
}
// 从堆中提取最小节点
struct MinHeapNode* extractMin(struct MinHeap*
minHeap)
{
if (isEmpty(minHeap))
return NULL;
// Store the root node
struct MinHeapNode* root = minHeap->array[0];
// Replace root node with last node
struct MinHeapNode* lastNode = minHeap->array[minHeap->size - 1];
minHeap->array[0] = lastNode;
// Update position of last node
minHeap->pos[root->v] = minHeap->size - 1;
minHeap->pos[lastNode->v] = 0;
// Reduce heap size and heapify root
--minHeap->size;
minHeapify(minHeap, 0);
return root;
}
// 使用最小堆的pos[]来获取最小堆中节点的当前索引
void decreaseKey(struct MinHeap* minHeap,
int v, int dist)
{
int i = minHeap->pos[v];
// Get the node and update its dist value
minHeap->array[i]->dist = dist;
// Travel up while the complete
// tree is not hepified.
// This is a O(Logn) loop
while (i && minHeap->array[i]->dist <minHeap->array[(i - 1) / 2]->dist)
{
// 当前结点与其父结点交换
minHeap->pos[minHeap->array[i]->v] =(i - 1) / 2;
minHeap->pos[minHeap->array[(i - 1) / 2]->v] = i;
swapMinHeapNode(&minHeap->array[i],&minHeap->array[(i - 1) / 2]);
i = (i - 1) / 2;
}
}
// 判断当前结点是否在最小堆中
bool isInMinHeap(struct MinHeap* minHeap, int v)
{
if (minHeap->pos[v] < minHeap->size)
return true;
return false;
}
// 输出最短路径的结果
void printArr(int dist[], int n)
{
printf("Vertex Distance from Source\n");
for (int i = 0; i < n; ++i)
printf("%d \t\t %d\n", i, dist[i]);
}
// 计算从src到所有顶点的最短路径距离的主要函数
void dijkstra(struct Graph* graph, int src)
{
int V = graph->V;
int dist[V];
// minHeap represents set E
struct MinHeap* minHeap = createMinHeap(V);
// 初始化最小堆
for (int v = 0; v < V; ++v)
{
dist[v] = INT_MAX;
minHeap->array[v] = newMinHeapNode(v,dist[v]);
minHeap->pos[v] = v;
}
// 将src顶点的dist值设为0,以便首先提取
minHeap->array[src] = newMinHeapNode(src, dist[src]);
minHeap->pos[src] = src;
dist[src] = 0;
decreaseKey(minHeap, src, dist[src]);
minHeap->size = V;
//在接下来的循环中,min堆包含所有最短距离尚未结束的节点。
while (!isEmpty(minHeap))
{
// 提取距离值最小的顶点
struct MinHeapNode* minHeapNode = extractMin(minHeap);
int u = minHeapNode->v;
// 遍历u的所有相邻顶点,并更新它们的距离值
struct AdjListNode* pCrawl = graph->array[u].head;
while (pCrawl != NULL)
{
int v = pCrawl->dest;
// 如果到v的最短距离尚未确定,且通过u到v的距离小于之前计算的距离
if (isInMinHeap(minHeap, v) && dist[u] != INT_MAX && pCrawl->weight + dist[u] < dist[v])
{
dist[v] = dist[u] + pCrawl->weight;
decreaseKey(minHeap, v, dist[v]);
}
pCrawl = pCrawl->next;
}
}
printArr(dist, V);
}
int main()
{
int V = 9;
struct Graph* graph = createGraph(V);
addEdge(graph, 0, 1, 4);
addEdge(graph, 0, 7, 8);
addEdge(graph, 1, 2, 8);
addEdge(graph, 1, 7, 11);
addEdge(graph, 2, 3, 7);
addEdge(graph, 2, 8, 2);
addEdge(graph, 2, 5, 4);
addEdge(graph, 3, 4, 9);
addEdge(graph, 3, 5, 14);
addEdge(graph, 4, 5, 10);
addEdge(graph, 5, 6, 2);
addEdge(graph, 6, 7, 1);
addEdge(graph, 6, 8, 6);
addEdge(graph, 7, 8, 7);
dijkstra(graph, 0);
return 0;
}
时间复杂度
在该算法的dijkstra的函数中,执行了两个嵌套的 while 循环。可以观察到,内部循环中的语句执行了 O(V+E) 次(类似于 BFS广度优先遍历)。另外内部循环有 reduceKey() 操作,需要 O(LogV) 时间。所以总的时间复杂度 = O(E+V)*O(LogV) = O(ELogV)。(V代表图结点数,E代表边数)
三、Dijkstra’s shortest path algorithm using set in STL
思路
使用邻接表的方式在时间复杂度方面更好,但由于手动实现了最小堆(也就是优先队列的功能),因此代码比较复杂。STL 提供了priority_queue,但提供的优先队列不支持减少key和删除操作。在 Dijkstra 的算法中,我们需要一个优先级队列以及对优先级队列的以下操作:
ExtractMin :从所有尚未找到最短距离的顶点中,我们需要得到距离最小的顶点。
DecreaseKey :提取顶点后,我们需要更新其相邻顶点的距离,如果新的距离更小,则更新数据结构中的距离。
上面的操作可以通过c++ STL的set数据结构实现。set保持它所有的key都是有序的,所以最小距离的顶点总是在开始,我们可以从那里提取它,这就是ExtractMin操作,并相应地更新其他相邻的顶点如果任何顶点的距离变小,则删除其先前的条目并插入新的更新条目,即 DecreaseKey 操作。
代码思路:
- 将所有顶点的距离初始化为INF无穷大。
- 创建一个set类型的空集setds,集合中的每一项元素都是一对pair(weight,vertex)。weight表示权重或者距离,vertex表示顶点。把weight放在pair的第一位,是为了令集合中的元素按照weight值来排序。
- 将源点src插入集合setds,并使其距离为 0。
- while(setds不为空):
a) 提取setds中的第一项元素,该元素就是距离值最小的点,记为u。(u = setds.begin().second)
b) 遍历u的所有相邻结点(记为),以更新v最短路径:
if(dist[v] > dist[u] + weight(u, v)) {
从setds中找到顶点v的对应项,删除之;
更新v的距离值:dist[v] = dist[u] + weight(u, v);
将更新后的距离值和点v下标重新以pair形式插入setds,完成更新。(setds.insert(make_pair(dist[v], v)))- 输出距离数组 dist[ ],打印所有的最短路径。
代码示例
#include<bitsstdc++.h>
using namespace std;
# define INF 0x3f3f3f3f
/* 该类用邻接表表示有向图 */
class Graph
{
int V;
list< pair<int, int> >* adj;//加权图需要存储每条边的顶点和权值对
public:
Graph(int V);
void addEdge(int u, int v, int w);
void shortestPath(int s);
};
Graph::Graph(int V)
{
this->V = V;
adj = new list< pair<int, int> >[V];
}
void Graph::addEdge(int u, int v, int w)
{
adj[u].push_back(make_pair(v, w));
adj[v].push_back(make_pair(u, w));
}
/* 执行Dijkstra算法的主函数 */
void Graph::shortestPath(int src)
{
// 创建一个集合来存储正在处理的顶点
set< pair<int, int> > setds;
// 为距离创建一个矢量,并将所有距离初始化为无穷大(INF)
vector<int> dist(V, INF);
// 首先向集合setds中插入起点src,其距离值为0
setds.insert(make_pair(0, src));
dist[src] = 0;
while (!setds.empty())
{
// 集合中的第一个顶点为距离最小顶点,将其从集合中提取出来
pair<int, int> tmp = *(setds.begin());
setds.erase(setds.begin());
int u = tmp.second;//所提取出来的结点的序号为pair的后者(前者为距离值)
// 遍历当前结点的所有相邻结点
list< pair<int, int> >::iterator i;
for (i = adj[u].begin(); i != adj[u].end(); ++i)
{
// 得到u的顶点标签和当前邻边的权值
int v = (*i).first;
int weight = (*i).second;
// 更新最短路径
if (dist[v] > dist[u] + weight)
{
if (dist[v] != INF)
setds.erase(setds.find(make_pair(dist[v], v)));
dist[v] = dist[u] + weight;
setds.insert(make_pair(dist[v], v));
}
}
}
// 输出
printf("Vertex Distance from Source\n");
for (int i = 0; i < V; ++i)
printf("%d \t\t %d\n", i, dist[i]);
}
/* 测试 */
int main()
{
int V = 9;
Graph g(V);
g.addEdge(0, 1, 4);
g.addEdge(0, 7, 8);
g.addEdge(1, 2, 8);
g.addEdge(1, 7, 11);
g.addEdge(2, 3, 7);
g.addEdge(2, 8, 2);
g.addEdge(2, 5, 4);
g.addEdge(3, 4, 9);
g.addEdge(3, 5, 14);
g.addEdge(4, 5, 10);
g.addEdge(5, 6, 2);
g.addEdge(6, 7, 1);
g.addEdge(6, 8, 6);
g.addEdge(7, 8, 7);
g.shortestPath(0);
return 0;
}
时间复杂度
该算法使用了STL中的SET。C++ 中的集合通常使用自平衡二叉搜索树来实现,其插入、删除等集合操作的时间复杂度是对数的,因此该解决方案的时间复杂度为 O(ELogV))。E表示边数,V表示结点数。
四、Dijkstra’s Shortest Path Algorithm using priority_queue of STL
思路
本质上是利用stl中已写好的优先队列,来代替“最小堆”在算法中起到的作用——从一组尚未包含的顶点中获取最小距离顶点。
在上面的第三种方法中,我们使用了SET集合,而集合自身使用的是自平衡二叉搜索树。对于 Dijkstra 算法,始终建议使用堆(或优先队列),因为所需的操作(提取最小值和减少键)与堆(或优先级队列)的特性相匹配。
使用优先队列的问题是:priority_queue 不支持减键(decrease key)。要解决此问题,我们考虑,不是去更新key值,而是再插入一份key的副本。也就是说,允许优先队列中相同顶点存在多个实例。这种方法不需要减键操作,并且具有以下重要属性:
每当一个顶点的距离减少时,我们就会在priority_queue 中再添加一个顶点实例。即使有多个实例,我们也只考虑距离最小的实例,同一顶点的其他实例都可以不要去考虑。