基本概念
Dijkstra算法用来解决只含有非负权图的单源最短路径问题(SSSP), 设定一个起始点node
, 计算出该点到其他顶点的最短距离, 算法结束时会生成一棵最短路径树.
Dijkstra提出一个按路径长度不减次序生成最短路径的算法, 将图中顶点集合 V V V分成两组, 令 S S S表示已求出最短路径的顶点集合为A组, 其余尚未确定最短路径的顶点集合为B组。按照最短路径长度从小到大的顺序逐个将顶点加入到 S S S中, 直到从 n o d e node node出发的到达的顶点全部在 S S S中.
这个过程维持着以下几个性质:
node
到 S S S中各顶点的最短路径 < = <= <=node
到B组中的最短路径.- B组顶点的距离是
node
到此顶点的只包含 S S S中顶点为中间顶点的当前最短路径的长度.
算法正确性证明
类似Prim算法的贪心策略, 从起点node
, 每次扩展一个B组中距离最短的点, 再以该点为中间点, 更新node
到其他顶点的距离, 由于边权为非负值, 所以不会存在距离更短且未被扩展的点.
算法步骤
- 初始化
dist[node]=0
,node
到其他顶点的距离dist[i]=INF
. - 找到未访问的点
k
(vis[k]=false
), 满足dist[k]
最小 - 标记点
k
- 以
k
为中间节点, 修改node
到其他未标记点j
的距离值dist[j]
- 循环
N
轮后, 可以得到node
到所有N
个点的最短距离
代码模板
/*
时间复杂度: O(n^2)
输入:
n 表示图中的点数
g g[i][j]表示i到j之间边的距离
输出:
dist dist[i]表示node到i的最短距离
*/
const int N=1005;
int dist[N];
int g[N][N];
bool vis[N];
void dijkstra(int n, int node){
memset(vis, 0x00, sizeof vis);
memset(dist, 0x3f, sizeof dist);
dist(node)=0;
for(int i=1; i<=n; ++i){ // loop n rounds
int v=-1, mindis=0x3f3f3f3f;
for(int j=1; j<=n; ++j) // 找到未访问的最小距离
if(!vis[j] && dist[j]<mindis){
mindis=dist[j];
v=j;
}
if(v==-1) break;
vis[v]=true;
for(int j=1; j<=n; ++j){
if(!vis[j]) dist[j]=min(dist[j], dist[v]+g[v][j]);
}
}
}
时间复杂度
算法需要循环
∣
V
∣
|V|
∣V∣轮,
V
V
V表示顶点个数, 每次需要在B集合中查找距离最小值, 并对该点所连接的边进行扫描, 如果在维护最小值时使用堆(priority_queue
), 即堆优化版本的Dijkstra算法, 复杂度为
O
(
(
∣
E
∣
+
∣
V
∣
)
l
o
g
∣
V
∣
)
O((|E|+|V|)log|V|)
O((∣E∣+∣V∣)log∣V∣); 使用线性扫描法得到最小值, 时间复杂度为
O
(
∣
V
∣
2
+
∣
E
∣
)
O(|V|^2+|E|)
O(∣V∣2+∣E∣).
在最坏情况下,
O
(
∣
E
∣
)
O(|E|)
O(∣E∣)与
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)是同阶复杂度, 此时, 堆优化版本的Dijkstra算法的复杂度甚至还要高于线性版, 但是在一般情况下, 图中边的数量要显著小于顶点的数量
∣
E
∣
<
<
∣
V
∣
2
|E|<<|V|^2
∣E∣<<∣V∣2, 即稀疏图(Sparse Graph), 堆优化版的Dijkstra算法有着显著的优势.
相关例题
洛谷: 【模板】单源最短路径(标准版)
洛谷: 【模板】单源最短路径(标准版)
给出一种堆优化版(小根堆存储)的Dijkstra解法如下
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
#define fi first
#define se second
const int N=100005;
int n, m, s;
int dist[N];
bool vis[N];
vector<PII> g[N];
void dijkstra(){
dist[s]=0;
priority_queue<PII, vector<PII>, greater<PII>> q;
q.push({0, s});
while(!q.empty()){
int node=q.top().se;
q.pop();
if(vis[node]) continue;
vis[node]=true;
for(auto &e: g[node]){
int v=e.fi;
int w=e.se;
if(dist[v]>dist[node]+w){
dist[v]=dist[node]+w;
q.push({dist[v], v});
}
}
}
}
int main(){
memset(dist, 0x3f, sizeof dist);
memset(vis, 0x00, sizeof vis);
cin>>n>>m>>s;
int x, y, w;
while(m--){
cin>>x>>y>>w;
g[x].push_back({y, w});
}
dijkstra();
for(int i=1; i<=n; ++i) cout<<dist[i]<<" ";
return 0;
}
双端队列BFS
从堆优化版的Dijkstra的过程可以看出bfs
算法的原型, 这种逐层搜索的方法可以解决从起始状态到每个状态需要的最少步数的问题, 可以等价于在一个边权均为1的图上执行BFS
算法, 求出每个点相对于起点的最短距离(层次), 对于一类边权为0/1的问题, 可以考虑使用双端队列的BFS求解.
LeetCode 1368. 使网格图至少有一条有效路径的最小代价
LeetCode 1368. 使网格图至少有一条有效路径的最小代价
算法主体框架与BFS
类似, 但是在扩展节点时略有不同, 如果该边的权值为0, 则将其加入队头; 如果该边的权值为1, 则将其加入队尾.
typedef pair<int, int> PII;
#define fi first;
#define se second;
class Solution {
public:
int minCost(vector<vector<int>>& grid) {
const int dirs[4][2]={{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int r=grid.size();
int c=grid[0].size();
int dist[r][c];
bool vis[r][c];
memset(dist, 0x3f, sizeof dist);
memset(vis, 0x00, sizeof vis);
dist[0][0]=0;
deque<PII> q;
q.push_back({dist[0][0], 0*c+0});
vis[0][0]=true;
while(!q.empty()){ // 双端队列BFS
int node=q.front().se;
int x=node/c;
int y=node%c;
q.pop_front();
for(int i=0; i<4; ++i){
int nx=x+dirs[i][0];
int ny=y+dirs[i][1];
bool cost=grid[x][y]-1!=i;
if(0<=nx && nx<r && 0<=ny && ny<c && !vis[nx][ny]){
if(dist[nx][ny]>dist[x][y]+cost){
dist[nx][ny]=dist[x][y]+cost;
// 根据不同权重插入队列不同位置
if(cost) q.push_back({dist[nx][ny], nx*c+ny});
else q.push_front({dist[nx][ny], nx*c+ny});
}
}
}
}
return dist[r-1][c-1];
}
};
参考文献
算法竞赛入门经典(第二版)
信息学奥赛一本通
ACM国际大学生程序设计竞赛 知识与入门/算法与实现