Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
不是因为看到了希望才坚持,而是因为坚持了才能看到希望。共勉
每日刷题第56天 2021.03.06
2100. 适合打劫银行的日子
- leetcode原题链接:leetcode-cn.com/problems/fi…
- 难度:中等
- 方法:滑动窗口、前缀和运用
题目描述
- 你和一群强盗准备打劫银行。给你一个下标从
0开始的整数数组security,其中security[i]是第i天执勤警卫的数量。日子从0开始编号。同时给你一个整数time。 - 如果第
i天满足以下所有条件,我们称它为一个适合打劫银行的日子: - 第
i天前和后都分别至少有time天。 - 第
i天前连续time天警卫数目都是非递增的。 - 第
i天后连续time天警卫数目都是非递减的。 - 更正式的,第
i天是一个合适打劫银行的日子当且仅当:security[i - time] >= security[i - time + 1] >= ... >= security[i] <= ... <= security[i + time - 1] <= security[i + time]. - 请你返回一个数组,包含 所有 适合打劫银行的日子(下标从
0开始)。返回的日子可以 任意 顺序排列。
示例
- 示例1
输入:security = [5,3,3,3,5,6,2], time = 2
输出:[2,3]
解释:
第 2 天,我们有 security[0] >= security[1] >= security[2] <= security[3] <= security[4] 。
第 3 天,我们有 security[1] >= security[2] >= security[3] <= security[4] <= security[5] 。
没有其他日子符合这个条件,所以日子 2 和 3 是适合打劫银行的日子。
- 示例2
输入:security = [1,1,1,1,1], time = 0
输出:[0,1,2,3,4]
解释:
因为 time 等于 0 ,所以每一天都是适合打劫银行的日子,所以返回每一天。
- 示例3
输入:security = [1,2,3,4,5,6], time = 2
输出:[]
解释:
没有任何一天的前 2 天警卫数目是非递增的。
所以没有适合打劫银行的日子,返回空数组。
- 示例4
输入: security = [1], time = 5
输出: []
解释:
没有日子前面和后面有 5 天时间。
所以没有适合打劫银行的日子,返回空数组。
提示
1 <= security.length <= 10^50 <= security[i], time <= 10^5
思路分析
TLE错误:超出时间限制- 分析错误原因:看提示信息可知:数组的长度最长为
10^5 - 滑动窗口固定长度后,每次都只往前移动一格,那么最坏的情况下,时间复杂度就是
O(n); - 与此同时,内部的每一个滑动窗口内,需要左右遍历,时间复杂度就是
o(n) - 那么总的时间复杂度:
o(n * n) => 10 ^ 5 * 10 ^ 5 = 10 ^ 10,超过了10 ^ 8,那么就会超出时间限制。因此需要保持外部o(n)遍历的同时,优化内部的左右遍历方法,变为o(logn)或者o(1)。
- 分析错误原因:看提示信息可知:数组的长度最长为
- 在保持外部的滑动窗口不变的情况下,将每次滑动窗口内部数据的处理方式改变。
- 预处理:当前窗口左边和右边满足的个数,每次移动的时候,更新左右满足的个数,当
左边满足的个数 = time = 右边满足的个数的时候,那么就将其记录下来。 - 常规的滑动窗口思路
var goodDaysToRobBank = function(security, time) {
// 取time为中间值
let ans = [];
let len = security.length;
if(len < time * 2 + 1) return ans;
// 可以查找到合适的
// 预处理 time = 0
let pre = 0;
if(time == 0){
while(pre < len){
ans.push(pre);
pre++;
}
return ans;
}
// 需要重复执行的代码 保证最后的不超出界限即可
// last
let i = 0;
while(2 * time + 1 + i <= len){
// 每次需要往后移动1
let mid = security[time + i];
let flagLeft = false;
let flagLast = false;
let left = security.slice(0 + i,time + i);
let testLeft = security.slice(0 + i,time + i);
testLeft.sort((a, b) => b - a);
// console.log('testLeft',testLeft)
if(left.toString() == testLeft.toString()){
flagLeft = true;
}
let last = security.slice(time + 1 + i, 2 * time + 1 + i);
let testLast = security.slice(time + 1 + i, 2 * time + 1 + i);
testLast.sort((a, b) => a - b);
// console.log('testLast')
if(last.toString() == testLast.toString()){
flagLast = true;
}
if(flagLast && flagLeft && security[time - 1 + i] >= security[time + i] && security[time + 1 + i] >= security[time + i]){
// console.log(security[time - 1 + i] >= security[time + i],security[time + 1 + i] >= security[time + i]);
ans.push(time + i);
}
i++;
}
return ans;
};
AC代码
- 使用前缀和后缀预处理的方式,对于数组中的每一个节点,统计其前面和后面的符合条件的个数。将每个元素前面符合的记为
pre[i],后面符合的记为last[i]- 前面符合的条件:
security[i] <= security[i - 1] - 后面符合的条件:
security[i] <= security[i + 1] - 此时需要一个全局变量记为:
max,用来统计找到符合条件的个数。找到符合的就一直++,因为前面的全部都是符合条件的,相反的遇到一个不符合的就需要将前面累加的清0。
- 前面符合的条件:
- 最后只需要一层
for循环遍历,时间复杂度o(n),遍历查找当前的元素,前面符合的个数和后面符合的个数是否大于等于time,即:pre[i] >= time && last[i] >= time
var goodDaysToRobBank = function(security, time) {
// 使用滑动窗口解决
// 维护当前左边的递减个数和右边的递增个数,即符合要求的个数,这样直接和time进行比较即可
// time = left = right那么就是正确的,记录下来
// 左边:只需要判断中间的是否小于左边的
// 右边:新加入的是否大于右边的最后一个
// 先写另外一种方法
let ans = [];
let len = security.length;
let n = 0;
if(time == 0){
while(n < len){
ans.push(n);
n++;
}
return ans;
}
// 预处理数据
let pre = new Array(len);
let last = new Array(len);
pre[0] = 0;
let max = 0;
for(let i = 1; i < len; i++){
if(security[i - 1] >= security[i]){
// 大于当前的值
max++;
}else {
max = 0;
}
pre[i] = max;
}
last[len - 1] = 0;
max = 0;
for(let i = len - 2; i >= 0; i--) {
// console.log(last[i + 1],last[i])
if(security[i + 1] >= security[i]){
max++;
}else {
max = 0;
}
last[i] = max;
}
// console.log(pre,last)
for(let i = 0; i < len; i++) {
if(pre[i] >= time && last[i] >= time){
ans.push(i);
}
}
return ans;
};
总结
- 滑动窗口的时间复杂度的优化方法
- 滑动窗口内部的判断不像前缀和后缀这样的简单,需要考虑的情况较多。
- 预处理的前缀和后缀的方法,十分的巧妙