目录
- 前言
- 算法的属性
- 算法的原理和实现
前言
读完本文,可以尝试自己写代码做一下下面的两道题:
- Dijkstra 未优化版练习
- Dijkstra 优化版练习
注意:第 1 1 1 道题可以使用 O ( N 2 ) O(N^2) O(N2) 的时间复杂度通过,第二道题则需要使用 O ( N log N ) O(N \log N) O(NlogN) 的时间复杂度通过。其余方面二者没有任何区别。可以参考本文下面给出的两种 Dijkstra 算法的 C++ 模版,但建议自己写代码加深记忆。(理解为主,不要死记硬背算法,我之前吃过这方面的亏!!!)
一、算法属性
- Dijkstra 属于单元最短路算法,适用于计算以有向图中单一的一个点作为起点到图中任意一个点的距离。
- Dijkstra 算法的时间复杂度为 O ( N 2 ) O(N^2) O(N2) 或 O ( N log N ) O(N \log N) O(NlogN) 级别,且不能处理负边权的情况。
- Dijkstra 适用于处理点少边多,即稠密图的最短路问题。
二、算法原理及实现
- 算法准备
Dijkstra 也可以理解为一种 涂色法 。我们将一个图中的所有店分为两类:未确定的点和已确定的点,可以理解为红色点和白色点。为实现这个方案,我们需要开一个bool
型的vis
数组,用于存放我们的点的涂色情况, 0 0 0 表示白色点(即未确定), 1 1 1 表示已涂色红色点(即已确定)。为存储最短路,我们还需要一个 d d d 数组,存放的是从第 k k k 号点到任意一个点的距离。(程序中指定的 k k k 点作为起点) - 算法实现
(1)初始化: d k = 0 , d i = ∞ d_k=0, d_i=\infin dk=0,di=∞ 其中, i i i 在这个数组中的任意位置且 i ≠ k i \neq k i=k 。
(2)核心部分
我们可以使用一个for
循环选出所有的白色点中的点中 d d d 值最小的点,记这个点的位置为 p ( p → 白色点 ) p (p \rightarrow \text{白色点}) p(p→白色点) 。接下来,我们开始枚举与这个点相邻的边,通过这个点的值更新边的另一个端点的值。具体来说,设我们枚举的这条边是有向边 p → w p \rightarrow w p→w ,则 d d d 数组的更新方式如下:
d w ← min { d w , d p + W p → w } d_w\leftarrow \min \{d_w, d_p+W_{p \rightarrow w}\} dw←min{dw,dp+Wp→w}
在上面的算式中, W W W 数组表示每条边的边权。这个操作有一个更加书面化的表达:松弛。每松弛一次,周围点的 d d d 值也会相应的更新一次。
(3)举例模拟
为了更好地理解上面所述的文字内容,我将用下面的图片说明算法的实现。
通过上面的操作,我们可以得知, d = { 0 , 3 , 2 , 2 } d=\{0, 3, 2, 2\} d={0,3,2,2} 。
(4)代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 2510;
struct Node
{
int to, wi;
};
int n, m, x, y, vis[N], d[N];
vector<Node> g[N];
int main()
{
cin >> n >> m >> x >> y;
for (int i = 1, u, v, w; i <= m; ++i)
{
cin >> u >> v >> w;
g[u].push_back( Node{v, w} );
g[v].push_back( Node{u, w} );
}
memset(d, 0x3f, sizeof(d));
d[x] = 0;
for (int i = 1; i <= n - 1; ++i)
{
int minn = 1e9, p = 0;
for (int j = 1; j <= n; ++j)
{
if (vis[j]) continue;
if (d[j] < minn)
{
minn = d[j];
p = j;
}
}
vis[p] = 1;
for (auto j : g[p])
{
if (vis[j.to]) continue;
if (d[p] + j.wi < d[j.to])
{
d[j.to] = d[p] + j.wi;
}
}
}
cout << d[y] << endl;
return 0;
}
- 考虑优化
上面代码的时间复杂度为 O ( N 2 ) O(N^2) O(N2) ,不能解决 N ≤ 1 × 1 0 5 N \le 1 \times 10^5 N≤1×105 数据规模以上的问题。下面考虑优化,将其时间复杂度降为 O ( N log N ) O(N \log N) O(NlogN) 。
前置知识:优先队列(priority_queue)
使用优先队列前,需要#include <queue>
或#include <bits/stdc++.h>
。
优先队列相当于一个堆,有大根堆和小根堆。下面介绍优先队列的模版参数:
#include <bits/stdc++.h>
priority_queue< type, container, rule > q;
参数名 | 作用 | 取值示例 |
---|---|---|
type | 指定 priority_queue 中存放的数据类型 | int , char 等 |
container | 指定 priority_queue 用来存放数组的结构 | 常见的是 vector<type> |
rule | 指定 priority_queue 用来比较大小的结构 | greater<type> 为小根堆,less<type> 为大根堆 |
下面是优先队列的一种不同于普通 queue
的常用操作。
方法 | 作用 |
---|---|
top | priority_queue 具有堆的性质的体现,返回 priority_queue 堆顶的值 |
注意:优先队列的 push
,pop
和 top
操作的时间复杂度均为
O
(
log
N
)
O(\log N)
O(logN)
我们可以开始的时候定义一个 typedef pair<int, int> pii
,然后将 priority_queue
里面存放成 first
项为最短路,second
项为编号的 pair
类型对象。我们每次取堆顶元素并删除(使用 pop
操作),然后遍历堆顶元素的对应边,更新时只需将另一个端点的最短路和编号插入 priority_queue
即可(使用 push
操作)。
具体代码如下:
#include <bits/stdc++.h>
using namespace std;
#define x first
#define y second
const int N = 2e5 + 10;
typedef pair<int, int> pii;
int n, m, d[N], v[N];
priority_queue< pii, vector<pii>, greater<pii> > q;
vector< pii > e[N];
int main()
{
cin >> n >> m;
for (int i = 1, u, v, w; i <= m; ++i)
{
cin >> u >> v >> w;
e[u].push_back( {v, w} );
}
memset(d, 0x3f, sizeof(d));
q.push( {0, 1} );
d[1] = 0;
while (!q.empty())
{
pii h = q.top();
q.pop();
if (v[h.y]) continue;
v[h.y] = 1;
for (pii t : e[h.y])
{
if (d[t.x] > h.x + t.y)
{
d[t.x] = h.x + t.y;
q.push( {d[t.x], t.x} );
}
}
}
if (d[n] < 1e9) cout << d[n] << endl;
else cout << -1 << endl;
return 0;
}
附:本文中的输入格式
第一行包含整数
n
,
m
n,m
n,m ,表示点数和边数。
第二行至第
m
+
1
m+1
m+1 行,每行三个整数
u
,
v
,
w
u,v,w
u,v,w ,表示每条边的起点、终点和边权。