最短路算法笔记
本笔记以 Leetcode.743为例,搞清楚最短路算法的多样化。 代码均来自 公众号 : 宫水三叶的刷题日记,仅是当作笔记使用。
1、Floyd 算法
- Floyd算法求的是从任意起点出发,到达任意起点的最短距离,时间复杂度为O(n^3),使用三个for循环,过程为枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作。搭配邻接矩阵,不需要多讲解直接上代码:
class Solution {
int N = 110, M = 6010;
// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] w = new int[N][N];
int INF = 0x3f3f3f3f;
int n, k;
public int networkDelayTime(int[][] ts, int _n, int _k) {
n = _n; k = _k;
// 初始化邻接矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = w[j][i] = i == j ? 0 : INF;
}
}
// 存图
for (int[] t : ts) {
int u = t[0], v = t[1], c = t[2];
w[u][v] = c;
}
// 最短路
floyd();
// 遍历答案
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = Math.max(ans, w[k][i]);
}
return ans >= INF / 2 ? -1 : ans;
}
void floyd() {
// floyd 基本流程为三层循环:
// 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作
for (int p = 1; p <= n; p++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]);
}
}
}
}
}
2、Dijkstra (普通)算法 + 邻接矩阵
- dijkstra 算法求的是单源最短路 ,时间复杂度为O(n^2),内涵是记录源点到另一个点的最短距离并不断更新,用双层for就能解决,一层用来遍历源点到其他点,一层用来更新源点到其他点的最短距离。看代码:
class Solution {
int N = 110, M = 6010;
// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] w = new int[N][N];
// dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
int[] dist = new int[N];
// 记录哪些点已经被更新过
boolean[] vis = new boolean[N];
int INF = 0x3f3f3f3f;
int n, k;
public int networkDelayTime(int[][] ts, int _n, int _k) {
n = _n; k = _k;
// 初始化邻接矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = w[j][i] = i == j ? 0 : INF;
}
}
// 存图
for (int[] t : ts) {
int u = t[0], v = t[1], c = t[2];
w[u][v] = c;
}
// 最短路
dijkstra();
// 遍历答案
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = Math.max(ans, dist[i]);
}
return ans > INF / 2 ? -1 : ans;
}
void dijkstra() {
// 起始先将所有的点标记为「未更新」和「距离为正无穷」
Arrays.fill(vis, false);
Arrays.fill(dist, INF);
// 只有起点最短距离为 0
dist[k] = 0;
// 迭代 n 次
for (int p = 1; p <= n; p++) {
// 每次找到「最短距离最小」且「未被更新」的点 t
int t = -1;
for (int i = 1; i <= n; i++) {
if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
}
// 标记点 t 为已更新
vis[t] = true;
// 用点 t 的「最小距离」更新其他点
for (int i = 1; i <= n; i++) {
dist[i] = Math.min(dist[i], dist[t] + w[t][i]);
}
}
}
}
3、Dijkstra 算法+优先队列(堆)+邻接表
- 这个算法比普通的多用了一个优先队列,每次优先弹出最短距离最小的点,就少用了一个for循环
时间复杂度为O(m*logn),顺便讲一下邻接表,与数组存储单链表一样是使用头插法,初始化代码如下:
int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
int idx;
void add(int a, int b, int c) {
e[idx] = b; //e 可以得到哪条边对应的点
ne[idx] = he[a];//ne作用是找到下一条边
he[a] = idx;//he是某个节点对应的边集合的头节点
w[idx] = c;//存储边的权值
idx++;
}
//若要遍历从源点到所有点
for (int i = he[a]; i != -1; i = ne[i]) {
int b = e[i], c = w[i]; // 存在由 a 指向 b 的边,权重为 c
}
可以看别人(大佬)的解释:
数组 he 的下标表示结点,值是一个索引 ind,e[ind] 表示 对应一条边,ne[ind] 表示下一个连接结点的索引,假设与 结点a 相连的结点有 b, c, 那么通过 he[a]取得一个索引 ind1 后,通过 e[ind1] = b 可以得到与 a 相连的第一个结点是 b,然后通过 ne[ind1] 可以获得下一个结点的索引 ind2 ,通过 e[ind2] = c 可以得到与 a 相连的第二个结点是 c,最后 ne[ind2] = -1 说明没有下一个结点了
add函数采用链表的头插法,假设 结点a 已经有一个相连的结点 b,那么就有 he[a]=ind, e[ind]=b ,此时再给 a 增加一个相连的结点 c,那么就要建立由b的索引到新结点c的索引 ne[new_ind] = he[a] = ind ,然后新建一条边 e[new_ind], 最后更新 he[a] = new_ind ,就完成了由 a -> b 到 a -> c -> b 的添加操作
可以理解为 he 是邻接表的表头,key是结点val是一个指向存有相邻结点的链表头指针,e是链表结点的val即相邻结点,ne是链表结点的next指针
反推一下,在a->b 中插入一个c 可得 a->c->b,所以e[c_idx] = c,ne[c_idx] = b_idx = he[a],he[a] = c_idx。还是挺形象的。
还有使用邻接表存图的时候,得判断是有向图还是无向图,有权图还是无权图,若是无向图 就需要在add的时候把两个点都加进去 比如说 a和b:
for(int[]t:ts){
add(t[0],t[1]);
add(t[1],t[0]);
}
一般不带权的图使用普通BFS就OK,带权的才会算最短路径。
改良后dijkstra算法上代码:
class Solution {
int N = 110, M = 6010;
// 邻接表
int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
// dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
int[] dist = new int[N];
// 记录哪些点已经被更新过
boolean[] vis = new boolean[N];
int n, k, idx;
int INF = 0x3f3f3f3f;
void add(int a, int b, int c) {
e[idx] = b;
ne[idx] = he[a];
he[a] = idx;
w[idx] = c;
idx++;
}
public int networkDelayTime(int[][] ts, int _n, int _k) {
n = _n; k = _k;
// 初始化链表头
Arrays.fill(he, -1);
// 存图
for (int[] t : ts) {
int u = t[0], v = t[1], c = t[2];
add(u, v, c);
}
// 最短路
dijkstra();
// 遍历答案
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = Math.max(ans, dist[i]);
}
return ans > INF / 2 ? -1 : ans;
}
void dijkstra() {
// 起始先将所有的点标记为「未更新」和「距离为正无穷」
Arrays.fill(vis, false);
Arrays.fill(dist, INF);
// 只有起点最短距离为 0
dist[k] = 0;
// 使用「优先队列」存储所有可用于更新的点
// 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
q.add(new int[]{k, 0});
while (!q.isEmpty()) {
// 每次从「优先队列」中弹出
int[] poll = q.poll();
int id = poll[0], step = poll[1];
// 如果弹出的点被标记「已更新」,则跳过
if (vis[id]) continue;
// 标记该点「已更新」,并使用该点更新其他点的「最短距离」
vis[id] = true;
for (int i = he[id]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[id] + w[i]) {
dist[j] = dist[id] + w[i];
q.add(new int[]{j, dist[j]});
}
}
}
}
}
4、Bellman Ford 算法(比较简单)
BF算法和Floyd算法一样都是基于动态规划衍生的算法,BF可以用在负权图中求最短路,这个也是一种单源最短路算法。可以使用类和邻接表或者邻接矩阵来配合使用,BF的核心操作就是遍历所有的边,这时候就可以不需要另外开一个空间存图。BF适合用在有条件限制的求最短路问题。
这里以另一题为例 Leetcode.787
这种题就是有限制的最短路问题,也可以用动态规划来做,但BF其实可以看成由原始状态 f[i][k] 从源点到i点最多经过k条边的最短路径,以下为代码:
class Solution {
int N = 110, INF = 0x3f3f3f3f;
int[][] g = new int[N][N];
int[] dist = new int[N];
int n, m, s, t, k;
public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
n = _n; s = _src; t = _dst; k = _k + 1;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
g[i][j] = i == j ? 0 : INF;
}
}
for (int[] f : flights) {
g[f[0]][f[1]] = f[2];
}
int ans = bf();
return ans > INF / 2 ? -1 : ans;
}
int bf() {
Arrays.fill(dist, INF);
dist[s] = 0;
for (int limit = 0; limit < k; limit++) {
int[] clone = dist.clone(); //这里很关键
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dist[j] = Math.min(dist[j], clone[i] + g[i][j]);
}
}
}
return dist[t];
}
}
其中有个关键的代码,就是在迭代的时候,我们如果要使用dist[a] 来更新dist[b],不能保证dist[a]是否在同一次迭代更新,这也就有点判断是否遍历过的意思,所以每次都需要备份以下dist数组以防止超出次数限制。