古典叙事 · 技术札记

序章

山河有卷
人间有声

写代码,也写长风、旧城与未熄的灯。
愿每一篇随笔,都有自己的山水与回声。
阅览随笔

SOSDP(多维前缀和)

在状态压缩 DP 中,我们经常会遇到这样的问题:
给定一个数组 \(A\),我们需要求一个新的数组 \(F\),使得 \(F[mask]\) 等于 “\(mask\) 的所有子集”(或超集)的 \(A\) 值之和。
数学表达:\(F[mask] = \sum_{sub \subseteq mask} A[sub]\)
暴力枚举子集的复杂度:\(O(3^N)\) (当 \(N=20\) 时,计算量约 34 亿,必然 TLE)。
SOS DP 的复杂度:\(O(N \cdot 2^N)\) (当 \(N=20\) 时,计算量约 2000 万,跑得飞快!)。

核心思想

对于每一个二进制,我们把他当成n维空间的一个坐标,每次只关注一个维度,让数据在这一个维度上流动,循环n次,数据就自动汇聚到了终点

  1. 严谨的状态定义(二维)定义 dp[i][mask] 表示:在状态 mask 的子集中,只有最右边的 \(i\) 位可以和 mask 不同,第 \(i\) 位往左的高位必须和 mask 严格一致的所有子集之和。
  2. 状态转移方程我们现在要推导 dp[i][mask](处理到了第 \(i\) 位,当前状态是 mask):
    情况 A:mask 的第 \(i\) 位是 0那么它的子集在第 \(i\) 位也必须是 0。所以前 \(i\) 位能产生的子集,和前 \(i-1\) 位一模一样。dp[i][mask] = dp[i-1][mask]
    情况 B:mask 的第 \(i\) 位是 1它的子集在这一位既可以是 1,也可以是 0。所以把它拆成两部分:第 \(i\) 位是 1 的子集之和 + 第 \(i\) 位是 0 的子集之和。dp[i][mask] = dp[i-1][mask] + dp[i-1][mask ^ (1 << i)]
  3. 终极空间优化(一维滚动)因为 dp[i] 的状态只依赖于 dp[i-1],我们完全可以像“01背包”那样,把第一维 i 砍掉,原地滚动更新!并且,因为我们用的是上一层(第 \(i-1\) 层)的异或状态,不论是正向遍历 mask 还是逆向遍历,都不会产生数据污染(因为提供数据的格子在这个循环里是只读的)。

三、 标准代码模板(直接默写,必背!)

模板 1:求子集和(要求越来越低)特征:1 去拿 0 的数据。场景:求所有属于我子集的值的和(例如:技能比我少的人的集合)。

// 假设最大状态数有 N 位二进制 (N=20)
int N = 20;
int max_mask = 1 << N;
// F[mask] 初始存放原数组的值 A[mask]

// ⚠️ 极其重要:外层循环必须是维度 i,内层循环必须是状态 mask!
for (int i = 0; i < N; ++i) { 
    for (int mask = 0; mask < max_mask; ++mask) {
        // 如果我的第 i 位是 1
        if (mask & (1 << i)) { 
            // 我就去把第 i 位是 0 的兄弟(子集)拿过来
            F[mask] += F[mask ^ (1 << i)];
        }
    }
}
// 运行结束后,F[mask] 里装的就是 mask 所有子集的和

模板 2:求超集和(要求越来越高)特征:0 去拿 1 的数据。场景:求所有包含了我的超集的值的和(例如:通关科目包含我的人的集合)

int N = 20;
int max_mask = 1 << N;
// F[mask] 初始存放原数组的值 A[mask]

for (int i = 0; i < N; ++i) {
    for (int mask = 0; mask < max_mask; ++mask) {
        // 如果我的第 i 位是 0 (注意前面有个 !)
        if (!(mask & (1 << i))) { 
            // 我就去把第 i 位是 1 的老哥(超集)拿过来
            // 注意:这里可以用 F[mask ^ (1<<i)] 也可以用 F[mask | (1<<i)]
            F[mask] += F[mask | (1 << i)];
        }
    }
}
// 运行结束后,F[mask] 里装的就是 mask 所有超集的和

四、 进阶心法:

正难则反(De Morgan's Law)有时候题目要求解的是“超集”,但我们不想写模板 2,或者我们对求“子集”更顺手,怎么办?利用集合论的定律:如果集合 \(A \supseteq B\)(A是B的超集),那么它们的补集一定满足 \(\complement A \subseteq \complement B\)。操作步骤(求超集转化为求子集):输入时取反:不再记录“拥有的正向技能”,而是记录“缺失的缺点(补集)”。bad_mask = ((1 << N) - 1) ^ mask;把初始值存入 F[bad_mask]。跑子集模板:一字不改,直接跑上面的【模板 1】。查询时取反:想查状态 k 的超集和,直接去查它补集的子集和:ans = F[((1 << N) - 1) ^ k]。总结:求超集 = 数据取反存入 + 求子集 + 补集查询。

posted @ 2026-07-30 19:41  Morphis‘  阅读(16)  评论(0)    收藏  举报