寶寶也能看懂的 leetcode 周賽 - 168 - 4

寶寶也能看懂的 leetcode 周賽 - 168 - 4

Hi 你們好,我是張小豬。歡迎來到『寶寶也能看懂』系列之 leetcode 題解。git

這裏是第 168 期的第 4 題,也是題目列表中的第 1298 題 -- 『Maximum Number of Occurrences of a Substring』github

題目描述

Given n boxes, each box is given in the format [status, candies, keys, containedBoxes] where:shell

  • status[i]: an integer which is 1 if box[i] is open and 0 if box[i] is closed.
  • candies[i]: an integer representing the number of candies in box[i].
  • keys[i]: an array contains the indices of the boxes you can open with the key in box[i].
  • containedBoxes[i]: an array contains the indices of the boxes found in box[i].

You will start with some boxes given in initialBoxes array. You can take all the candies in any open box and you can use the keys in it to open new boxes and you also can use the boxes you find in it.數組

Return the maximum number of candies you can get following the rules above.函數

Example 1:post

Input: status = [1,0,1,0], candies = [7,5,4,100], keys = [[],[],[1],[]], containedBoxes = [[1,2],[3],[],[]], initialBoxes = [0]
Output: 16
Explanation: You will be initially given box 0. You will find 7 candies in it and boxes 1 and 2. Box 1 is closed and you don't have a key for it so you will open box 2. You will find 4 candies and a key to box 1 in box 2.
In box 1, you will find 5 candies and box 3 but you will not find a key to box 3 so box 3 will remain closed.
Total number of candies collected = 7 + 4 + 5 = 16 candy.

Example 2:測試

Input: status = [1,0,0,0,0,0], candies = [1,1,1,1,1,1], keys = [[1,2,3,4,5],[],[],[],[],[]], containedBoxes = [[1,2,3,4,5],[],[],[],[],[]], initialBoxes = [0]
Output: 6
Explanation: You have initially box 0. Opening it you can find boxes 1,2,3,4 and 5 and their keys. The total number of candies will be 6.

Example 3:優化

Input: status = [1,1,1], candies = [100,1,100], keys = [[],[0,2],[]], containedBoxes = [[],[],[]], initialBoxes = [1]
Output: 1

Example 4:spa

Input: status = [1], candies = [100], keys = [[]], containedBoxes = [[]], initialBoxes = []
Output: 0

Example 5:code

Input: status = [1,1,1], candies = [2,3,2], keys = [[],[],[]], containedBoxes = [[],[],[]], initialBoxes = [2,1,0]
Output: 7

Constraints:

1 <= status.length <= 1000
status.length == candies.length == keys.length == containedBoxes.length == n
status[i] is 0 or 1.
1 <= candies[i] <= 1000
0 <= keys[i].length <= status.length
0 <= keys[i][j] < status.length
All values in keys[i] are unique.
0 <= containedBoxes[i].length <= status.length
0 <= containedBoxes[i][j] < status.length
All values in containedBoxes[i] are unique.
Each box is contained in one box at most.
0 <= initialBoxes.length <= status.length
0 <= initialBoxes[i] < status.length

官方難度

HARD

解決思路

又是一個參數挺多的題,連約束條件都這麼長。總的來講是一個並不複雜的套娃小遊戲。(套娃真的都這麼流行了麼 ~_~)

遊戲的過程就是,有一天,聖誕老人拿給了你幾個盒子,其中每一個盒子打開以後可能會有 3 種東西:

  • 必定數量的糖果
  • 必定數量的鑰匙
  • 必定數量的盒子

糖果,固然很開心啦,雖然是從煙囪裏進來的;
鑰匙,固然不是別人家的啦,是用來打開上鎖的盒子;
盒子,也就是套娃,有可能被鎖住了,須要用鑰匙來打開。

至於打開以後的話...『有一天,聖誕老人拿給了你幾個盒子...』...直到咱們無盒可開。最終返回咱們能拿到的糖糖的數量便可。

看完題目內容,我其實有點驚異。竟然不是對於初始盒子和開盒狀況加必定的限制條件,而後讓咱們給出可以獲得最多糖的初始盒子方案。不是很喜歡出這樣的題麼 >.<

那麼對於如今的題目,其實咱們不難發現,整個過程是沒有變數的,由於全部的參數和條件都已經提供給咱們了。咱們所須要作的僅僅是根據遊戲玩法推演出過程,並最終統計出可以獲取的糖的數量。那麼基於這道題,我就寫多一點一步一步的優化過程。

直接方案

因爲上面已經羅列了遊戲的玩法和基本思路,那麼咱們這裏就直接基於玩法來實現相關的代碼:

const maxCandies = (status, candies, keys, containedBoxes, initialBoxes) => {
  const queue = initialBoxes;
  const closedBoxes = new Uint8Array(1000);
  let unusedKeys = [];
  let ret = 0;
  for (let i = 0; i < queue.length; ++i) {
    const cur = queue[i];
    ret += candies[cur];
    for (const box of containedBoxes[cur]) {
      status[box] === 1 ? queue.push(box) : (closedBoxes[box] = 1);
    }
    unusedKeys = unusedKeys.concat(keys[cur]).filter(key => {
      if (closedBoxes[key] === 0) return true;
      closedBoxes[key] = 0;
      queue.push(key);
    });
  }
  return ret;
};

這是我最開始寫的比較無腦的直接過程。若是拿這段代碼去提交,就會發現什麼是擦邊低空飄過。時間整整花了 2200ms 多。好吧,我知道爲何這道沒有變數的題被放進 HARD 了,可能和測試數據有關。

因而立刻改寫了一點 concatfilter 函數的調用部分。固然,這確定不會有質變,只是想順便看一下影響會有多大。代碼以下:

const maxCandies = (status, candies, keys, containedBoxes, initialBoxes) => {
  const queue = initialBoxes;
  const closedBoxes = new Uint8Array(1000);
  let unusedKeys = [];
  let ret = 0;
  for (let i = 0; i < queue.length; ++i) {
    const cur = queue[i];
    ret += candies[cur];
    for (const box of containedBoxes[cur]) {
      status[box] === 1 ? queue.push(box) : (closedBoxes[box] = 1);
    }
    const leftKeys = [];
    unusedKeys.push(...keys[cur]);
    for (const key of unusedKeys) {
      if (closedBoxes[key] === 0) {
        leftKeys.push(key);
      } else {
        closedBoxes[key] = 0;
        queue.push(key);
      }
    }
    unusedKeys = leftKeys;
  }
  return ret;
};

提交後時間來到了 1600ms 多。影響比我預期的明顯。不過具體工做中的 production 代碼,你們仍是根據具體狀況酌情考慮吧。接下來開始正式的優化。

優化

回看上面的代碼,咱們會發現對於等待被打開的盒子們,每次打開一個盒子,咱們都會去完整的遍歷未使用的鑰匙。這樣的效率實際上是很低的,由於每次增量的鎖住盒子實際上是不多的。那麼最簡單的作法就是,咱們能夠把同一批的盒子都打開,而後再統一作處理和校驗。代碼以下:

const maxCandies = (status, candies, keys, containedBoxes, initialBoxes) => {
  const closedBoxes = new Uint8Array(1000);
  let queue = initialBoxes;
  let unusedKeys = [];
  let ret = 0;
  while (queue.length) {
    const next = [];
    for (const cur of queue) {
      ret += candies[cur];
      for (const box of containedBoxes[cur]) {
        status[box] === 1 ? next.push(box) : (closedBoxes[box] = 1);
      }
      unusedKeys.push(...keys[cur]);
    }
    const leftKeys = [];
    for (const key of unusedKeys) {
      closedBoxes[key] === 0 ? leftKeys.push(key) : (closedBoxes[key] = 0, next.push(key));
    }
    unusedKeys = leftKeys;
    queue = next;
  }
  return ret;
};

這下時間有了數量級的變化,成了 160ms。

固然這裏還能夠有一點小優化,咱們對於從當前一批盒子中拿到的鑰匙,每次都所有放進了未使用的鑰匙的數組,而後再作處理。其實能夠分開,從而節省數據轉移的時間。代碼以下:

const maxCandies = (status, candies, keys, containedBoxes, initialBoxes) => {
  const closedBoxes = new Uint8Array(1000);
  let queue = initialBoxes;
  let unusedKeys = [];
  let ret = 0;
  while (queue.length) {
    const next = [];
    for (const cur of queue) {
      ret += candies[cur];
      for (const box of containedBoxes[cur]) {
        status[box] === 1 ? next.push(box) : (closedBoxes[box] = 1);
      }
    }
    const leftKeys = [];
    for (const key of unusedKeys) {
      closedBoxes[key] === 0 ? leftKeys.push(key) : ((closedBoxes[key] = 0), next.push(key));
    }
    for (const cur of queue) {
      for (const key of keys[cur]) {
        closedBoxes[key] === 0 ? leftKeys.push(key) : ((closedBoxes[key] = 0), next.push(key));
      }
    }
    unusedKeys = leftKeys;
    queue = next;
  }
  return ret;
};

時間稍微縮短到了 130ms 左右。

再優化

從新整理思路。以前的想法其實比較的規整,即『打開->歸類->處理』。可是其實題目沒有作任何限制,也就是說咱們徹底能夠在歸類的時候就完成處理這個操做。與此同時,處理的數據量也會小不少。由於咱們只須要處理當前打開盒子中的內容,而不是以前那樣再遍歷未使用的東西列表。

基於這個思路,咱們來列一下過程:

  1. 打開一個可打開的盒子
  2. 拿出內部的糖果作計數
  3. 拿出內部的盒子

    • 若是是沒有鎖的盒子,直接進入可打開隊列
    • 若是有鎖,檢查一下以前的未使用的鑰匙

      • 若是有鑰匙,盒子進入可打開隊列,鑰匙去掉
      • 若是沒鑰匙,記錄該盒子被鎖住
  4. 拿出內部的鑰匙

    • 檢查被鎖住的盒子中是否有能夠被打開的

對比一下能夠發現,這個過程當中循環的內容明顯少了不少。其中須要被檢查的內容多了一個,可是咱們能夠經過 Map 作到 O(1),因此徹底不是問題。而且因爲不存在對於歷史內容的遍歷,因此咱們也就不用再去合併一批一批的操做,直接單個操做便可。

基於以上分析,咱們能夠寫出相似這樣的代碼:

const maxCandies = (status, candies, keys, containedBoxes, initialBoxes) => {
  const closedBoxes = new Uint8Array(1000);
  const unusedKeys = new Uint8Array(1000);
  const queue = initialBoxes;
  let ret = 0;
  for (let i = 0; i < queue.length; ++i) {
    const cur = queue[i];
    ret += candies[cur];
    for (const box of containedBoxes[cur]) {
      if (status[box] === 1) {
        queue.push(box);
      } else if (unusedKeys[box] === 1) {
        queue.push(box);
        unusedKeys[box] = 0;
      } else {
        closedBoxes[box] = 1;
      }
    }
    for (const key of keys[cur]) {
      if (closedBoxes[key] === 0) {
        unusedKeys[key] = 1;
      } else {
        closedBoxes[key] = 0;
        queue.push(key);
      }
    }
  }
  return ret;
};

時間縮短到了 90ms 多。初見成效。

再再優化

魯迅可能說過,「全部的初見成效以後,必定還有更多內容」。因而咱們固然不會就此知足。

回看上面的代碼,其中有一個 unusedKeys 看起來十分不爽。它的做用就是單純的爲了記錄沒有使用過的鑰匙。那麼咱們是否有什麼方法把它去掉呢?

咱們能夠注意到,在處理新的盒子的時候,咱們用到了 status[box] === 1 這個判斷,也用到了 unusedKeys[box] === 1 這個判斷。本質是由於前者只是最初的情況,並不夠完整,因此咱們須要後者來補充。說到這裏,相信你們已經注意到了這一點,那就是爲何咱們不繼續更新 status 的狀態呢?

基於此咱們能夠獲得相似下面的代碼:

const maxCandies = (status, candies, keys, containedBoxes, initialBoxes) => {
  const closedBoxes = new Uint8Array(1000);
  const queue = initialBoxes;
  let ret = 0;
  for (let i = 0; i < queue.length; ++i) {
    const cur = queue[i];
    ret += candies[cur];
    for (const box of containedBoxes[cur]) {
      status[box] === 1 ? queue.push(box) : (closedBoxes[box] = 1);
    }
    for (const key of keys[cur]) {
      if (closedBoxes[key] === 0) {
        status[key] = 1;
      } else {
        closedBoxes[key] = 0;
        queue.push(key);
      }
    }
  }
  return ret;
};

那麼,咱們繼續。這個 closedBoxes 是否是也看着很不爽?它的做用就是單純的爲了記錄鎖住的還沒找到鑰匙的盒子。那麼咱們來試試把它也去掉吧。

咱們能夠注意到,對於這個數組的使用,是做爲 status 狀態更新的依據。而咱們的 status 狀態目前只有兩個,0 表示鎖住了,1 表示能夠打開。那麼問題的本質實際上是,咱們目前沒有辦法表示『咱們已經拿到了這個盒子,可是尚未鑰匙』的這個狀態。那麼爲何咱們不添加這麼一個狀態呢?反正 status 的狀態更新已經由咱們本身維護了。

基於此咱們能夠獲得相似下面的代碼:

const maxCandies = (status, candies, keys, containedBoxes, initialBoxes) => {
  const queue = initialBoxes;
  let ret = 0;
  for (let i = 0; i < queue.length; ++i) {
    ret += candies[queue[i]];
    for (const box of containedBoxes[queue[i]]) {
      status[box] === 1 ? queue.push(box) : (status[box] = -1);
    }
    for (const key of keys[queue[i]]) {
      status[key] === -1 ? (status[key] = 0, queue.push(key)) : (status[key] = 1);
    }
  }
  return ret;
};

這一段代碼我跑到了 68ms,暫時 beats 100%。因而先湊合着這樣了。

相關連接

相關文章
相關標籤/搜索