Dijkstra 算法是典型最短路算法,用于计算一个节点到其他所有节点的最短路径
定义:
- 代价:F(n) = g(n)
- g (n) :从起点到节点 n 的代价(距离)
- open list :存放 当前可到达、且未确定最小代价路径的节点
- closed list :存放 已经找到最小代价路径的节点
流程:
- 从起点开始逐步扩展,每一步为一个节点找到代价最小的路径,即:
- 从 open list 选择代价最小的节点,将其收录到 closed list
- 遍历新收录节点的所有可访问邻节点,更新代价
最优性证明(为什么被收录的节点已经找到代价最小的路径?):反证法
特性:
- 完备性:如果在起始点和目标点之间有路径解存在,就一定可以得到解;如果得不到解,就一定说明没有解存在
- 最优性:对于某个评价指标(一般为路径的长度),规划得到的路径是最优的
算法:
将起点放入 open list
执行循环:
- 如果 open list 为空:搜索失败,结束
- 取 open list 中代价(即,g(n))最小的节点(记作 Node1),将其放入 closed list
- 如果节点 Node1 为终点:找到目标路径,结束
- 遍历节点 Node1 的(不在 closed list 中的)邻接节点
- 记当前遍历的邻接节点为 Node2
- 如果节点 Node2 在 open list 中:更新节点 Node2 的代价
- 如果节点 Node2 在 open list 中:计算节点 Node2 的代价,并将其加入 open list
待循环结束,即得到从起点到终点的代价最小路径
# 朴素 Dijkstra
# 邻接矩阵法
采用邻接矩阵 edges 存储边的有关信息,其中,edges [i][j] 表示 i 到 j 的代价,若 edges [i][j] == INT_MAX ,则表示不存在由节点 i 指向节点 j 的边
/* | |
@param: | |
edges: 邻接矩阵 | |
n: 节点数 | |
start: 起始点 | |
@return: | |
起始点到其他点的最小代价 | |
*/ | |
vector<int> dijkstra(vector<vector<int>>& edges, int n, int start) { | |
vector<int> dist(n, INT_MAX); //dist [i] 表示 start 到 i 的最小代价 | |
dist[start] = 0; //start 到自身的代价为 0 | |
vector<int> closedlist(n, 0); //closedlist [i] 表示 i 是否在 closed list 中 | |
for (int i = 0; i < n; ++i) { // 从起点开始逐步扩展,每一步为一个节点找到代价最小的路径 | |
int idx = 0; // 不在 closed list 中的、代价最小的节点 | |
for (int j = 0; j < n; ++j) { | |
if (!closedlist[j] && dist[j] <= dist[idx]) { | |
idx = j; | |
} | |
} | |
closedlist[idx] = 1; // 将代价最小的节点 idx 添加到 closed list | |
for (int j = 0; j < n; ++j) { // 更新 idx 邻居节点的代价 | |
if (!closedlist[j] && edges[idx][j] < INT_MAX) { | |
dist[j] = min(dist[j], dist[idx] + edges[idx][j]); | |
} | |
} | |
} | |
return dist; | |
} |
# 邻接表法
采用 链式前向星(用数组模拟实现的链表,也就是静态链表)作为邻接表
结构体数组 edges 存储所有边:edges [i] 表示第 i 条边
- edges [i].to 表示第 i 条边的终点
- edges [i].next 表示与第 i 条边同起点的下一条边的编号
- edges [i].w 表示第 i 条边的权值
数组 head 存储顶点的第一条边:head [i] 表示以 i 为起点的第一条边(编号最大的边)的编号
/* | |
@param: | |
edges: 静态链表 | |
head: 节点数组 | |
n: 节点数 | |
start: 起始点 | |
@return: | |
起始点到其他点的最小代价 | |
*/ | |
vector<int> dijkstra(Edge& edges[], vector<int>& head, int n, int start) { | |
vector<int> dist(n, INT_MAX); | |
dist[start] = 0; | |
vector<int> closedlist(n, 0); | |
for (int i = 0; i < n; ++i) { | |
int idx = 0; | |
for (int j = 0; j < n; ++j) { | |
if (!closedlist[j] && dist[j] <= dist[idx]) { | |
idx = j; | |
} | |
} | |
closedlist[idx] = 1; | |
for (int j = head[idx]; j != -1; j = edges[j].next) { // 遍历链式前向星 | |
dist[edges[j].to] = min(dist[edges[j].to], dist[idx] + edges[j].w); | |
} | |
} | |
return dist; | |
} |
# 堆优化 Dijkstra
可以使用堆(优先级队列)来查找 open list 中的代价最小的节点 idx
定义一个小顶堆:由于 pair<typeA, typeB>
排序时会默认按照 typeA
排序,应将起点到节点的代价作为 typeA
、将节点的编号作为 typeB
,以便每次可以从堆中取出代价最小的节点
typedef pair<int, int> pii; // 分别存储代价和节点编号 | |
priority_queue<pii, vector<pii>, greater<pii>> pq; // 小顶堆 |
priority_queue
位于<queue>
头文件,pair
位于<utility>
头文件
# 邻接矩阵法
当探索到一个代价最小的节点 idx 时,会遍历 idx 的邻居节点 i ,如果 dist [idx] + edges [idx][i] < dist [i] ,则需更新节点 i 的代价为 dist [idx] + edges [idx][i]
注意,即便堆中可能已经有 {xx, i} 存在,但由于无法对堆进行寻址访问,故而无法直接将堆中已有的 {xx, i} 更新为
因此,我们采用的策略是:
- 更新 dist [i] 为 dist [idx] + edges [idx][i]
- 将 {dist [i], i} 添加到堆(此时,{dist [i], i} 与 {xx, i} 都存在于堆中,但由于 dist [i] < xx ,{dist [i], i} 优先级高于 {xx, i},因此该操作也等效于更新了堆中的 {xx, i} )
/* | |
@param: | |
edges: 邻接矩阵 | |
n: 节点数 | |
start: 起始点 | |
@return: | |
起始点到其他点的最小代价 | |
*/ | |
vector<int> Dijkstra(vector<vector<int>>& edges, int n, int start) { | |
vector<int> dist(n, INT_MAX); | |
typedef pair<int, int> pii; | |
priority_queue<pii, vector<pii>, greater<pii>> pq; | |
dist[start] = 0; | |
pq.emplace(make_pair(0, start)); | |
while (!pq.empty()) { | |
int idx = pq.top().second; | |
pq.pop(); | |
for (int i = 0; i < n; ++i) { | |
if (edges[idx][i] < INT_MAX) { | |
int cost = dist[idx] + edges[idx][i]; | |
if (cost < dist[i]) { | |
dist[i] = cost; // 实时维护 dist 数组,记录当前搜索到的节点 i 的最小代价 | |
pq.emplace(make_pair(cost, i)); // 可能已经有 {val, i} 在堆中了,但是 val 必然大于 cost | |
} | |
} | |
} | |
} | |
return dist; | |
} |
# 邻接表法
堆优化也可以采用链式前向星结构
/* | |
@param: | |
edges: 静态链表 | |
head: 节点数组 | |
n: 节点数 | |
start: 起始点 | |
@return: | |
起始点到其他点的最小代价 | |
*/ | |
vector<int> dijkstra(Edge& edges[], vector<int>& head, int n, int start) { | |
vector<int> dist(n, INT_MAX); | |
typedef pair<int, int> pii; | |
priority_queue<pii, vector<pii>, greater<pii>> pq; | |
dist[start] = 0; | |
pq.emplace(make_pair(0, start)); | |
while (!pq.empty()) { | |
int idx = pq.top().second; | |
pq.pop(); | |
for (int i = head[idx]; i != -1; i = edges[i].next) { | |
int cost = dist[idx] + edges[i].w; | |
if (cost < dist[edges[i].to]) { | |
dist[edges[i].to] = cost; | |
pq.emplace(make_pair(cost, edges[i].to)); | |
} | |
} | |
} | |
return dist; | |
} |
# 复杂度分析
设图中的节点数为 ,边数为 ,其中
时间复杂度:
- 朴素 Dijkstra 算法:
- 堆优化的 Dijkstra 算法:
空间复杂度:
- 邻接矩阵法:
- 邻接表法:
堆优化的时间复杂度优于朴素算法
邻接表法的空间复杂度优于邻接矩阵法
参考: