题目链接
题目描述
给定 个正整数。我们定义一个正整数
“包含” 另一个正整数
,当且仅当
(A | B) == A
,其中 |
是按位或运算。这个条件的含义是,在二进制表示下,所有 中为
的位,在
中也必须为
。
任务是从这 个数中选出尽可能少的数,组成一个集合,使得原来
个数中的每一个数,都至少被这个新集合中的某一个数所“包含”。
输入:
- 第一行一个正整数
。
- 第二行
个正整数
。
输出:
- 一个整数,表示最少需要选出的数字个数。
解题思路
这是一个利用位运算和动态规划解决的集合覆盖问题。一个朴素的 做法是检查每对数,但这在
较大时会超时。考虑到数值范围
,我们可以使用 Sum over Subsets (SOS) DP(或称为位运算DP)来优化。
核心思想是:一个数 如果能被集合中另一个不等于它的数
包含,那么我们就不需要选择
。我们只需要统计有多少个数是无法被其他任何数包含的。
高效的算法步骤如下:
-
预处理:
- 首先对输入的数进行去重,因为重复的数不影响覆盖关系。
- 创建一个大小为
的布尔数组
present
,present[i] = true
表示数字i
存在于输入中。
-
动态规划 (SOS DP):
- 创建一个大小为
的布尔数组
sos
,sos[mask]
的含义是:数字mask
或mask
的某个超集是否存在于输入中。 - 初始化
sos
数组,令sos[i] = present[i]
。 - 通过动态规划填充
sos
数组。我们从高位到低位遍历所有二进制位i
,再从大到小遍历所有mask
。状态转移方程可以理解为:如果mask
的第i
位为 0,那么它的超集mask | (1 << i)
的信息可以传递给它。 if (!(mask & (1 << i))) sos[mask] |= sos[mask | (1 << i)];
- 经过这个过程,
sos[mask]
就正确地存储了所需信息。
- 创建一个大小为
-
计数:
- 遍历所有去重后的输入数
x
。 - 对于每个
x
,我们需要判断是否存在一个严格超集y
(且
) 在输入中。
- 一个
的严格超集,必然是在
的某个为 0 的位上为 1。
- 所以,我们遍历
x
的所有为 0 的二进制位i
,构造一个mask = x | (1 << i)
。 - 我们查询
sos[mask]
。如果sos[mask]
为true
,说明存在一个输入中的数y
是mask
或mask
的超集,这个y
必然是x
的严格超集。因此x
可以被覆盖。 - 如果在检查完
x
所有为 0 的位后,都没有找到能覆盖它的数,那么x
就是必须选择的。 - 统计所有必须选择的数的个数即为答案。
- 遍历所有去重后的输入数
代码
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
#include <set>
using namespace std;
const int MAX_VAL = 262144; // 2^18
bool present[MAX_VAL];
bool sos[MAX_VAL];
int main() {
ios::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
set<int> unique_nums_set;
for (int i = 0; i < n; ++i) {
int a;
cin >> a;
unique_nums_set.insert(a);
}
vector<int> a(unique_nums_set.begin(), unique_nums_set.end());
for (int x : a) {
present[x] = true;
}
for (int i = 0; i < MAX_VAL; ++i) {
sos[i] = present[i];
}
for (int i = 0; i < 18; ++i) {
for (int mask = MAX_VAL - 1; mask >= 0; --mask) {
if (!(mask & (1 << i))) {
sos[mask] |= sos[mask | (1 << i)];
}
}
}
int ans = 0;
for (int x : a) {
bool is_covered_by_proper_superset = false;
for (int i = 0; i < 18; ++i) {
if (!(x & (1 << i))) {
if (sos[x | (1 << i)]) {
is_covered_by_proper_superset = true;
break;
}
}
}
if (!is_covered_by_proper_superset) {
ans++;
}
}
cout << ans << endl;
return 0;
}
import java.util.Scanner;
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
Set<Integer> uniqueNums = new HashSet<>();
for (int i = 0; i < n; i++) {
uniqueNums.add(sc.nextInt());
}
int maxVal = 262144; // 2^18
boolean[] sos = new boolean[maxVal];
for (int x : uniqueNums) {
sos[x] = true;
}
for (int i = 0; i < 18; i++) {
for (int mask = maxVal - 1; mask >= 0; mask--) {
if ((mask & (1 << i)) == 0) {
sos[mask] |= sos[mask | (1 << i)];
}
}
}
int ans = 0;
for (int x : uniqueNums) {
boolean isCovered = false;
for (int i = 0; i < 18; i++) {
if ((x & (1 << i)) == 0) {
if (sos[x | (1 << i)]) {
isCovered = true;
break;
}
}
}
if (!isCovered) {
ans++;
}
}
System.out.println(ans);
}
}
n = int(input())
a = list(map(int, input().split()))
unique_a = sorted(list(set(a)))
MAX_VAL = 262144 # 2^18
sos = [False] * MAX_VAL
for x in unique_a:
sos[x] = True
for i in range(18):
for mask in range(MAX_VAL - 1, -1, -1):
if not (mask & (1 << i)):
sos[mask] |= sos[mask | (1 << i)]
ans = 0
for x in unique_a:
is_covered = False
for i in range(18):
if not (x & (1 << i)):
if sos[x | (1 << i)]:
is_covered = True
break
if not is_covered:
ans += 1
print(ans)
算法及复杂度
- 算法:位运算动态规划 (Sum over Subsets DP)
- 时间复杂度:
,其中
是输入数字个数,
是数值上限 (
)。
用于读取和去重,
用于DP计算和最终检查。
- 空间复杂度:
,需要数组来存储DP状态。