题目
我们有一个非负整数数组 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 的子数组的按位或的结果,那么先依次增加 j,j 初始为 0,result[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
}
复用和剪枝
首先肯定需要复用,不然一定会超时
然后在此基础上,还需要剪枝,即我知道结果不可能再有变化了,就不用循环下去了
我们将遍历一个 i 从 0 到 length-1,此时 i 是子数组的结尾,然后遍历一个 j,j 从 i 前面一位,慢慢减到 0
for(let i=0;i<len;i++){
for(let j=i-1;j>=0;j--){
}
}
这里为什么 j 要从后往前呢?让我们模拟一下,对于 a,b,c 三个数,
- 如果从前往后
- 先得到
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 - 再得到
101,然后与10相或,得到111 - 再得到
100,100与101相或还是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,添加1i=1,添加4j=0,计算4和1相或是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,添加3j=1,计算3和4相或是7,同时更新arr[j=1]为7
实例更新为:5,7,3,3
- `j`=`0`,计算`3`和`5`相或是`7`,同时更新`arr[j=1]`为`7`
实例更新为:7,7,3,3
i=3,添加3j=2,计算3和3相或是3,与arr[j=2]相同,直接break,不会有新的值出现了
总结
- 按位或:按位或的性质(值永远是增加的)
- 子数组,连续:通常都要用到前缀和,后缀和,和这里不一定是相加之和,复用的的时候既可以从前往后,也可以从后往前(有的时候就是需要从后往前)