#P3434. 第3题-小红的区间或
-
ID: 2776
Tried: 16
Accepted: 7
Difficulty: 4
所属公司 :
阿里
时间 :2025年8月22日-菜鸟
-
算法标签>位运算
第3题-小红的区间或
思路
关键观察:将区间右端点从 i−1 扩展到 i 时,任何以 i 结尾的子区间的按位或,要么等于 ai,要么是某个以前缀结尾的按位或再与 ai 做一次 ∣。并且按位或只会“开新位”不会关掉已有位,因此对于固定右端点 i,不同的“以 i 结尾的子区间按位或结果”的种类数是有限的,至多与二进制位数相关。由于 ai≤109,二进制位数 B≤31,所以每个位置最多保留 O(B) 个不同的结果。
做法(在线去重):
-
维护两个集合:
- Sprev:所有“以前一个位置结尾”的按位或结果集合;
- Scur:所有“以当前位置 i 结尾”的按位或结果集合。
-
转移:
- 把 ai 加入 Scur;
- 对 v∈Sprev,把 v∣ai 加入 Scur;
-
用一个全局集合 Sall 收集所有出现过的值;每轮把 Scur 并入 Sall;最后答案是 ∣Sall∣。
-
由于每轮 ∣Sprev∣,∣Scur∣≤O(B),整体时间为 O(nB),这里 B≤31,可过 n 达到 105 的数据。
正确性证明
用数学归纳法证明:对每个位置 i,Scur 正好等于“所有以 i 结尾子区间的按位或结果”的集合。
-
基础:i=1 时,唯一的以 1 结尾子区间是 [1,1],其按位或为 a1,算法构造 Scur=a1,成立。
-
归纳:假设对 i−1 成立,考虑以 i 结尾的任意子区间 [l,i]。
- 若 l=i,其值为 ai,被加入。
- 若 l<i,则 [l,i] 的值等于 ([l,i−1])∣ai,而 [l,i−1] 的值在归纳假设下必在 Sprev,因此 ([l,i−1])∣ai 会被加入 Scur。 反之,Scur 中每个元素要么是 ai,要么是某个以 i−1 结尾子区间值再与 ai 做 ∣,都对应某个以 i 结尾子区间。于是二者相等。 将所有 i 的 Scur 并入 Sall,正是全部子区间的值集合,因此答案为 ∣Sall∣。
C++
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<int> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
// S_prev: 以前一个位置结尾的按位或结果集合
// S_cur : 以当前位置结尾的按位或结果集合
// S_all : 全部出现过的按位或结果(全局去重)
unordered_set<int> S_prev, S_cur, S_all;
S_prev.reserve(64);
S_cur.reserve(64);
S_all.reserve((size_t)min<long long>(1e6, 1LL * n * 32));
for (int x : a) {
S_cur.clear();
// 以 x 单独成段
S_cur.insert(x);
// 由前一轮的结果延伸到当前位置
for (int v : S_prev) {
S_cur.insert(v | x);
}
// 并入全局集合
for (int v : S_cur) S_all.insert(v);
// 下一轮
S_prev = S_cur; // 规模至多约 31,拷贝成本很低
}
cout << S_all.size() << "\n";
return 0;
}
Python
import sys
def main():
data = sys.stdin.read().strip().split()
if not data:
return
it = iter(data)
n = int(next(it))
a = [int(next(it)) for _ in range(n)]
# S_prev: 以前一个位置结尾的按位或集合
# S_all : 全局去重集合
S_prev = set()
S_all = set()
for x in a:
# 以当前位置结尾的集合
S_cur = {x} # 单独成段
for v in S_prev:
S_cur.add(v | x) # 延伸
S_all |= S_cur # 并入全局
S_prev = S_cur # 下一轮
print(len(S_all))
if __name__ == "__main__":
main()
Java
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine();
if (s == null || s.isEmpty()) return;
int n = Integer.parseInt(s.trim());
StringTokenizer st = new StringTokenizer(br.readLine());
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken());
// S_prev: 以前一个位置结尾的按位或集合
// S_all : 全局去重集合
HashSet<Integer> S_prev = new HashSet<>();
HashSet<Integer> S_all = new HashSet<>();
for (int x : a) {
// 以当前位置结尾的集合(用临时集合,避免边遍历边修改)
HashSet<Integer> S_cur = new HashSet<>();
S_cur.add(x); // 单独成段
for (int v : S_prev) {
S_cur.add(v | x); // 延伸
}
S_all.addAll(S_cur);
S_prev = S_cur; // 下一轮
}
System.out.println(S_all.size());
}
}
题目内容
小红有一个长度为 n 的数组 a ,记子区间 [l,r] 的权值为 al∣al+1∣...∣ar ,即区间内所有数的按位或运算的结果。一共有 n×(n+1)/2 个子区间,小红想知道对应的 n×(n+1)/2 个权值中,有多少个不同的取值。
输入描述
第一行一个整数 n ,表示数组长度。
第二行 n 个整数 a1,a2,...,an ,表示数组 a 的元素。
1≤n≤105
1≤ai≤109
输出描述
输出一个整数,表示不同的取值个数。
样例1
输入
3
1 2 4
输出
6
说明
[1,1] 的权值为 1
[1,2] 的权值为 3
[1,3] 的权值为 7
[2,2] 的权值为 2
[2,3] 的权值为 6
[3,3] 的权值为 4