子数组按位或操作 图解题解
这道题到底在问什么
- 输入
- arr=[1,2,4]
- 输出
- 6 (能得到 1,2,3,4,6,7 六种)
- 输入
- arr=[0,0]
- 输出
- 1 ([0]、[0]、[0,0] 全是 0,只一种)
最优解:为什么这么做
一句话答案:LeetCode 898 子数组按位或操作:只维护「以当前元素结尾的所有子数组按位或值」这一小撮集合,滚动并入总集合去重。按位或只增位、集合恒小,时间 O(n·30)、空间 O(n·30)。
按位或的不同结果,到底在数什么
给整数数组 arr,把每个非空子数组(连续一段,不能跳着取)各做一遍按位或,问有多少个不同的值。按位或就是把一段的数逐位比较,某位只要有一个是 1,结果那位就是 1。题面 arr=[1,2,4] 能或出 1、2、3、4、6、7 六个值,答案 6;arr=[0,0] 三段全为 0 只有一种,答案 1。数的是不同结果个数,不是子数组条数。
为什么把每个子数组都或一遍会超时
长度 n 的数组有 n(n+1)/2 个子数组,逼近 O(n²) 个(大 O 记号描述规模变大时运算量怎么涨)。把每段从头或到尾又各要 O(n) 次,硬枚举奔着 O(n³) 去,n 到十万扛不住。更亏的是以同一右端点结尾的段大量重叠、前半截反复重或,这些或值该攒着复用。
盯住『以当前元素结尾』的那一小撮或值
只盯以当前这个数结尾的所有子数组各自的按位或值,收进集合 cur(自动去重)。关键是 cur 永远很小:按位或有个死性子——只把二进制位从 0 变成 1,从不把 1 清回 0。固定右端点、左端点往左扩,这段的或值只增不减。题目里的数不超过 10^9,只占 30 个二进制位;或值每真变大一次至少点亮一个新位,最多点亮 30 次就涨不动了。所以以任一位置结尾,不同或值至多约 30 个——cur 恒小的根就在这。
新一轮的或值集合,凭什么只用旧集合推
滚到下一个数 x,不必重扫前面。以 x 结尾的子数组只有两类:只有 x 自己的,或值就是 x;由前一段接上 x 而来的,或值等于那段旧或值再或上 x。上一轮的 cur 正好存着以上一个数结尾的全部或值,把每个都或一遍 x、再补上 x 自己,就是新 cur:new cur = {x | y :y 属于旧 cur} 再并上 {x}。每得一轮就并进总集合 ans(同样去重),扫完 arr,ans 的大小即答案。这套滚着推正是动态规划:把以每个位置结尾的小答案攒下来直接复用。
拿 [1,2,4] 把集合一轮轮滚出来
拿题面 arr=[1,2,4] 滚一遍,ans 起初为空。数 1:cur={1},ans={1}。数 2:旧 cur={1},1 或 2 得 3,再补上 2,cur={2,3},ans 新增 2、3 成 {1,2,3}。数 4:旧 cur={2,3},2 或 4 得 6、3 或 4 得 7,再补 4,cur={4,6,7},ans 新增 4、6、7 成 {1,2,3,4,6,7}。扫完 ans 里正好六个值,答案 6,和题面对上。
漏掉『只含自己』那个子数组,答案为什么会少一截
每轮忘了往 cur 补上 x 自己,是最常见的写反:单个元素单独成段的或值全丢,只有一个数的输入会连它本身都数不到。复杂度上,每个位置的 cur 至多约 30 个,扫 n 个数各做一次或,时间 O(n·30)、接近线性;ans 最多攒约 30n 个值,空间 O(n·30),cur 只占 O(30)。两个边界想清:数组全是 0 时每段或值都是 0,答案只能是 1,别数成子数组条数;各数二进制位完全错开时或值组合最多,cur 塞得最满也不超 30 个。
▶ 动画逐步走查(共 27 步)——想跟着动画一帧帧对照就展开
- 3关键直觉:按位或只会把二进制位从 0 变 1、从不清零,所以越往左扩 OR 值只增不减,种类有限,cur 始终很小(不超过 32 个)。
- 4开局:res 是最终要数的「不同 OR 值」集合,现在为空。我们从左到右,一位一位把它喂大。
- 5从第 0 位 1 开始。它前面没有元素,以它结尾的子数组就只有它自己,所以新 cur 先从空白起步。
- 6别忘了只含 1 自己的那个子数组,OR 值就是 1,也加进来。现在以第 0 位结尾的全部 OR 值都齐了。
- 7把本轮 cur 整个倒进 res,去掉重复后净增 1 个新值(1),res 涨到 1 种。
- 8轮到第 1 位 2。上一轮以第 0 位结尾的 OR 集合是 {1},把它们每个都和 2 求一次或,就得到「多接了一个 2」的新子数组的 OR 值。
- 9拿上一轮的 1 出来,和当前的 2 逐位求或:哪一位有 1 结果就是 1。
- 102 或 1 得 3,放进新 cur。可以看到 3 的二进制位是 2 和 1 两者 1 位的并集。
- 11别忘了只含 2 自己的那个子数组,OR 值就是 2,也加进来。现在以第 1 位结尾的全部 OR 值都齐了。
- 12把本轮 cur 整个倒进 res,去掉重复后净增 2 个新值(2、3),res 涨到 3 种。
- 13轮到第 2 位 4。上一轮以第 1 位结尾的 OR 集合是 {2, 3},把它们每个都和 4 求一次或,就得到「多接了一个 4」的新子数组的 OR 值。
- 14拿上一轮的 2 出来,和当前的 4 逐位求或:哪一位有 1 结果就是 1。
- 154 或 2 得 6,放进新 cur。可以看到 6 的二进制位是 4 和 2 两者 1 位的并集。
- 16拿上一轮的 3 出来,和当前的 4 逐位求或:哪一位有 1 结果就是 1。
- 174 或 3 得 7,放进新 cur。可以看到 7 的二进制位是 4 和 3 两者 1 位的并集。
- 18别忘了只含 4 自己的那个子数组,OR 值就是 4,也加进来。现在以第 2 位结尾的全部 OR 值都齐了。
- 19把本轮 cur 整个倒进 res,去掉重复后净增 3 个新值(4、6、7),res 涨到 6 种。
- 20轮到第 3 位 3。上一轮以第 2 位结尾的 OR 集合是 {4, 6, 7},把它们每个都和 3 求一次或,就得到「多接了一个 3」的新子数组的 OR 值。
- 21拿上一轮的 4 出来,和当前的 3 逐位求或:哪一位有 1 结果就是 1。
- 223 或 4 得 7,放进新 cur。可以看到 7 的二进制位是 3 和 4 两者 1 位的并集。
- 23拿上一轮的 6 出来,和当前的 3 逐位求或:哪一位有 1 结果就是 1。
- 243 或 6 得 7,放进新 cur。可以看到 7 的二进制位是 3 和 6 两者 1 位的并集。
- 25拿上一轮的 7 出来,和当前的 3 逐位求或:哪一位有 1 结果就是 1。
- 263 或 7 得 7,放进新 cur。注意结果和 7 一样,说明 3 的 1 位都被 7 盖住了。
- 27别忘了只含 3 自己的那个子数组,OR 值就是 3,也加进来。现在以第 3 位结尾的全部 OR 值都齐了。
- 28本轮 cur 里的值 res 早就有了(3、7 都见过),所以 res 一个没涨,还是 6 种。这正是「去重」在起作用。
- 29全程扫完,res 里一共 6 个不同的 OR 值(1、2、3、4、6、7),答案就是 6。
⚠️ 容易写错的地方
✗ 错:硬枚举全部 O(n²) 个子数组逐段求或
✓ 对:以 i 结尾维护滚动集合 cur
逐段重算是 O(n²) 甚至更糟,n 到 5 万必超时;滚动集合让每步只做 ≤ 32 次或
✗ 错:忘了把 arr[i] 自己加入 cur
✓ 对:每轮显式 cur.add(arr[i])
长度为 1 的子数组也要算,漏了会少一批 OR 值
✗ 错:用数组而非集合存 cur
✓ 对:必须用 set 去重
相邻 OR 值常重复,用数组会让 cur 膨胀、退化成 O(n²)
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def subarrayBitwiseORs(self, arr: List[int]) -> int:
ans = set()
s = set()
for x in arr:
s = {x | y for y in s} | {x}
ans |= s
return len(ans)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int subarrayBitwiseORs(vector<int>& arr) {
unordered_set<int> ans;
unordered_set<int> s;
for (int x : arr) {
unordered_set<int> t;
for (int y : s) {
t.insert(x | y);
}
t.insert(x);
ans.insert(t.begin(), t.end());
s = move(t);
}
return ans.size();
}
};Java
import java.util.*;
class Solution {
public int subarrayBitwiseORs(int[] arr) {
Set<Integer> ans = new HashSet<>();
Set<Integer> s = new HashSet<>();
for (int x : arr) {
Set<Integer> t = new HashSet<>();
for (int y : s) {
t.add(x | y);
}
t.add(x);
ans.addAll(t);
s = t;
}
return ans.size();
}
}复杂度
时间
O(n·32)
每位的 cur 集合最多 32 个不同值(OR 单调增、位数有限),扫 n 位约 32n 次或运算,近似 O(n log max)
空间
O(n·32)
res 最多存约 32n 个不同值;滚动的 cur 只占 O(32)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 子数组按位或操作 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么以每个位置结尾的或值集合最多只有约 30 个?+
因为按位或只增位、从不清位。固定右端点,把左端点一路往左扩,这一段的或值只会不断变大,是单调的。每次它真变大,至少有一个新的二进制位从 0 被点亮成 1;题目里的数不超过 10^9,只用到 30 个二进制位,最多被点亮 30 次就再也涨不动。所以以任一位置结尾,不同的或值至多约 30 个,集合 cur 恒小,整套办法才接近线性。
把按位或换成按位与,这套办法还成立吗?+
成立,只是单调方向反过来。按位与(AND,两位都是 1 结果才是 1)往左扩只会把 1 清成 0、位数越来越少,同样最多变 30 次,所以「以每个位置结尾的与值集合」照样恒小。把维护 cur 时的或换成与、总集合并入照旧,返回它的大小就是不同 AND 值的个数。
为什么非得用集合,用数组行不行?+
集合的作用是自动去重。以某个位置结尾的不同段可能或出同一个值(越往左扩,一旦或值被前面的数盖满就不再变),数组会把这些重复值全留着,让 cur 虚胖、还丢掉「恒小」的保证;总集合 ans 更要去重,否则数出来的是子数组条数、不是不同值的个数。用哈希集合(按值判重的容器,每步近似 O(1) 判断某个值在不在里面)最省事。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 子数组按位或操作 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。