寶寶也能看懂的 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)));

友情提示:生產環境中不要這樣寫哈,被打死我不負責,哈哈哈哈。

總結

這道題中咱們能夠了解到深度優先遍歷和廣度優先遍歷這兩種遍歷思路,以及在具體的場景中咱們還能夠作的一些小優化。這兩種遍歷方式在之後應該會挺常見的。最後再強調一下,一行的那個寫法真的會被同事打死的,哈哈哈哈嗝 >.<

相關連接

相關文章
相關標籤/搜索