宝宝也能看懂的 leetcode 周赛 - 169 - 3

1306. Jump Game III

Hi 你们好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 题解。git

这里是第 169 期的第 3 题,也是题目列表中的第 1306 题 -- 『Jump Game III』github

题目描述

Given an array of non-negative integers arr, you are initially positioned at start index of the array. When you are at index i, you can jump to i + arr[i] or i - arr[i], check if you can reach to any index with value 0.shell

Notice that you can not jump outside of the array at any time.数组

Example 1:数据结构

Input: arr = [4,2,3,0,3,1,2], start = 5
Output: true
Explanation:
All possible ways to reach at index 3 with value 0 are:
index 5 -> index 4 -> index 1 -> index 3
index 5 -> index 6 -> index 4 -> index 1 -> index 3

Example 2:ide

Input: arr = [4,2,3,0,3,1,2], start = 0
Output: true
Explanation:
One possible way to reach at index 3 with value 0 is:
index 0 -> index 4 -> index 1 -> index 3

Example 3:post

Input: arr = [3,0,2,1,2], start = 2
Output: false
Explanation: There is no way to reach at index 1 with value 0.

Constraints:优化

1 <= arr.length <= 5 * 10^4
0 <= arr[i] < arr.length
0 <= start < arr.length

官方难度

MEDIUMspa

解决思路

题目的内容是一个小游戏,能够想象这样一个场景:code

  1. 面前放着几张纸牌,每一个纸牌下面写着一个数字
  2. 游戏开始时,做为咱们起始位置的纸牌已经肯定啦
  3. 把纸牌翻过来,看到后面的数字为 n,那么咱们如今能够选择向左走 n 步或者向右走 n 步,可是不能超出纸牌的范围
  4. 不断的重复这个过程,直到咱们遇到翻过来的数字为 0 咱们就成功啦
  5. 若是不管如何都找不到 0,那么咱们就失败啦

那么回到题目中,纸牌和背后的数字是一个给定的由非负整数组成的数组,起始位置是给定的一个下标,咱们须要返回 true 或者 false

咱们先想想,若是是玩这个游戏的话,咱们会怎么玩呢?因为出发点是肯定的,而结束的点不肯定,由于可能会有多个 0 的存在,因此咱们能够从出发点开始不断的作尝试。基于此咱们能够获得两种思路:

  • 遇到了选择左右的状况时,咱们把两种状况都记录下来,而后继续针对全部已经记录的内容逐个继续尝试,不过须要注意循环的状况。
  • 遇到了选择左右的状况时,先选择一个方向,而后继续走下去,直到发生了循环再退到上一个选择点从新选择。

其实上述两种思路也就对应了两种很常见的遍历思路,即广度优先遍历和深度优先遍历。这部份内容我会在数据结构的新坑中详细介绍。

广度优先遍历

基于上面的第一种思路,咱们用到了一个 visited 集合来判断是否已经访问过,从而规避循环。同时咱们用一个 queue 来保存全部记录节点,方便基于延伸。具体代码以下:

const canReach = (arr, start) => {
  const visited = new Set();
  const queue = [start];
  for (let len = 0, max = arr.length; len < queue.length; ++len) {
    const idx = queue[len];
    if (visited.has(idx)) continue;
    if (arr[idx] === 0) return true;
    visited.add(idx);
    idx + arr[idx] < max && queue.push(idx + arr[idx]);
    idx - arr[idx] >= 0 && queue.push(idx - arr[idx]);
  }
  return false;
};

这里算是一个很是常见的广度优先遍历的实现模板了,不过具体到这道题其实还能够再优化一下。咱们能够注意到题目的限制条件里,arr 的每一个值取值范围是 [0, arr.length]。基于此,咱们能够经过赋值为一个范围外的特殊值来标识已经访问过,从而去掉 visited 集合的使用。我这里直接使用了 -1 做为特殊值,具体代码以下:

const canReach = (arr, start) => {
  const queue = [start];
  for (let len = 0, max = arr.length; len < queue.length; ++len) {
    const idx = queue[len];
    if (arr[idx] === -1) continue;
    if (arr[idx] === 0) return true;
    idx + arr[idx] < max && queue.push(idx + arr[idx]);
    idx - arr[idx] >= 0 && queue.push(idx - arr[idx]);
    arr[idx] = -1;
  }
  return false;
};

深度优先遍历

基于上面的第二种思路,咱们能够经过基于递归的方式来实现不断的层层深刻,以及遇到循环后回退。具体代码以下:

const canReach = (arr, start) => {
  const val = arr[start];
  if (val === 0) return true;
  if (val === -1) return false;
  arr[start] = -1;
  return (start - val >= 0 && canReach(arr, start - val)) || (start + val < arr.length && canReach(arr, start + val));
};

递归实现的一个很明显的好处就是,看起来代码量更少了。特别适合懒懒的张小猪本猪,哈哈哈哈 >.<

另外值得注意的一点是,咱们这里没有用到那个额外的 queue 去记录。那么是否咱们的额外空间复杂度就是 O(1) 了呢?其实并非的。由于咱们实际上是经过递归的调用栈来变相的记录了路径和回退过程。因此,在考虑额外的空间复杂度的时候,咱们须要把递归的调用栈考虑进去,也就是说这里的空间复杂度其实仍是 O(n)。

最后,利用到参数的默认值能够进行运算、逻辑运算符、以及逗号表达式,咱们能够把上述代码变成一行实现,具体以下:

const canReach = (arr, start, val = arr[start]) => val === 0 || (arr[start] = -1, val !== -1) && ((start - val >= 0 && canReach(arr, start - val)) || (start + val < arr.length && canReach(arr, start + val)));

友情提示:生产环境中不要这样写哈,被打死我不负责,哈哈哈哈。

总结

这道题中咱们能够了解到深度优先遍历和广度优先遍历这两种遍历思路,以及在具体的场景中咱们还能够作的一些小优化。这两种遍历方式在之后应该会挺常见的。最后再强调一下,一行的那个写法真的会被同事打死的,哈哈哈哈嗝 >.<

相关连接

相关文章
相关标签/搜索