8

1298. 你能从盒子里获得的最大糖果数

Hi 大家好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 周赛题解。

这里是第 168 期的第 4 题,也是题目列表中的第 1298 题 -- 『你能从盒子里获得的最大糖果数』

题目描述

给你 n 个盒子,每个盒子的格式为 [status, candies, keys, containedBoxes] ,其中:

  • 状态字 status[i]:整数,如果 box[i] 是开的,那么是 1 ,否则是 0
  • 糖果数 candies[i]: 整数,表示 box[i] 中糖果的数目。
  • 钥匙 keys[i]:数组,表示你打开 box[i] 后,可以得到一些盒子的钥匙,每个元素分别为该钥匙对应盒子的下标。
  • 内含的盒子 containedBoxes[i]:整数,表示放在 box[i] 里的盒子所对应的下标。

给你一个 initialBoxes 数组,表示你现在得到的盒子,你可以获得里面的糖果,也可以用盒子里的钥匙打开新的盒子,还可以继续探索从这个盒子里找到的其他盒子。

请你按照上述规则,返回可以获得糖果的 最大数目

示例 1:

输入:status = [1,0,1,0], candies = [7,5,4,100], keys = [[],[],[1],[]], containedBoxes = [[1,2],[3],[],[]], initialBoxes = [0]
输出:16
解释:
一开始你有盒子 0 。你将获得它里面的 7 个糖果和盒子 1 和 2。
盒子 1 目前状态是关闭的,而且你还没有对应它的钥匙。所以你将会打开盒子 2 ,并得到里面的 4 个糖果和盒子 1 的钥匙。
在盒子 1 中,你会获得 5 个糖果和盒子 3 ,但是你没法获得盒子 3 的钥匙所以盒子 3 会保持关闭状态。
你总共可以获得的糖果数目 = 7 + 4 + 5 = 16 个。

示例 2:

输入: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]
输出:6
解释:
你一开始拥有盒子 0 。打开它你可以找到盒子 1,2,3,4,5 和它们对应的钥匙。
打开这些盒子,你将获得所有盒子的糖果,所以总糖果数为 6 个。

示例 3:

输入:status = [1,1,1], candies = [100,1,100], keys = [[],[0,2],[]], containedBoxes = [[],[],[]], initialBoxes = [1]
输出:1

示例 4:

输入:status = [1], candies = [100], keys = [[]], containedBoxes = [[]], initialBoxes = []
输出:0

示例 5:

输入:status = [1,1,1], candies = [2,3,2], keys = [[],[],[]], containedBoxes = [[],[],[]], initialBoxes = [2,1,0]
输出:7

提示:

  • 1 <= status.length <= 1000
  • status.length == candies.length == keys.length == containedBoxes.length == n
  • status[i] 要么是 0 要么是 1
  • 1 <= candies[i] <= 1000
  • 0 <= keys[i].length <= status.length
  • 0 <= keys[i][j] < status.length
  • keys[i] 中的值都是互不相同的。
  • 0 <= containedBoxes[i].length <= status.length
  • 0 <= containedBoxes[i][j] < status.length
  • containedBoxes[i] 中的值都是互不相同的。
  • 每个盒子最多被一个盒子包含。
  • 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%。

总结

这道题的优化过程写的相对比较详细,目的就是借助这个 HARD 题目来尝试给出一些优化中的思路方向。以及当我们在一个方向上卡住的时候,也许可以尝试重新梳理目的和过程,这时候由于我们已经做了一些方案和优化,可能能比最开始解决的时候得到更好的思路。

相关链接

qrcode_green.jpeg


张小猪粉鼻子
2.9k 声望407 粉丝

一只粉色的小猪,喜欢写写代码,看看电影,玩玩游戏社恐,不太会讲话永远跟不上热点