宝宝也能看懂的 leetcode 周赛 - 168 - 4
Hi 大家好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 题解。
这里是第 168 期的第 4 题,也是题目列表中的第 1298 题 -- 『Maximum Number of Occurrences of a Substring』
题目描述
Given n
boxes, each box is given in the format [status, candies, keys, containedBoxes]
where:
-
status[i]
: an integer which is 1 ifbox[i]
is open and 0 ifbox[i]
is closed. -
candies[i]
: an integer representing the number of candies inbox[i]
. -
keys[i]
: an array contains the indices of the boxes you can open with the key inbox[i]
. -
containedBoxes[i]
: an array contains the indices of the boxes found inbox[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:
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:
Input: status = [1], candies = [100], keys = [[]], containedBoxes = [[]], initialBoxes = []
Output: 0
Example 5:
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 了,可能和测试数据有关。
于是马上改写了一点 concat
和 filter
函数的调用部分。当然,这肯定不会有质变,只是想顺便看一下影响会有多大。代码如下:
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 左右。
再优化
重新整理思路。之前的想法其实比较的规整,即『打开->归类->处理』。但是其实题目没有做任何限制,也就是说我们完全可以在归类的时候就完成处理这个操作。与此同时,处理的数据量也会小很多。因为我们只需要处理当前打开盒子中的内容,而不是之前那样再遍历未使用的东西列表。
基于这个思路,我们来列一下过程:
- 打开一个可打开的盒子
- 拿出内部的糖果做计数
-
拿出内部的盒子
- 如果是没有锁的盒子,直接进入可打开队列
-
如果有锁,检查一下之前的未使用的钥匙
- 如果有钥匙,盒子进入可打开队列,钥匙去掉
- 如果没钥匙,记录该盒子被锁住
-
拿出内部的钥匙
- 检查被锁住的盒子中是否有可以被打开的
对比一下可以发现,这个过程中循环的内容明显少了很多。其中需要被检查的内容多了一个,但是我们可以通过 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%。于是先凑合着这样了。