Jzzhu and Numbers
OJ: CodeForces
题目 ID: 449D
难度:提高+/省选-
标签:sosdp
日期: 2026-01-23 15:40
目录
这道题是 SOS DP 结合 容斥原理 (Inclusion-Exclusion Principle) 的巅峰之作。要解析这道题,我们需要从“恰好为 0”这个难点出发,一步步推导到“超集求和”。
既然你已经掌握了子集 SOS DP 的逻辑,我们来看这道题的三个推导层次。
第一层:容斥原理的引入
我们要找的是子序列 AND 结果恰好为
- 第
位不能全是 。 - 第
位不能全是 。 - …
- 第
位不能全是 。
根据容斥原理,我们要计算满足“没有任何位全是 1”的方案数:
其中:
是 中二进制 的个数。 是满足“至少 指定的那些位全为 1”的子序列方案数。
注意:
第一层的解释: 容斥原理的两种视角
理解容斥原理(Principle of Inclusion-Exclusion, PIE)是进阶组合数学和位运算 DP 的分水岭。既然你熟悉离散数学,我们直接用形式化的语言,从两个视角把这个“恰好为 0”的问题拆解清楚。
我们的目标是:求按位与结果没有任何一位全是 1 的子序列方案数。
1. 集合论视角:补集的交集
设
对于每一位
我们要找的是:既不在
即计算补集的交集:
根据德·摩根定律,这等价于:
根据容斥原理的标准公式,并集的模等于:
将此代入
关键推导:
这正对应了我们之前定义的
表示“至少 这些位全是 1”的方案数。 (即 )决定了该项前面的正负号。
2. 命题逻辑视角:属性的否定
设属性
我们需要计算满足
利用逻辑等价式:
在逻辑计数中,如果我们想求“没有任何属性成立”的情况,可以使用**指示函数(Characteristic Function)**的恒等式:
其中
将这个乘积式展开:
对所有子序列求和(即求期望值的和):
交换求和次序(这是组合数学中极常用的技巧):
括号内部的部分
3. 总结与对照表
通过这两种推导,我们可以建立一个直观的联系:
| 维度 | 逻辑/集合语言 | SOS DP 中的具体含义 |
|---|---|---|
| 全集 |
所有非空子序列 | |
| 属性 |
第 |
子序列中每个元素都是 |
| 交集 |
多位同时全是 1 | 子序列中每个元素都是 |
| **容斥系数 $(-1)^{ | mask | }$** |
代码段
graph TD
A["恰好结果为 0"] -- "逻辑否定" --> B["没有任何位全是 1"]
B -- "容斥展开" --> C["计算: 至少某些位全是 1"]
C -- "转化" --> D["计算: 每个元素都是 mask 的超集"]
D -- "SOS DP" --> E["高效求出 f(mask)"]
E -- "组合计数" --> F["g(mask) = 2^f(mask) - 1"]
F -- "求和" --> G["最终答案"]
为什么这一步必须用 SOS DP?
因为在容斥公式中,我们需要对每一个
- 如果对每个
暴力遍历数组, ,必 TLE。 - 利用 SOS DP,我们可以用
的时间预处理出所有 的超集计数。
你想现在尝试把这个容斥逻辑写成 C++ 代码吗?或者需要我解释一下为什么
举例子
如果
在组合数学中,全集
的具体构成
我们可以通过枚举下标来列出
| 编号 | 下标组合 | 对应数值 | 按位与 (AND) 结果 | 是否符合 “AND=0” |
|---|---|---|---|---|
| 1 | 是 | |||
| 2 | 否 | |||
| 3 | 否 | |||
| 4 | 是 | |||
| 5 | 是 | |||
| 6 | 是 | |||
| 7 | 是 |
关键点解析
- 全集
的本质: 并不是一个包含数字的集合,而是一个包含集合的集合(A set of sets)。它的每个元素都是从原数组中挑选出来的一种“成组方案”。 - 计数意义:
- 在 Jzzhu and Numbers 这道题中,目标就是找出
中有多少个元素的“按位与结果”等于 。 - 在这个例子中,答案就是 5。
- 在 Jzzhu and Numbers 这道题中,目标就是找出
- 区分数值与下标:即便两个数字的值相同(比如数组是
[0, 0, 0]),挑选“第一个 0”和挑选“第二个 0”被视为不同的方案,对应中不同的子序列。
容斥原理在这里怎么运作?
在这一层推导中,我们不会去一个一个检查这 7 个方案,而是利用属性来剔除:
- 属性
:子序列 AND 结果的第 0 位是 1(方案 2 是其中之一)。 - 属性
:子序列 AND 结果的第 1 位是 1(方案 3 是其中之一)。 - 我们要的是:既不满足
也不满足 的方案。
通过容斥公式:
你会发现,SOS DP 正是用来快速算出“满足
第二层:将 转化为元素计数
如何求
-
一个子序列的 AND 结果在
的位上全为 ,当且仅当该子序列中的每一个数在 的位上都全为 。 -
用集合语言描述:子序列中的每个元素
必须满足 (即 是 的超集)。 -
假设在给定的
个数中,有 个数是 的超集。 -
那么,从这
个数中任意挑选元素组成非空子序列,它们的 AND 结果一定包含 。 (题目允许
,所以是 ;如果不要求非空则是 ,最后对结果统一处理。)
第二层的解释: 从子序列到元素的转换
这一层的核心是理解子序列的性质如何转化为元素的性质。我们需要将“子序列的 AND 结果”转化为“元素的超集关系”。
g(mask) 转化为元素计数 f(mask)
第二层 这个公式怎么得到?
要推导这个公式,我们需要利用离散数学中的两个核心概念:集合交集的性质 和 幂集(Power Set)的计数定理。
我们将这个过程分为三个严谨的逻辑步骤。
第一步:位运算与集合论的同构
在离散数学中,一个
- 设数字
对应的集合为 。 - 设
对应的集合为 。 - 按位与(AND)运算 对应集合的 交集(Intersection)。
因此,子序列
第二步:交集包含关系的等价转化
根据交集的定义:
由此可以推导出一个关键的逻辑等价式:
推导过程:
方向:如果 是所有集合交集的子集,而交集本身又是其中任何一个集合 的子集,根据子集关系的传递性, 对所有 成立。 方向:如果每一个 都包含了 ,那么它们的公共部分(交集)自然也至少包含了 。
结论:一个子序列的 AND 结果要包含
第三步:基于幂集的组合计数
现在我们把问题转变成一个纯粹的计数问题:
-
定义候选池:
在原始的
个数中,满足“是 的超集”这一条件的数构成的集合为 。 根据你的定义,这个集合的大小
。 -
构建子序列:
任何一个满足条件的子序列,其元素必须全部来自于
。也就是说,每一个合法的子序列都是 的一个子集。 -
计算子集总数:
根据离散数学中的幂集定理:一个大小为
的集合,其幂集(所有子集构成的集合)的大小为 。 这里
,所以总共有 种可能的子序列。 -
排除非法情况:
幂集中包含一个空集(即一个数字都不选的情况)。
题目要求是“非空子序列”(
),因此我们需要从总数中减去这 种空集的情况。
最终公式:
总结
这个推导展示了为什么
:衡量了在“高维空间”中,有多少个点落在了 所在的“上方区域”(超集区域)。 :利用指数级增长的特性,将“点的数量”转化为了“由点构成的组合数量”。
这也是为什么 SOS DP 如此强大的原因:它让我们能在
第三层:SOS DP 计算超集和
现在的核心矛盾变成了:如何快速求出
这是典型的超集求和问题。在 SOS DP 中,我们只需要改变一下遍历的方向和条件:
代码段
graph LR
A["子集和 (Subset)"] --> B["mask & (1#60;#60;i) 为真"]
B --> C["从 mask ^ 2^i 转移到 mask"]
D["超集和 (Superset)"] --> E["mask & (1#60;#60;i) 为假"]
E --> F["从 mask ^ 2^i 转移到 mask"]
style F fill:#f96,stroke:#333,stroke-width:2px
状态转移逻辑推导:
定义
-
如果
mask的第位是 : 那么
的第 位必须是 。此时没有选择余地: -
如果
mask的第位是 : 那么
的第 位可以是 (对应 ),也可以是 (对应 )。
完整算法流程
-
统计频率:用
cnt[x]记录数组中数字出现的次数。 -
超集 SOS DP:
cppfor(int i = 0; i < 20; ++i) for(int mask = 0; mask < (1 << 20); ++mask) if (!(mask & (1 << i))) cnt[mask] = (cnt[mask] + cnt[mask ^ (1 << i)]) % MOD; -
容斥计算:
cpplong long ans = 0; for(int mask = 0; mask < (1 << 20); ++mask) { long long combinations = (pow2[cnt[mask]] - 1); // 2^f(mask) - 1 if (__builtin_popcount(mask) % 2 == 1) ans = (ans - combinations + MOD) % MOD; else ans = (ans + combinations) % MOD; }
为什么这道题很“高级”?
这道题巧妙地利用了位运算的对偶性:
- 题目求的是 AND(向下的操作)。
- 容斥原理将其转化为 超集 统计(向上的操作)。
- SOS DP 完美解决了多维空间下的前缀和计算,将
降到了 。
这道题你打算自己动手写一下吗?在写 pow2 预处理和处理取模时需要注意细节。如果你写好了,我们可以讨论一下这道题的变体:如果要求 OR 结果恰好为
信号反射
这种感觉非常正常。在 Codeforces 上,2200 分(Candidate Master 等级)及以上的题目,通常不会只考查一个孤立的知识点,而是考查知识点的复合转化。
你能通过这三个层级的推导,说明你已经具备了极强的逻辑拆解能力。比赛时想不出来,往往是因为还没建立起特定的**“信号反射”**。
如何建立“信号反射”:看到什么该想 SOS DP + 容斥?
你可以把这道题抽象成一个“思维模版”,以后看到类似特征就往这方面靠:
1. 信号一:关键词“子序列” + “位运算结果为 X”
- 反射:子序列意味着
的组合,位运算意味着我们可以按位考虑。 - 直觉:这通常涉及“贡献转换”。我们不能枚举子序列,但我们可以枚举位掩码 (mask)。
2. 信号二:要求结果“恰好”是 0,或者是某个特定值
- 反射:在位运算中,“恰好”往往意味着“某些位必须是 0”。
- 直觉:使用容斥原理 (PIE)。
- “恰好为 0”
“没有任何位全为 1”。 - “恰好为
” “固定某些位为 1,其余位随意”再进行容斥。
- “恰好为 0”
3. 信号三:需要对 个掩码分别计算某种“包含关系”的统计量
- 反射:如果暴力计算每个 mask 的超集/子集,复杂度是
或 。 - 直觉:这就是 SOS DP 的主场。它就是为了高效处理这种“高维前缀和”而生的。
你的思维进化路径
我们可以用一张图来总结你这次学习的思维升级:
代码段
graph TD
Level1[目标: 恰好为 0] -- "思维跳跃: 容斥原理" --> Level2[中间态: 至少某些位全是 1]
Level2 -- "思维跳跃: 组合数学" --> Level3[基础态: 统计有多少个数字是 mask 的超集]
Level3 -- "思维跳跃: 算法优化" --> Level4[工具: 超集 SOS DP]
style Level1 fill:#f9f,stroke:#333
style Level4 fill:#bbf,stroke:#333
给你的练习建议(巩固这个反射点)
如果你想在下次比赛中“秒看”出这种思路,可以尝试按照这个梯度再刷两三道题:
| 题目 | 重点 | 理由 |
|---|---|---|
| CF 383E (Vowels) | 补集 + SOS DP | 训练“正难则反”的直觉。 |
| ARC 100E (Or Plus Max) | SOS DP 维护非加和信息 | 让你意识到 SOS DP 不只是求和,还可以求最大值/次大值。 |
| CF 914G (Sum the 2^n) | FWHT + SOS DP | 这是位运算 DP 的终极挑战,涉及三种位运算(XOR, AND, OR)的综合运用。 |
老师的寄语
“算法不是背出来的,是‘看’出来的。”
你现在觉得难,是因为你正在把“离散数学的理论”翻译成“竞赛代码的本能”。你刚才对
、 和 的严谨推导,已经比绝大多数只会背模版的选手走得深得多。
你现在已经连续攻克了 SOS DP 的三个核心应用:补集转化、子集求和、超集容斥。