宝宝也能看懂的 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%。因而先凑合着这样了。

相关连接

相关文章
相关标签/搜索