喵~ Jzzhu 和他的数字们,一个关于位运算与容斥的魔法✨
哈喽,各位算法大师们,喵~ 今天咱家要和大家分享一道非常有趣的题目,来自 Codeforces 的 449D - Jzzhu and Numbers。这道题完美地将位运算、容斥原理和一种叫做“高维前缀和”(或者叫 SOS DP)的酷炫技巧结合在了一起。
准备好了吗?那就跟着咱家的爪爪,一步步揭开这道题的神秘面纱吧!🐾
题目大意
Jzzhu 有 n 个非负整数 a1, a2, ..., an。他想知道,有多少个由这些数字的下标组成的非空子序列 i1, i2, ..., ik (这里要求 1 <= i1 < i2 < ... < ik <= n),满足这些下标对应的数字的按位与(Bitwise AND)结果为 0?
也就是说,我们要找有多少个非空子集,它们的元素的 & 运算结果是 0。
比如说,如果数字是 [2, 3, 3]。
- 子集
{2}: AND 结果是 2 - 子集
{3}: AND 结果是 3 - 子集
{3}: AND 结果是 3 - 子集
{2, 3}:2 & 3 = (10)_2 & (11)_2 = (10)_2 = 2 - 子集
{2, 3}:2 & 3 = 2 - 子集
{3, 3}:3 & 3 = 3 - 子集
{2, 3, 3}:2 & 3 & 3 = 2没有一个子集的 AND 和是 0,所以答案是 0。
再比如,如果数字是 [0, 1, 2, 3]。
- 任何包含
0的子集,它们的 AND 和一定是0。{0}{0, 1}{0, 2}{0, 3}{0, 1, 2}{0, 1, 3}{0, 2, 3}{0, 1, 2, 3}- 这里就有
2^(4-1) = 8个了。
- 不包含
0的子集:{1, 2}:1 & 2 = 0。这是一个。{1, 2, 3}:1 & 2 & 3 = 0。这又是一个。 所以总共有8 + 1 + 1 = 10个。
题目要求我们输出这个数量,并对 10^9 + 7 取模。
解题方法
直接去数 AND 和为 0 的子集数量,感觉有点像无头苍蝇,喵~ 因为限制条件 ... = 0 太宽泛了。一个数的某一位是 0,就能让最终结果的这一位变成 0。
这时候,咱家的小脑袋瓜灵光一闪!正着不好算,咱就反着来嘛!
容斥原理登场!
我们可以换一个角度思考:
- 设
f(mask)表示 AND 和是mask的超集 的子集有多少个。- “
A是B的超集” 在位运算里是什么意思呢?就是A & B == B。也就是说,B中为 1 的位,在A中也必须为 1。
- “
- 那么,
f(mask)就是由 所有是mask的超集的那些输入数字a_i组成的非空子集的数量。 - 假设在输入中有
k个数字都是mask的超集,那么由它们能组成的非空子集就有2^k - 1个。所以f(mask) = 2^k - 1。
但是我们想求的是 AND 和恰好等于 0 的子集数量。这就要用到容斥原理了。
令 g(mask) 表示 AND 和恰好为 mask 的子集数量。我们想求的就是 g(0)。 根据容斥原理(或者说是子集反演),我们有: g(0) = f(0) - (f(1) + f(2) + f(4) + ...) + (f(3) + f(5) + f(6) + ...) - ...
用更优雅的数学语言来说就是: g(0) = Σ (-1)^popcount(mask) * f(mask) (这里的 popcount(mask) 是 mask 二进制表示中 1 的个数)
不过,f(mask) = 2^k - 1 带着一个 -1,处理起来有点麻烦。咱们可以稍微变通一下。
设 F(mask) 表示 AND 和是 mask 的超集 的子集有多少个(这次我们把空集也算上!)。 如果输入中有 cnt[mask] 个数字是 mask 的超集,那么 F(mask) = 2^cnt[mask]。
同样通过容斥原理,我们可以得到 AND 和恰好为 0 的子集数量: ans = Σ (mask from 0 to MAX_VAL-1) (-1)^popcount(mask) * F(mask)ans = Σ (mask from 0 to MAX_VAL-1) (-1)^popcount(mask) * 2^cnt[mask]
这个公式看起来就很清爽了,喵~
现在问题来了,怎么快速计算 cnt[mask] 呢? cnt[mask] 表示输入 a 中,有多少个 a_i 满足 a_i & mask == mask。 如果我们对每个 mask 都遍历一遍 n 个数字,那复杂度就是 O(n * MAX_VAL),太慢了!
高维前缀和(SOS DP)来救驾!
这个技巧也叫“Sum over Supermasks”或者“高维后缀和”。
- 首先,我们用一个数组
dp统计输入中每个数字出现的次数。dp[x]就是数字x的出现次数。 - 我们想计算的
cnt[mask]其实是Σ dp[x],其中x是mask的所有超集。 - 我们可以用一个 DP 来实现这个“后缀和”的计算。cpp这个 DP 的过程是从高位到低位(或者说从大
// dp[mask] 初始化为数字 mask 的出现次数 for (int i = 0; i < BITS; ++i) { // 遍历所有位 for (int mask = 0; mask < MAX_VAL; ++mask) { // 如果 mask 的第 i 位是 1 if (mask & (1 << i)) { // 那么 mask 的所有超集,也是 (mask 去掉第 i 位) 这个数的所有超集的一部分 // 所以把 mask 的计数,加到 (mask 去掉第 i 位) 的计数上 dp[mask ^ (1 << i)] += dp[mask]; } } }mask到小mask)累加。完成之后,dp[mask]就变成了我们想要的cnt[mask]了!它的复杂度是O(BITS * 2^BITS),非常快。
最终算法步骤
- 初始化一个频率数组
counts,counts[x]记录数字x在输入中出现的次数。 - 使用高维后缀和 DP(如上所述)来更新
counts数组。之后,counts[mask]就存储了输入中有多少个数是mask的超集。 - 预计算
2的幂次,p2[i] = 2^i % MOD。 - 初始化答案
ans = 0。 - 遍历从
0到MAX_VAL-1的所有mask:- 计算
term = p2[counts[mask]]。 - 如果
popcount(mask)是奇数,就从ans中减去term。 - 如果
popcount(mask)是偶数,就给ans加上term。 - 记得每次运算都取模,并且减法要
(ans - term + MOD) % MOD防止负数哦。
- 计算
- 最后得到的
ans就是我们的答案啦!
题解代码 (C++)
这是咱家整理好的代码,加了一些注释方便理解,喵~
#include <iostream>
#include <vector>
#include <numeric>
// 用一个宏来处理不同编译器下的 popcount 内置函数,很方便的小技巧喵~
#if defined(__GNUC__) || defined(__clang__)
#define BUILTIN_POPCOUNT __builtin_popcount
#else
#include <intrin.h>
#define BUILTIN_POPCOUNT __popcnt
#endif
void fast_io() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
}
const int MOD = 1000000007;
const int BITS = 20; // 题目中 a_i <= 10^6,小于 2^20,所以 20 位就够了
const int MAX_VAL = 1 << BITS;
int main() {
fast_io();
int n;
std::cin >> n;
// 1. 统计每个数字出现的次数
std::vector<int> counts(MAX_VAL, 0);
for (int i = 0; i < n; ++i) {
int a;
std::cin >> a;
counts[a]++;
}
// 2. 高维后缀和 (Sum over Supermasks DP)
// 计算 cnt[mask]:输入中有多少个数是 mask 的超集
// 这个循环结束后,counts[mask] 的含义就变了哦!
for (int i = 0; i < BITS; ++i) {
for (int mask = 0; mask < MAX_VAL; ++mask) {
// 我们从包含第 i 位的 mask,把它的数量加给不包含第 i 位的 mask
// 这样从高位到低位迭代,就能正确算出超集和
if (mask & (1 << i)) {
counts[mask ^ (1 << i)] += counts[mask];
}
}
}
// 3. 预计算 2 的幂
std::vector<long long> p2(n + 1);
p2[0] = 1;
for (int i = 1; i <= n; ++i) {
p2[i] = (p2[i - 1] * 2) % MOD;
}
// 4. 使用容斥原理计算最终答案
long long ans = 0;
for (int mask = 0; mask < MAX_VAL; ++mask) {
// F(mask) = 2^cnt[mask]
long long term = p2[counts[mask]];
// 根据 popcount 的奇偶性来决定是加还是减,这就是 (-1)^popcount(mask)
if (BUILTIN_POPCOUNT(mask) % 2 == 1) {
ans = (ans - term + MOD) % MOD; // 减法要小心负数
} else {
ans = (ans + term) % MOD;
}
}
std::cout << ans << std::endl;
return 0;
}知识点介绍
这道题用到的知识点都非常经典,掌握了它们,就能解决一大类位运算相关的计数问题呢!
位运算 (Bitwise Operations)
- 核心是
&(按位与) 运算。A & B的结果中,只有在A和B中都为 1 的位,结果才为 1,否则为 0。 A是B的超集 (Ais a supermask ofB),等价于A & B == B。A是B的子集 (Ais a submask ofB),等价于A & B == A。
- 核心是
容斥原理 (Inclusion-Exclusion Principle)
- 这是一个强大的组合计数工具。最简单的形式是
|A ∪ B| = |A| + |B| - |A ∩ B|。 - 在题目中,我们用它来处理“恰好”等于某个值的问题。我们先计算“至少”满足某些属性(是某个
mask的超集)的数量,然后通过加加减减来得到“恰好”满足无属性(mask=0)的数量。 - 公式
ans = Σ (-1)^popcount(mask) * F(mask)是容斥原理在子集问题上的经典应用,也叫“子集反演”或“莫比乌斯反演”。
- 这是一个强大的组合计数工具。最简单的形式是
高维前缀/后缀和 (SOS DP / High-Dimensional Prefix/Suffix Sums)
- 这是一种在
O(D * 2^D)时间内计算所有子集或超集信息的动态规划技巧(D 是位数)。 - 高维前缀和 (Sum over Submasks):计算
dp[mask] = Σ f[submask],其中submask是mask的子集。 - 高维后缀和 (Sum over Supermasks):计算
dp[mask] = Σ f[supermask],其中supermask是mask的超集。本题用的就是这种。 - 它们也被称为 快速沃尔什-阿达玛变换 (Fast Walsh-Hadamard Transform, FWHT) 的一部分,专门用于处理位运算卷积(AND, OR, XOR)。本题的计算过程,本质上就是对频率数组做了一个 AND 卷积的变换。
- 这是一种在
好啦,今天的题解分享就到这里啦!希望大家能有所收获,喵~ 如果有任何问题,欢迎随时来找咱家讨论哦!下次再见!💖