算法898:剪枝与复用

102 阅读3分钟

题目

我们有一个非负整数数组 arr 。

对于每个(连续的)子数组 sub = [arr[i], arr[i + 1], ..., arr[j]] ( i <= j),我们对 sub 中的每个元素进行按位或操作,获得结果 arr[i] | arr[i + 1] | ... | arr[j] 。

返回可能结果的数量。 多次出现的结果在最终答案中仅计算一次。

来源:力扣(LeetCode) 链接:leetcode.cn/problems/bi… 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

动态规划(超时)

一开始我想到的是动态规划,即 result[i][j] 表示以索引 i 开始,增加长度为 j 的子数组的按位或的结果,那么先依次增加 jj 初始为 0result[i][0] 就是 arr,后面的就是

result[i][j] => result[i][j-1] | arr[i+j][0]

/**
 * @param {number[]} arr
 * @return {number}
 */
var subarrayBitwiseORs = function(arr) {
  let len = arr.length
  let result = []
  for(let i=0;i<len;i++){
    result.push(new Array(len-i).fill(''))
  }
  let resultObj = {}
  for(let j=0;j<len;j++){
    for(let i=0;i<len-j;i++){
      if(j===0){
        result[j][i] = arr[i]
      }else{
        result[j][i] = getOrBinaryString(result[j-1][i], arr[i+j])
      }
      resultObj[result[j][i]] = ''
    }
  }
  return Object.keys(resultObj).length
}


function getOrBinaryString(num1, num2){
  return num1 | num2
}

复用和剪枝

大佬的解法,也是我的参考

首先肯定需要复用,不然一定会超时

然后在此基础上,还需要剪枝,即我知道结果不可能再有变化了,就不用循环下去了

我们将遍历一个 i0length-1,此时 i 是子数组的结尾,然后遍历一个 jji 前面一位,慢慢减到 0

  for(let i=0;i<len;i++){
    for(let j=i-1;j>=0;j--){
    }
  }

这里为什么 j 要从后往前呢?让我们模拟一下,对于 abc 三个数,

  • 如果从前往后
    • 先得到 a,然后是 a|b,然后是 a|b|c
    • 轮到 b 时,因为从 b 开始,无法复用,因为上一轮的 a 是脏数据,使你不得不重新计算
  • 如果从后往前
    • 先是 a,此时从后往前,a 前面没有数了,到下一轮
    • 先是 b,然后是 a|b
    • 再是 c,然后是 b|c,然后是 a|b|c,注意此时 a|b|c 是可以复用前面的 a|b

剪枝

此外需要考虑剪枝,如果 某数前面的数 相或后的值 还是前面的数,就没有必要继续下去了,某数的二进制完全是 前面的数的子集,此时再与更前面的数相或,其结果也就是前面的数更前面的数相或,而这个值我们已经算过了。

例如:2,5,4

对应的二进制数是:10,101,100

  1. 首先得到 1
  2. 再得到 101,然后与10相或,得到111
  3. 再得到 100, 100101相或还是101,那就不用继续了,再往前面或也只能得到101与更前面的10相或的111

复用

注意我们前面说到 a|b|c 可以复用前面的 a|b,但是我们并不需要另一个数据结构来记录,直接更新 arr 就行了,这一块很复杂,建议看一下下面的代码,然后跟一个实例来思考

代码如下

  let len = arr.length
  let result = new Set()
  for(let i=0;i<len;i++){
    result.add(arr[i])
    for(let j=i-1;j>=0;j--){
      if((arr[i] | arr[j]) === arr[j]){
        break
      }
      arr[j] = arr[j] | arr[i]
      result.add(arr[j])
    }
  }

实例:1,4,3,3

  • i=0,添加 1
  • i=1,添加 4
    • j=0,计算41相或是5,同时更新arr[j=0]5

实例更新为5,4,3,3

这一步也就是复用了 a|b,因为我们永远不会只用a而不用b了,因为我们是从后往前的,再往下走下去,比如后面的a|b|c,甚至是a|b|c|d,如果要和a相或,也不可能避开b,所以把a变成a|b,方便后续的计算

  • i=2,添加 3
    • j=1,计算34相或是7,同时更新arr[j=1]7

实例更新为:5,7,3,3

- `j`=`0`,计算`3``5`相或是`7`,同时更新`arr[j=1]``7`

实例更新为:7,7,3,3

  • i=3,添加 3
    • j=2,计算33相或是3,与 arr[j=2] 相同,直接break,不会有新的值出现了

总结

  • 按位或:按位或的性质(值永远是增加的)
  • 子数组,连续:通常都要用到前缀和,后缀和,和这里不一定是相加之和,复用的的时候既可以从前往后,也可以从后往前(有的时候就是需要从后往前)