|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[882. 细分图中的可到达节点](https://leetcode.cn/problems/reachable-nodes-in-subdivided-graph/solution/by-ac_oier-yrhg/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「最短路」、「单源最短路」、「Dijkstra」、「SPFA」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个无向图(原始图),图中有 `n` 个节点,编号从 `0` 到 `n - 1` 。你决定将图中的每条边 细分 为一条节点链,每条边之间的新节点数各不相同。 |
| 10 | + |
| 11 | +图用由边组成的二维数组 `edges` 表示,其中 $edges[i] = [u_{i}, v_{i}, cnt_{i}]$ 表示原始图中节点 $u_{i}$ 和 $v_{i}$ 之间存在一条边,$cnt_{i}$ 是将边 细分 后的新节点总数。注意,$cnt_{i} = 0$ 表示边不可细分。 |
| 12 | + |
| 13 | +要 细分边 $[u_{i}, v_{i}]$ ,需要将其替换为 $(cnt_{i} + 1)$ 条新边,和 $cnt_{i}$ 个新节点。新节点为 $x_1, x_2, ..., x_{cnt_{i}}$ ,新边为 $[u_{i}, x_{1}], [x_{1}, x_{2}], [x_{2}, x_{3}], ..., [x_{cnt_{i}}+1, x_{cnt_{i}}], [x_{cnt_{i}}, v_{i}]$ 。 |
| 14 | + |
| 15 | +现在得到一个 新的细分图 ,请你计算从节点 `0` 出发,可以到达多少个节点?如果节点间距离是 `maxMoves` 或更少,则视为 可以到达 。 |
| 16 | + |
| 17 | +给你原始图和 `maxMoves` ,返回 新的细分图中从节点 `0` 出发 可到达的节点数 。 |
| 18 | + |
| 19 | +示例 1: |
| 20 | +``` |
| 21 | +输入:edges = [[0,1,10],[0,2,1],[1,2,2]], maxMoves = 6, n = 3 |
| 22 | + |
| 23 | +输出:13 |
| 24 | + |
| 25 | +解释:边的细分情况如上图所示。 |
| 26 | +可以到达的节点已经用黄色标注出来。 |
| 27 | +``` |
| 28 | +示例 2: |
| 29 | +``` |
| 30 | +输入:edges = [[0,1,4],[1,2,6],[0,2,8],[1,3,1]], maxMoves = 10, n = 4 |
| 31 | + |
| 32 | +输出:23 |
| 33 | +``` |
| 34 | +示例 3: |
| 35 | +``` |
| 36 | +输入:edges = [[1,2,4],[1,4,5],[1,3,1],[2,3,4],[3,4,5]], maxMoves = 17, n = 5 |
| 37 | + |
| 38 | +输出:1 |
| 39 | + |
| 40 | +解释:节点 0 与图的其余部分没有连通,所以只有节点 0 可以到达。 |
| 41 | +``` |
| 42 | + |
| 43 | +提示: |
| 44 | +* 0ドル <= edges.length <= \min(n * (n - 1) / 2, 10^4)$ |
| 45 | +* $edges[i].length = 3$ |
| 46 | +* 0ドル <= u_{i} < v_{i} < n$ |
| 47 | +* 图中 不存在平行边 |
| 48 | +* 0ドル <= cnt_{i} <= 10^4$ |
| 49 | +* 0ドル <= maxMoves <= 10^9$ |
| 50 | +* 1ドル <= n <= 3000$ |
| 51 | + |
| 52 | +--- |
| 53 | + |
| 54 | +### 朴素 Dijkstra |
| 55 | + |
| 56 | +为了方便,我们将原始图边的数量记为 `m`,因此对于原始图而言,点的数量 3000ドル,ドル边的数量为 10000ドル$。 |
| 57 | + |
| 58 | +题目要我们求新图上,从 `0` 点出发可到达的点的数量,我们将原图上存在的点称为「原点」,细分边上增加的点称为「细分点」,两类点中可达点的数量即是答案。 |
| 59 | + |
| 60 | +在分别考虑如何统计两类点之前,我们需要重新定义一下边的权重:**若原点 `u` 和原点 `v` 的边上存在 `c` 个细分点,我们将原点 `u` 和原点 `v` 之间的边看作是一条权重为 `c + 1` 的无向边(结合题意,`c` 个点存在 `c + 1` 个分段/距离)**。 |
| 61 | + |
| 62 | +重新定义边的权重后,因为该图是「稠密图」,我们可以使用「朴素 Dijkstra」来求解最短路,得到 $dist$ 数组,其中 $dist[x] = t$ 含义为从原点 `0` 点出发,到达原点 `x` 的最短距离为 `t`。 |
| 63 | + |
| 64 | +> **不了解最短路的同学可以看前置 🧀 : [涵盖所有的「存图方式」与「最短路算法(详尽注释)」](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d)** |
| 65 | + |
| 66 | +随后考虑如何统计答案(可达点的数量),根据统计点的类型分情况讨论: |
| 67 | + |
| 68 | +1. 对于原点:若有 $dist[x] < max$ 的话,说明原点 `x` 可达,累加到答案中; |
| 69 | + |
| 70 | +2. 对于细分点:由于所有的细分点都在原图边上,因此我们可以统计所有原图边上有多少细分点可达。 |
| 71 | + 对于任意一条边 $e(u, v)$ 而言,该边上可达点数量包含「经过原点 `u` 可达」以及「经过原点 `v` 可达」的交集,其中原点 `0` 到达原点 `u` 以及原点 `v` 的距离,我们是已知的。因此经过原点 `u` 可达的数量为 $\max(0, max - dist[u]),ドル经过原点 `v` 可达的数量为 $\max(0, max - dist[v]),ドル两者之和与该边上细分点的总数取 `min` 即是这条边可达点的数量。 |
| 72 | + |
| 73 | +代码: |
| 74 | +```Java |
| 75 | +class Solution { |
| 76 | + static int N = 3010, INF = 0x3f3f3f3f; |
| 77 | + static int[][] g = new int[N][N]; |
| 78 | + static int[] dist = new int[N]; |
| 79 | + static boolean[] vis = new boolean[N]; |
| 80 | + public int reachableNodes(int[][] edges, int max, int n) { |
| 81 | + // 建图 |
| 82 | + for (int i = 0; i < n; i++) Arrays.fill(g[i], INF); |
| 83 | + for (int[] info : edges) { |
| 84 | + int a = info[0], b = info[1], c = info[2] + 1; |
| 85 | + g[a][b] = g[b][a] = c; |
| 86 | + } |
| 87 | + // 朴素 Dijkstra |
| 88 | + Arrays.fill(dist, INF); |
| 89 | + Arrays.fill(vis, false); |
| 90 | + dist[0] = 0; |
| 91 | + for (int i = 0; i < n; i++) { |
| 92 | + int t = -1; |
| 93 | + for (int j = 0; j < n; j++) { |
| 94 | + if (!vis[j] && (t == -1 || dist[j] < dist[t])) t = j; |
| 95 | + } |
| 96 | + vis[t] = true; |
| 97 | + for (int j = 0; j < n; j++) dist[j] = Math.min(dist[j], dist[t] + g[t][j]); |
| 98 | + } |
| 99 | + // 统计答案 |
| 100 | + int ans = 0; |
| 101 | + for (int i = 0; i < n; i++) { |
| 102 | + if (dist[i] <= max) ans++; |
| 103 | + } |
| 104 | + for (int[] info : edges) { |
| 105 | + int a = info[0], b = info[1], c = info[2]; |
| 106 | + int c1 = Math.max(0, max - dist[a]), c2 = Math.max(0, max - dist[b]); |
| 107 | + ans += Math.min(c, c1 + c2); |
| 108 | + } |
| 109 | + return ans; |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | +* 时间复杂度:建图复杂度为 $O(m)$;使用朴素 Dijkstra 求最短路复杂度为 $O(n^2)$;统计答案复杂度为 $O(n + m)$。整体复杂度为 $O(m + n^2)$ |
| 114 | +* 空间复杂度:$O(n^2)$ |
| 115 | + |
| 116 | +--- |
| 117 | + |
| 118 | +### SPFA |
| 119 | + |
| 120 | +从数据范围来看,无论是朴素 Dijkstra 还是堆优化版的 Dijkstra 都可以过,复杂度分别为 $O(n^2)$ 和 $O(m\log{n})$。 |
| 121 | + |
| 122 | +那 Bellman Ford 类的单源最短路就无法通过了吗? |
| 123 | + |
| 124 | +理论上,无论是 Bellman Ford 还是 SPFA 复杂度均为 $O(n \times m),ドル均无法通过本题。但实际上 SPFA 由于使用队列对松弛顺序进行了调整,因此在应对「非菊花图」时均表现良好,复杂度可视为 $O(k \times m),ドル近似 $O(m)$。 |
| 125 | + |
| 126 | +代码: |
| 127 | +```Java |
| 128 | +class Solution { |
| 129 | + static int N = 3010, M = 20010, INF = 0x3f3f3f3f, idx = 0; |
| 130 | + static int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M]; |
| 131 | + static int[] dist = new int[N]; |
| 132 | + static boolean[] vis = new boolean[N]; |
| 133 | + void add(int a, int b, int c) { |
| 134 | + e[idx] = b; |
| 135 | + ne[idx] = he[a]; |
| 136 | + w[idx] = c; |
| 137 | + he[a] = idx++; |
| 138 | + } |
| 139 | + public int reachableNodes(int[][] edges, int max, int n) { |
| 140 | + // 建图 |
| 141 | + Arrays.fill(he, -1); |
| 142 | + idx = 0; |
| 143 | + for (int[] info : edges) { |
| 144 | + int a = info[0], b = info[1], c = info[2] + 1; |
| 145 | + add(a, b, c); add(b, a, c); |
| 146 | + } |
| 147 | + // SPFA |
| 148 | + Arrays.fill(dist, INF); |
| 149 | + Arrays.fill(vis, false); |
| 150 | + Deque<Integer> d = new ArrayDeque<>(); |
| 151 | + d.addLast(0); |
| 152 | + dist[0] = 0; |
| 153 | + vis[0] = true; |
| 154 | + while (!d.isEmpty()) { |
| 155 | + int t = d.pollFirst(); |
| 156 | + vis[t] = false; |
| 157 | + for (int i = he[t]; i != -1; i = ne[i]) { |
| 158 | + int j = e[i]; |
| 159 | + if (dist[j] > dist[t] + w[i]) { |
| 160 | + dist[j] = dist[t] + w[i]; |
| 161 | + if (vis[j]) continue; |
| 162 | + d.addLast(j); |
| 163 | + vis[j] = true; |
| 164 | + } |
| 165 | + } |
| 166 | + } |
| 167 | + // 统计答案 |
| 168 | + int ans = 0; |
| 169 | + for (int i = 0; i < n; i++) { |
| 170 | + if (dist[i] <= max) ans++; |
| 171 | + } |
| 172 | + for (int[] info : edges) { |
| 173 | + int a = info[0], b = info[1], c = info[2]; |
| 174 | + int c1 = Math.max(0, max - dist[a]), c2 = Math.max(0, max - dist[b]); |
| 175 | + ans += Math.min(c, c1 + c2); |
| 176 | + } |
| 177 | + return ans; |
| 178 | + } |
| 179 | +} |
| 180 | +``` |
| 181 | +* 时间复杂度:建图复杂度为 $O(m)$;使用 SPFA 求最短路复杂度为 $O(n \times m)$;统计答案复杂度为 $O(n + m)$。整体复杂度为 $O(n \times m)$ |
| 182 | +* 空间复杂度:$O(n + m)$ |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +### 最后 |
| 187 | + |
| 188 | +这是我们「刷穿 LeetCode」系列文章的第 `No.882` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 189 | + |
| 190 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 191 | + |
| 192 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 193 | + |
| 194 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 195 | + |
0 commit comments