2100. 适合打劫银行的日子(前缀和、滑动窗口)

208 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

不是因为看到了希望才坚持,而是因为坚持了才能看到希望。共勉

每日刷题第56天 2021.03.06

2100. 适合打劫银行的日子

题目描述

  • 你和一群强盗准备打劫银行。给你一个下标从 0 开始的整数数组 security ,其中 security[i] 是第 天执勤警卫的数量。日子从开始编号。同时给你一个整数 time 。
  • 如果第 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] 。
没有其他日子符合这个条件,所以日子 23 是适合打劫银行的日子。
  • 示例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^5
  • 0 <= 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;
};

总结

  • 滑动窗口的时间复杂度的优化方法
    • 滑动窗口内部的判断不像前缀和后缀这样的简单,需要考虑的情况较多。
  • 预处理的前缀和后缀的方法,十分的巧妙