文章目录
- 图的存储
- 邻接矩阵
- 邻接表
- 链式向前星
- 一、Dijkstra 算法
- [743. 网络延迟时间](https://leetcode.cn/problems/network-delay-time/)
- [1631. 最小体力消耗路径](https://leetcode.cn/problems/path-with-minimum-effort/)
- [1928. 规定时间内到达终点的最小花费](https://leetcode.cn/problems/minimum-cost-to-reach-destination-in-time/)
- 二、弗洛伊德 Floyd 算法
- [743. 网络延迟时间](https://leetcode.cn/problems/network-delay-time/)
- [1334. 阈值距离内邻居最少的城市](https://leetcode.cn/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/)
- 三、SPFA 算法
- [787. K 站中转内最便宜的航班](https://leetcode.cn/problems/cheapest-flights-within-k-stops/)
- 1344:【例4-4】最小花费
- [675. 为高尔夫比赛砍树](https://leetcode.cn/problems/cut-off-trees-for-golf-event/)
- 399. 除法求值
- 882. 细分图中的可到达节点
- 1368. 使网格图至少有一条有效路径的最小代价
- 1514. 概率最大的路径
- 1786. 从第一个节点出发到最后一个节点的受限路径数
- 1976. 到达目的地的方案数
- 2045. 到达目的地的第二短时间
- 2203. 得到要求路径的最小带权子图
- 2290. 到达角落需要移除障碍物的最小数目
图的存储
若一个图的每一对不同顶点恰有一条边相连,则称为 完全图。
稀疏图 的边数远远少于完全图;稠密图 的边数接近于或等于完全图。
稀疏图用 邻接表 存储;稠密图用 邻接矩阵 存储。
邻接矩阵
邻接矩阵基于二维数组的:
int[][] g = new int[n][n];
用 g[i][j] 表示结点 i 到 j 的权值,g[i][j] = INF 表示 i 和 j 之间无边。
对于无向图:g[i][j] = g[j][i];
常用的做法是初始化所有结点之间的权值都为 INF, 然后根据图去更新各点之间的权值。
- 存储复杂度 O(V2) 太高。如果用来存储稀疏图,大量的空间会被浪费。
- 一般情况下,不能存储重边。(u,v)之间可能有两条或者更多的边,它们的权值不同,是不能合并的。
邻接表
edge 类来表示边,并定义 3 个变量 from, to, w。分别表示边的起点、终点和权值。
class Edge {
int from, to, w; // 边的起点、终点和权值
public Edge(int a, int b, int c) { // 构造方法
this.from = a;
this.to = b;
this.w = c;
}
}
集合存集合的方式:
List<List<Edge>> list = new ArrayList();
for (int i = 0; i <= n; i++) {
list.add(new ArrayList()); // 先给每个点分配一个空邻边集合
}
要访问 i 点的邻边,可以通过 list.get(i) 返回 i 点的邻边集合。
链式向前星
更紧凑的存图方式:链式向前星。它是对邻接表的改进,不使用集合(或数组)来存放某个点的所有邻边,直接存放一个邻边,然后邻边指向下一个邻边,依次类推。它的实现可以基于几个一维数组。
int[] head = new int[N+1]; // (N是点的数量)存放各个点的第一个邻边的数组
int[] next = new int[M+1]; // (M是边的数量)存放当前边的下一条邻边的数组
int[] e = new int[M+1]; // 存放当前边是到哪个点的数组
int[] w = new int[M+1]; // 存放边的权值的数组
一、Dijkstra 算法
从图中的某个顶点出发到达另外一个顶点的所经过的 边的权重和最小 的一条路径,称为 最短路径。
Dijkstra 算法: 单源正权值 最短路径,算法思想 贪心算法:每次确定最短路径的一个点然后维护(更新)这个点周围点的距离加入预选队列,等待下一次的抛出确定。需要邻接矩阵(表)储存权值,需要优先队列(或者每次都比较)维护一个预选点的集合。还要用一个 boolean 数组标记是否已经确定。
在 n 点图中求 多源 最短路径,Dijkstra 算法需要执行 n 次才能获得所有点之间的最短路径,复杂度为O(n3)。
DisjKstra 的两种实现
使用 优先队列,适用于 稀疏图:将所有点分成 visited 和 unvisited 两个集合,优先队列记录的是和 visited 的点相连的边,队头是这些边中权值最小的一条边。使用 邻接表 表示邻接关系。
使用 数组,适用于 稠密图:引入 visited 向量记录是否使用过了(使用过的意思是被当做中间节点 ),使用 邻接矩阵 表示邻接关系。
无权图的最短路算法 Finding Shortest Path in Unweighted GraphsDijkstra 算法 寻找有权图中最短路 Finding Shortest Path in Weighted Graphs手写迪杰斯特拉-Dijkstra
- Dijkstra (迪杰斯特拉算法)基于 贪心 思想,用于求某个单源点到其余各个顶点的最短路径,图中边的权值不能出现负数,即 非负权 的单源最短路径算法。
- Floyd (弗洛伊德算法)基于 动态规划 思想,用于求任意一对顶点间的最短路径,图中边的权值可以是负数,但不能出现 负环,主要处理多源最短路问题。
- SPFA 基于 队列 优化思想,用于求某个单源点到其余各个顶点的最短路径,图中边的权值可以是负数,但不能出现 负环。
743. 网络延迟时间
求节点 k 到其他所有点中最远的距离,即节点 K 到其他所有点的最短路,然后取最大值。
题解
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
final int INF = 0x3f3f3f3f; // 防止溢出。
// 构图 1、邻接矩阵
int[][] g = new int[n][n];
for (int i = 0; i < n; ++i) Arrays.fill(g[i], INF);
for (int[] t : times) g[t[0] - 1][t[1] - 1] = t[2]; // 节点标记为 1 到 n。
// 2、邻接表
// List<int[]>[] g = new List[n];
// for (int i = 0; i < n; ++i) g[i] = new ArrayList();
// for (int[] t : times) g[t[0] - 1].add(new int[]{t[1] - 1, t[2]});
// 从源点到某点的距离数组
int[] dist = new int[n];
Arrays.fill(dist, INF);
// 由于从 k 开始,所以该点距离设为 0,也即源点
dist[k - 1] = 0;
// 节点是否被更新数组
boolean[] used = new boolean[n];
for (int i = 0; i < n; ++i) { // 循环 n 次
// 1、在还未确定最短路的点中,寻找离源点距离最小的点。
int x = -1;
for (int y = 0; y < n; ++y)
if (!used[y] && (x == -1 || dist[y] < dist[x]))
x = y; // 首先找到的是 0,第一个循环结束找到的一定是 k - 1
// 每一轮确定一个点
used[x] = true;
// 2、用该点更新所有其他点的距离
for (int y = 0; y < n; ++y)
dist[y] = Math.min(dist[y], dist[x] + g[x][y]);
}
// var pq = new PriorityQueue<Integer>((a, b) -> dist[a] - dist[b]);
// pq.offer(k - 1);
// while (!pq.isEmpty()) {
// int x = pq.poll();
// for (int[] e : g[x]) {
// int y = e[0], d = dist[x] + e[1];
// if (d < dist[y]) {
// dist[y] = d;
// pq.offer(y);
// }
// }
// }
// 找到距离最远的点
int ans = Arrays.stream(dist).max().getAsInt();
return ans == INF ? -1 : ans;
}
}
1631. 最小体力消耗路径
二分答案,并查集,单源最短路径。
图论模型:将地图中的每一个格子看成图中的一个节点;将两个相邻(左右相邻或者上下相邻)的两个格子对应的节点之间连接一条无向边,边的权值为这两个格子的高度差的绝对值。
需要找到一条从左上角到右下角的最短路径,其中一条路径的长度定义为其经过的所有边权的最大值。
class Solution {
public int minimumEffortPath(int[][] heights) {
int[] dir = {1, 0, -1, 0, 1};
int m = heights.length, n = heights[0].length;
boolean[][] vis = new boolean[m][n];
vis[0][0] = true; // 关键结点,作为最小值更新四周并扩展。
int[][] dist = new int[m][n]; // 记录到达点的最小权值
for (int[] d : dist)
Arrays.fill(d, Integer.MAX_VALUE);
dist[0][0] = 0;
PriorityQueue<int[]> q = new PriorityQueue<int[]>((a, b) -> a[2] - b[2]);
q.offer(new int[]{0, 0, 0});
while (!q.isEmpty()) {
int[] tmp = q.poll();
int i = tmp[0], j = tmp[1], d = tmp[2];
if (i == m - 1 && j == n - 1) return d; // break; 最终返回 dist[m - 1][n - 1];
vis[i][j] = true; // 作为最小值向四周扩展
for (int k = 0; k < 4; k++) {
int x = i + dir[k], y = j + dir[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y]) {
// 到达 (i, j) 最小权重为 d, 到达 (x, y) 取 max, 更新 dist。
int diff = Math.max(d, Math.abs(heights[i][j] - heights[x][y]));
if (diff < dist[x][y]) {
q.add(new int[]{x, y, diff});
dist[x][y] = diff;
}
}
}
}
return 0;
}
}
1928. 规定时间内到达终点的最小花费
class Solution {
public int minCost(int maxTime, int[][] edges, int[] passingFees) {
int n = passingFees.length;
int INF = Integer.MAX_VALUE;
// 建图
List<int[]>[] g = new List[n];
for (int i = 0; i < n; i++) {
g[i] = new ArrayList();
}
for (int[] e: edges) {
g[e[0]].add(new int[]{e[1], e[2]});
g[e[1]].add(new int[]{e[0], e[2]});
}
int[] minFee = new int[n];
int[] minTime = new int[n];
Arrays.fill(minTime, INF);
Arrays.fill(minFee, INF);
minTime[0] = 0;
minFee[0] = passingFees[0];
// 最小堆。 按 fee 排升序,因为最终要求最少花费
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
pq.offer(new int[]{0, passingFees[0], 0}); // 城市, 花费, 时间
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int u = cur[0], fee = cur[1], time = cur[2];
if (u == n - 1) return fee; // 达到最后一个城市
for (int[] neighbour: g[u]) {
int v = neighbour[0];
int newTime = neighbour[1] + time;
int newFee = passingFees[v] + fee;
if (newTime > maxTime) continue;
if (newTime < minTime[v]) { // 时间更短
minTime[v] = newTime;
minFee[v] = newFee;
pq.offer(new int[]{v, newFee, newTime});
} else if (newFee < minFee[v]) { // 花费更少
minFee[v] = newFee;
pq.offer(new int[]{v, newFee, newTime});
}
}
}
return -1;
}
}
class Solution {
private static final int INF = 0x3f3f3f3f;
public int minCost(int maxTime, int[][] edges, int[] passingFees) {
int n = passingFees.length;
List<int[]>[] adj = buildAdjacencyList(n, maxTime, edges);
// {u, time, fee}
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[2] - b[2]);
pq.add(new int[]{0, 0, passingFees[0]});
int[] minTime = new int[n];
int[] minFee = new int[n];
Arrays.fill(minTime, INF);
Arrays.fill(minFee, INF);
minTime[0] = 0;
minFee[0] = passingFees[0];
while (!pq.isEmpty()) {
int[] from = pq.poll();
int u = from[0], time = from[1], fee = from[2];
if (u == n - 1) return fee;
if (time > minTime[u] && fee > minFee[u]) continue;
for (int[] e: adj[u]) {
int v = e[0];
int newTime = time + e[1];
int newFee = fee + passingFees[v];
if (newTime > maxTime || newTime >= minTime[v] && newFee >= minFee[v]) continue;
minTime[v] = Math.min(minTime[v], newTime);
minFee[v] = Math.min(minFee[v], newFee);
pq.add(new int[]{v, newTime, newFee});
}
}
return -1;
}
// 邻接表
private List<int[]>[] buildAdjacencyList(int n, int maxTime, int[][] edges) {
List<int[]>[] adj = new List[n];
Arrays.setAll(adj, e -> new ArrayList<>());
for (int[] e: edges) {
// if (e[2] > maxTime) continue;
adj[e[0]].add(new int[]{e[1], e[2]});
adj[e[1]].add(new int[]{e[0], e[2]});
}
return adj;
}
}
二、弗洛伊德 Floyd 算法
Floyd-Warshall 用于解决任意两点间的最短路径(多源最短路径问题),支持负权,而 Dijkstra 算法不支持负权。
Floyd 算法又称为插点法,利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与 Dijkstra 算法类似。该算法名称以创始人之一、1978 年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。
1 .邻接矩阵(二维数组) dist 储存路径,数组中的值开始表示点点之间初始直接路径,最终是点点之间的最小路径,如果没有直接相连的两点那么默认为 INF,自己和自己的距离要为 0。
2 .从第 1 个到第 n 个点依次加入松弛计算,每个点加入进行试探枚举是否有路径长度被更改(自己能否更新路径)。顺序加入( k 枚举)松弛的点时候,需要遍历图中每一个点对(i, j 双重循环),判断每一个点对距离是否因为加入的点而发生最小距离变化,如果发生改变(变小),那么两点 (i, j) 距离就更改。
3 .重复上述直到最后插点试探完成。
状态转移方程为:
dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k][j])
其中 dp[i][j] 表示点 i 到点 j 的最短路径。
743. 网络延迟时间
第一步: 定义 s[i][j] 存储 i 到 j 的(最短)路径长度,并按如下顺序进行初始化:
自己到自己的长度初始化为 0
其他两点赋予 Integer.MAX_VALUE / 2。(防止最大值 + 某个数时溢出)
最后赋值题目已知的某两点距离
// 节点从 1 - n
int INF = 0x3f3f3f3f;
int[][] s = new int[n+1][n+1]; // s[i][j] 表示 i 到 j 的(最小)距离
// 数据初始化(到自己是 0)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(i != j) s[i][j] = INF; // 没有通路,赋予最大值
for(int[] t : times)
s[t[0]][t[1]] = t[2];
第二步:弗洛伊德算法核心,三个 For 循环(递推式部分)
当经过 k 时的 i 到 j 的距离 s[i][k] + s[k][j] 比已知距离 s[i][j] 小,则更新 s[i][j]。(此步骤在《算法导论》中称之为松弛 Relaxation)
// 第二步:弗洛伊德算法
for(int k = 1;k <= n;k++){
// 计算经过 k 节点时,i 到 j 的最短路径长度
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
s[i][j] = Math.min(s[i][j], s[i][k] + s[k][j]);
第三步:查看 s[i][j] 任意两点之间的最短路径只要不是 INF,便可达。
时间复杂度为 O(n^3)
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
int INF = 0x3f3f3f3f;
int[][] grid = new int[n+1][n+1]; // grid[i][j] 表示 i 到 j 的(最小)距离
// 数据初始化
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(i != j) grid[i][j] = INF; // 没有通路,赋予最大值
for(int[] t : times)
grid[t[0]][t[1]] = t[2];
// 弗洛伊德算法
for(int m = 1; m <= n; m++){
// 计算经过 m 节点时,更新 i 到 j 的最短路径长度
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
grid[i][j] = Math.min(grid[i][j], grid[i][m] + grid[m][j]);
// 寻找k到每个节点的最大距离
int res = 0;
for(int i = 1; i <= n; i++)
res = Math.max(res, grid[k][i]);
return res == INF ? -1 : res;
}
}
Floyd 算法支持负权重的边,但是不支持权重为负值的环路。
class Solution {
static int INF = 0x3f3f3f3f;
public int networkDelayTime(int[][] times, int n, int k) {
/* BFS */
// 建图 - 邻接表
Map<Integer, Map<Integer, Integer>> mp = new HashMap<>();
for (int[] edg : times) {
mp.computeIfAbsent(edg[0] - 1, v -> new HashMap()).put(edg[1] - 1, edg[2]);
}
// 记录结点最早收到信号的时间
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[k - 1] = 0;
// 队列中存放 [结点,收到信号时间]
Deque<int[]> q = new ArrayDeque();
q.offer(new int[]{k - 1, 0}); // k - 1 是源点
while (!q.isEmpty()) {
int[] cur = q.poll();
if (!mp.containsKey(cur[0])) continue;
for (int key : mp.get(cur[0]).keySet()) {
int t = mp.get(cur[0]).get(key) + cur[1];
if (t < dist[key]) {
dist[key] = t;
q.add(new int[]{key, t});
}
}
}
int res = -1;
for (int i = 0; i < n; ++i)
res = Math.max(res, dist[i]);
return res == INF ? -1 : res;
}
}
1334. 阈值距离内邻居最少的城市
在 distanceThreshold 范围内找到能够到达的最少点的编号,如果多个取最大编号。
1 .先使用 Floyd 算法求出点点之间的最短距离,时间复杂度 O(n3)
2 . 统计每个点与其他点距离在 distanceThreshold 之内的点数量,统计的同时看看是不是小于等于已知最少个数的,如果是,那么保存更新。
class Solution {
public int findTheCity(int n, int[][] edges, int distanceThreshold) {
int INF = 0x3f3f3f3f;
int[][] g = new int[n][n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (i != j) g[i][j] = INF;
for (int[] e : edges) {
g[e[0]][e[1]] = e[2];
g[e[1]][e[0]] = e[2];
}
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
g[i][j] = Math.min(g[i][j], g[i][k] + g[k][j]);
int res = -1;
int min = INF;
for (int i = 0; i < n; i++) {
int tmp = 0;
for (int j = 0; j < n; j++)
if (g[i][j] <= distanceThreshold) tmp++;
if (tmp <= min) {
min = tmp;
res = i;
}
}
return res;
}
}
三、SPFA 算法
SPFA 算法是对 Bellman-Ford 算法的优化,通常使用队列来优化。
Bellman-Ford 算法每次需要对所有边做松驰操作,而 SPFA 只需要对队列中的元素做松驰操作即可,可以减少遍历次数,提高效率。
787. K 站中转内最便宜的航班
class Solution {
int INF = 0x3f3f3f3f;
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
int[][] g = new int[n][n];
for (int[] t : g) Arrays.fill(t, INF);
for (int[] x : flights) g[x[0]][x[1]] = x[2];
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[src] = 0;
boolean[] inqueue = new boolean[n];
int[] queue = new int[1001];
int offerIndex = 0, pollIndex = 0;
queue[offerIndex++] = src;
inqueue[src] = true;
while (k-- >= 0) {
int[] clone = dist.clone();
int size = offerIndex - pollIndex;
while (size-- > 0) {
int node = queue[pollIndex++];
inqueue[node] = false;
for (int i = 0; i < n; i++) {
int tmp = clone[node] + g[node][i];
if (tmp < dist[i]) {
dist[i] = tmp;
queue[offerIndex++] = i;
inqueue[i] = true;
}
}
}
}
return dist[dst] >= INF ? -1 : dist[dst];
}
}
1344:【例4-4】最小花费
计算带权边的单源最短路径,如果边不带权重,直接用 bfs 计算。
675. 为高尔夫比赛砍树
399. 除法求值
882. 细分图中的可到达节点
1368. 使网格图至少有一条有效路径的最小代价
1514. 概率最大的路径
1786. 从第一个节点出发到最后一个节点的受限路径数
1976. 到达目的地的方案数
2045. 到达目的地的第二短时间
2203. 得到要求路径的最小带权子图
2290. 到达角落需要移除障碍物的最小数目