|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1345. 跳跃游戏 IV](https://leetcode-cn.com/problems/jump-game-iv/solution/gong-shui-san-xie-noxiang-xin-ke-xue-xi-q9tb1/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「图论 BFS」、「双向 BFS」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个整数数组 `arr` ,你一开始在数组的第一个元素处(下标为 `0`)。 |
| 10 | + |
| 11 | +每一步,你可以从下标 i 跳到下标: |
| 12 | +* `i + 1` 满足:`i + 1 < arr.length` |
| 13 | +* `i - 1` 满足:`i - 1 >= 0` |
| 14 | +* `j` 满足:`arr[i] == arr[j]` 且 `i != j` |
| 15 | + |
| 16 | +请你返回到达数组最后一个元素的下标处所需的 **最少操作次数** 。 |
| 17 | + |
| 18 | +注意:任何时候你都不能跳到数组外面。 |
| 19 | + |
| 20 | +示例 1: |
| 21 | +``` |
| 22 | +输入:arr = [100,-23,-23,404,100,23,23,23,3,404] |
| 23 | + |
| 24 | +输出:3 |
| 25 | + |
| 26 | +解释:那你需要跳跃 3 次,下标依次为 0 --> 4 --> 3 --> 9 。下标 9 为数组的最后一个元素的下标。 |
| 27 | +``` |
| 28 | +示例 2: |
| 29 | +``` |
| 30 | +输入:arr = [7] |
| 31 | + |
| 32 | +输出:0 |
| 33 | + |
| 34 | +解释:一开始就在最后一个元素处,所以你不需要跳跃。 |
| 35 | +``` |
| 36 | +示例 3: |
| 37 | +``` |
| 38 | +输入:arr = [7,6,9,6,9,6,9,7] |
| 39 | + |
| 40 | +输出:1 |
| 41 | + |
| 42 | +解释:你可以直接从下标 0 处跳到下标 7 处,也就是数组的最后一个元素处。 |
| 43 | +``` |
| 44 | +示例 4: |
| 45 | +``` |
| 46 | +输入:arr = [6,1,9] |
| 47 | + |
| 48 | +输出:2 |
| 49 | +``` |
| 50 | +示例 5: |
| 51 | +``` |
| 52 | +输入:arr = [11,22,7,7,7,7,7,7,7,22,13] |
| 53 | + |
| 54 | +输出:3 |
| 55 | +``` |
| 56 | + |
| 57 | +提示: |
| 58 | +* 1ドル <= arr.length <= 5 * 10^4$ |
| 59 | +* $-10^8 <= arr[i] <= 10^8$ |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +### 单向 BFS |
| 64 | + |
| 65 | +根据跳跃规则,我们能够进行「前后跳」和「等值跳」,问题为到达结尾位置的最少步数,容易想到 `BFS`。 |
| 66 | + |
| 67 | +为了方便进行「等值跳」,我们可以先使用「哈希表」记录某个值有哪些下标。 |
| 68 | + |
| 69 | +在进行 `BFS` 时,假如当前走到的位置为 $t,ドル我们尝试将 $t - 1$、$t + 1$ 和与 $arr[t]$ 等值的位置进行入队,为了防止重复同队,我们可以使用 $dist$ 数组记录到达某个位置的最小步数(初始化为 `INF`),只有 $dist[ne]$ 为 `INF` 时,该点没有被遍历过,可以入队并更新最小步数。 |
| 70 | + |
| 71 | +但光使用 $dist$ 还不能确保复杂度为 $O(n),ドル因为每次都需要遍历与 $arr[t]$ 等值的下标,为确保等值下标的遍历只会发生一次,我们需要在将等值下标添加到队列后,将 $arr[t]$ 从哈希表中移除。 |
| 72 | + |
| 73 | +容易证明每次将于 $arr[t]$ 的等值元素添加到队列后,将 $arr[t]$ 从哈希表中移除的正确性: |
| 74 | + |
| 75 | +首次检索到 $arr[t]$ 值时,必然是最小步数,记为 $step,ドル此时 `BFS` 做法将其他等值下标距离更新为 $step + 1$: |
| 76 | + |
| 77 | +* 若 $arr[t]$ 与结尾元素值相等,且 $t$ 为 $n - 1,ドル此时 $step$ 即是答案; |
| 78 | +* 若 $arr[t]$ 与结尾元素值相等,但 $t$ 不为 $n - 1,ドル此时会再跳一步到达结尾位置,即 $step + 1$ 为答案。那么是否可能存在使用比 $step + 1$ 更小的步数,也能到达结尾的位置呢? |
| 79 | + 答案是:**可能存在,但如果最后是通过「等值跳」到达结尾位置的话,不可能存在比 $step + 1$ 更小的步数。** |
| 80 | + 由于我们每次加入等值时都会进行哈希表的移除,因此到达 $t$ 的方式不可能是「等值跳」,而只能是「前后跳」。 |
| 81 | + |
| 82 | + * 假设是通过前跳到达位置 $t,ドル即点分布如图,步数满足等于 $step + 1$: |
| 83 | + |
| 84 | +  |
| 85 | + |
| 86 | + * 假设是通过后跳到达位置 $t,ドル即点分布如图,步数满足「如果是等值跳到达结尾,步数为 $step + 1$」: |
| 87 | + |
| 88 | +  |
| 89 | + |
| 90 | + **综上,如果 $n - 1$ 是经过「等值跳」加入队列的话,起所能达到的最小步数必然为发起点 $t$ 的最小步数 $+1$。** |
| 91 | + |
| 92 | + **也就是说,即使首次等值跳,加入队列后会将其从哈希表中进行移除,正确性也是可以保证的。** |
| 93 | + |
| 94 | +基于此,我们可以额外增加一个 trick,就是在构建哈希表的时候,使用「倒序」的形式构建等值下标列表,这样可以确保如果最后位置是通过「等值跳」而来是,能够优先出队。 |
| 95 | + |
| 96 | +**代码(感谢 [@Benhao](/u/himymben/) 和 [@🍭可乐可乐吗](/u/littletime_cc/) 同学提供的其他语言版本):** |
| 97 | +```Java |
| 98 | +class Solution { |
| 99 | + int INF = 0x3f3f3f3f; |
| 100 | + public int minJumps(int[] arr) { |
| 101 | + int n = arr.length; |
| 102 | + Map<Integer, List<Integer>> map = new HashMap<>(); |
| 103 | + // 倒序插入 list,相当于给 deque 增加一个同层「下标越大,优先出队」的作用 |
| 104 | + for (int i = n - 1; i >= 0; i--) { |
| 105 | + List<Integer> list = map.getOrDefault(arr[i], new ArrayList<>()); |
| 106 | + list.add(i); |
| 107 | + map.put(arr[i], list); |
| 108 | + } |
| 109 | + int[] dist = new int[n]; |
| 110 | + Arrays.fill(dist, INF); |
| 111 | + Deque<Integer> d = new ArrayDeque<>(); |
| 112 | + d.addLast(0); |
| 113 | + dist[0] = 0; |
| 114 | + while (!d.isEmpty()) { |
| 115 | + int t = d.pollFirst(), step = dist[t]; |
| 116 | + if (t == n - 1) return step; |
| 117 | + if (t + 1 < n && dist[t + 1] == INF) { |
| 118 | + d.addLast(t + 1); |
| 119 | + dist[t + 1] = step + 1; |
| 120 | + } |
| 121 | + if (t - 1 >= 0 && dist[t - 1] == INF) { |
| 122 | + d.addLast(t - 1); |
| 123 | + dist[t - 1] = step + 1; |
| 124 | + } |
| 125 | + List<Integer> list = map.getOrDefault(arr[t], new ArrayList<>()); |
| 126 | + for (int ne : list) { |
| 127 | + if (dist[ne] == INF) { |
| 128 | + d.addLast(ne); |
| 129 | + dist[ne] = step + 1; |
| 130 | + } |
| 131 | + } |
| 132 | + map.remove(arr[t]); |
| 133 | + } |
| 134 | + return -1; // never |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | +- |
| 139 | +```C++ |
| 140 | +class Solution { |
| 141 | +public: |
| 142 | + int minJumps(vector<int>& arr) { |
| 143 | + const int inf = 0x3f3f3f3f; |
| 144 | + int n = arr.size(); |
| 145 | + unordered_map<int, vector<int>> map; |
| 146 | + for(int i = n - 1; ~i; i--) { |
| 147 | + map[arr[i]].push_back(i); |
| 148 | + } |
| 149 | + vector<int> dist(n, inf); |
| 150 | + queue<int> q; |
| 151 | + q.push(0); |
| 152 | + dist[0] = 0; |
| 153 | + while(q.size()) { |
| 154 | + auto t = q.front(), step = dist[t]; |
| 155 | + q.pop(); |
| 156 | + if(t == n - 1) return step; |
| 157 | + if(t + 1 < n and dist[t + 1] == inf) { |
| 158 | + q.push(t + 1); |
| 159 | + dist[t + 1] = step + 1; |
| 160 | + } |
| 161 | + if(t - 1 >= 0 and dist[t - 1] == inf) { |
| 162 | + q.push(t - 1); |
| 163 | + dist[t - 1] = step + 1; |
| 164 | + } |
| 165 | + const auto& list = map[arr[t]]; |
| 166 | + for(auto ne :list) { |
| 167 | + if(dist[ne] == inf) { |
| 168 | + q.push(ne); |
| 169 | + dist[ne] = step + 1; |
| 170 | + } |
| 171 | + } |
| 172 | + map[arr[t]].clear(); //or map.erase(arr[t]); |
| 173 | + } |
| 174 | + return -1; |
| 175 | + } |
| 176 | +}; |
| 177 | +``` |
| 178 | +- |
| 179 | +```python |
| 180 | +class Solution: |
| 181 | + def minJumps(self, arr: List[int]) -> int: |
| 182 | + n = len(arr) |
| 183 | + mp = defaultdict(list) |
| 184 | + for i, num in enumerate(arr): |
| 185 | + mp[num].append(i) |
| 186 | + dist = [inf] * n |
| 187 | + d = deque([0]) |
| 188 | + dist[0] = 0 |
| 189 | + while len(d) > 0: |
| 190 | + t = d.popleft() |
| 191 | + step = dist[t] |
| 192 | + if t == n - 1: |
| 193 | + return step |
| 194 | + for ne in mp[arr[t]]: |
| 195 | + if dist[ne] == inf: |
| 196 | + d.append(ne) |
| 197 | + dist[ne] = step + 1 |
| 198 | + mp.pop(arr[t]) |
| 199 | + if dist[t + 1] == inf: |
| 200 | + d.append(t + 1) |
| 201 | + dist[t + 1] = step + 1 |
| 202 | + if t and dist[t - 1] == inf: |
| 203 | + d.append(t - 1) |
| 204 | + dist[t - 1] = step + 1 |
| 205 | + return -1 |
| 206 | +``` |
| 207 | +- |
| 208 | +```Go |
| 209 | +const INF int = 0x3f3f3f3f |
| 210 | +func minJumps(arr []int) int { |
| 211 | + n := len(arr) |
| 212 | + mp := map[int][]int{} |
| 213 | + dist := make([]int, len(arr)) |
| 214 | + for i := 0; i < n; i++{ |
| 215 | + list := mp[arr[i]] |
| 216 | + list = append(list, i) |
| 217 | + mp[arr[i]] = list |
| 218 | + dist[i] = INF |
| 219 | + } |
| 220 | + d := []int{0} |
| 221 | + dist[0] = 0 |
| 222 | + for len(d) > 0{ |
| 223 | + t := d[0] |
| 224 | + step := dist[t] |
| 225 | + if t == n - 1{ |
| 226 | + return step |
| 227 | + } |
| 228 | + d = d[1:] |
| 229 | + list := mp[arr[t]] |
| 230 | + delete(mp, arr[t]) |
| 231 | + list = append(list, t + 1) |
| 232 | + if t > 0 { |
| 233 | + list = append(list, t - 1) |
| 234 | + } |
| 235 | + for _, ne := range list { |
| 236 | + if dist[ne] == INF { |
| 237 | + dist[ne] = step + 1 |
| 238 | + d = append(d, ne) |
| 239 | + } |
| 240 | + } |
| 241 | + } |
| 242 | + return -1 |
| 243 | +} |
| 244 | +``` |
| 245 | +* 时间复杂度:预处理出 `map` 的复杂度为 $O(n)$;跑一遍 `BFS` 得到答案复杂度为 $O(n)$。整体复杂度为 $O(n)$ |
| 246 | +* 空间复杂度:$O(n)$ |
| 247 | + |
| 248 | +--- |
| 249 | + |
| 250 | +### 双向 BFS |
| 251 | + |
| 252 | +自然也能够使用「双向 `BFS`」进行求解。 |
| 253 | + |
| 254 | +不了解「双向 `BFS`」的同学,可以先看前置🧀:[【图论搜索专题】如何使用「双向 BFS」解决搜索空间爆炸问题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&token=1446568490&lang=zh_CN#rd) & [【图论搜索专题】双向 BFS 模板题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489502&idx=1&sn=dc863d4bc71c4739a4799b9a4558bd01&chksm=fd9cbcc1caeb35d749d0d72f485485527482c27b608c8f4062c29a997ede97a09ce598b58c7f&token=1446568490&lang=zh_CN#rd) 。 |
| 255 | + |
| 256 | +双向 `BFS` 能够有效解决搜索空间爆炸问题,本题使用双向 `BFS` 的话,可以不进行哈希表的 `remove` 操作。 |
| 257 | + |
| 258 | +代码: |
| 259 | +```Java |
| 260 | +class Solution { |
| 261 | + int[] arr; |
| 262 | + int INF = 0x3f3f3f3f; |
| 263 | + int n; |
| 264 | + Map<Integer, List<Integer>> map = new HashMap<>(); |
| 265 | + public int minJumps(int[] _arr) { |
| 266 | + arr = _arr; |
| 267 | + n = arr.length; |
| 268 | + if (n == 1) return 0; |
| 269 | + for (int i = n - 1; i >= 0; i--) { |
| 270 | + List<Integer> list = map.getOrDefault(arr[i], new ArrayList<>()); |
| 271 | + list.add(i); |
| 272 | + map.put(arr[i], list); |
| 273 | + } |
| 274 | + Deque<Integer> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>(); |
| 275 | + int[] dist1 = new int[n], dist2 = new int[n]; |
| 276 | + Arrays.fill(dist1, INF); |
| 277 | + Arrays.fill(dist2, INF); |
| 278 | + d1.addLast(0); |
| 279 | + dist1[0] = 0; |
| 280 | + d2.addLast(n - 1); |
| 281 | + dist2[n - 1] = 0; |
| 282 | + while (!d1.isEmpty() && !d2.isEmpty()) { |
| 283 | + int t = -1; |
| 284 | + if (d1.size() < d2.size()) t = update(d1, d2, dist1, dist2); |
| 285 | + else t = update(d2, d1, dist2, dist1); |
| 286 | + if (t != -1) return t; |
| 287 | + } |
| 288 | + return -1; // never |
| 289 | + } |
| 290 | + int update(Deque<Integer> d1, Deque<Integer> d2, int[] dist1, int[] dist2) { |
| 291 | + int t = d1.pollFirst(), step = dist1[t]; |
| 292 | + if (t + 1 < n) { |
| 293 | + if (dist2[t + 1] != INF) return step + 1 + dist2[t + 1]; |
| 294 | + if (dist1[t + 1] == INF) { |
| 295 | + d1.addLast(t + 1); |
| 296 | + dist1[t + 1] = step + 1; |
| 297 | + } |
| 298 | + } |
| 299 | + if (t - 1 >= 0) { |
| 300 | + if (dist2[t - 1] != INF) return step + 1 + dist2[t - 1]; |
| 301 | + if (dist1[t - 1] == INF) { |
| 302 | + d1.addLast(t - 1); |
| 303 | + dist1[t - 1] = step + 1; |
| 304 | + } |
| 305 | + } |
| 306 | + List<Integer> list = map.getOrDefault(arr[t], new ArrayList<>()); |
| 307 | + for (int ne : list) { |
| 308 | + if (dist2[ne] != INF) return step + 1 + dist2[ne]; |
| 309 | + if (dist1[ne] == INF) { |
| 310 | + d1.addLast(ne); |
| 311 | + dist1[ne] = step + 1; |
| 312 | + } |
| 313 | + } |
| 314 | + map.remove(arr[t]); |
| 315 | + return -1; |
| 316 | + } |
| 317 | +} |
| 318 | +``` |
| 319 | +* 时间复杂度:$O(n)$ |
| 320 | +* 空间复杂度:$O(n)$ |
| 321 | + |
| 322 | +--- |
| 323 | + |
| 324 | +### 其他「图论搜索 / 模拟」内容 |
| 325 | + |
| 326 | +题太简单?不如来学习热乎的 [简单图论搜索题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247490011&idx=1&sn=4c8cbd5ad858f571291d47fcef75e75b&chksm=fd9cb2c4caeb3bd2ac442b2d4d1417e8eb6d65b1feca8399179951ebfa132e8a97a3935e7498&token=252055586&lang=zh_CN#rd) 🍭🍭🍭 |
| 327 | + |
| 328 | +* [常规 BFS(二维转一维)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489621&idx=1&sn=5d43fb97bc167a50a7aeb4ae2068571c&chksm=fd9cb34acaeb3a5c7e1e2e2a88d460ae2418a3cef615e1abf017b5d58aa1e7f490856d67f800&token=2136593799&lang=zh_CN#rd) |
| 329 | +* [常规 BFS/迭代加深(结合二叉树)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489746&idx=2&sn=9e80b33c12e96369c7a770382a97adbb&chksm=fd9cb3cdcaeb3adb35c708e548851e419b00e41801c98cae146ba29f5bdc49370a43cddf668d&token=252055586&lang=zh_CN#rd) |
| 330 | +* [多源 BFS](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487179&idx=1&sn=e30a662c03fba3861254dbcf3fb9d6f2&chksm=fd9ca5d4caeb2cc205804fd17a2ce86b25d0408adc3417e73154f59d37e7cb17e02374f5122c&scene=178&cur_album_id=1917113998693449732#rd) |
| 331 | +* [双向 BFS](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489502&idx=1&sn=dc863d4bc71c4739a4799b9a4558bd01&chksm=fd9cbcc1caeb35d749d0d72f485485527482c27b608c8f4062c29a997ede97a09ce598b58c7f&scene=178&cur_album_id=1917113998693449732#rd) |
| 332 | +* [双向 BFS II](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&scene=178&cur_album_id=1917113998693449732#rd) |
| 333 | +* [双向 BFS III(结合并查集)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489671&idx=1&sn=c0f64de1a5e4613675f73d2ae43d0708&chksm=fd9cb398caeb3a8eae334c89dee17711fca43a00d93cf63a623792f3aac0c8bf586b4be9cc47&token=2074150457&lang=zh_CN#rd) |
| 334 | +* [灵活运用多种搜索方式(启发式)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489560&idx=2&sn=bb966d868c18d656620a20d31a425b23&chksm=fd9cb307caeb3a11424428f0a88e7f0cb86bb53b3e5a2b9e28683a24bcb3ac151655d2b6419e&scene=178&cur_album_id=1917113998693449732#rd) |
| 335 | +* [灵活运用多种搜索方式 II(启发式)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489588&idx=1&sn=479e4c0627247ab7e20af7909f2a8b64&chksm=fd9cb32bcaeb3a3d4f0bd73f023a92a165edabf212af1db9672a55bed1af7d4e32e8af9964c3&scene=178&cur_album_id=1917113998693449732#rd) |
| 336 | +* [灵活运用多种搜索方式 III(启发式 结合状态压缩)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489985&idx=1&sn=e503ce6ece048062f1d9ebee2572838a&chksm=fd9cb2decaeb3bc8c635c4a6cf0e78d5973723bb6c89a64875828435dc5b90ef07874ef7a6ae&token=252055586&lang=zh_CN#rd) |
| 337 | + |
| 338 | +--- |
| 339 | + |
| 340 | +### 最后 |
| 341 | + |
| 342 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1345` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 343 | + |
| 344 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 345 | + |
| 346 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 347 | + |
| 348 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 349 | + |
0 commit comments