子集和(SOS, Sum Over Subsets) DP
前置知识
高维前缀和, 状压 DP
.
高维前缀和
前缀和可以简单理解为「数列的前 \(n\) 项的和」, 是一种重要的预处理方式, 能大大降低查询的时间复杂度. -- OI Wiki
二维 / 多维前缀和
常见的多维前缀和的求解方式有两种:
-
基于容斥原理, 时间复杂度 \(\mathcal{O}(2^n V)\), 其中 \(n\) 为维数, \(V\) 为值域.
可以发现, 随着维数的增加, 时间复杂度呈指数级增长, 因此对于多维前缀和, 我们通常采用逐维拓展的方法来求. -
逐维前缀和, 以维为单位, 逐维拓展, 时间复杂度就会降为 \(\mathcal{O}(nV)\), 具体实现可以参考以下代码.
求二维前缀和:
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
a[i][j] += a[i - 1][j];
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
a[i][j] += a[i][j - 1];
求三维前缀和:
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
for (int k = 1; k <= n; ++k)
a[i][j][k] += a[i - 1][j][k];
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
for (int k = 1; k <= n; ++k)
a[i][j][k] += a[i][j - 1][k];
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
for (int k = 1; k <= n; ++k)
a[i][j][k] += a[i][j][k - 1];
对于更高维的前缀和如法炮制即可.
概念
SOS DP(高位前缀和 / 子集 DP
), 状压 DP
的一种, 主要用来解决子集方案数和贡献的问题.
问题引入
给定一个含 \(2^n\) 个整数的序列 \(a\), 我们需要求一序列 \(f\), 其中 \(f_{\mathbb{S}} = \sum_{i \in \mathbb{S}} a_i\).
更通俗地, 就是求解每个集合中所有子集元素之和的总和. 其中每个 \(a_i\) 只有 选(1) 和 不选(0) 两种状态, 所以 \(\mathbb{S}\) 其实是一个二进制串.
\(\tt{Solution}. 1\) 暴力
考虑最朴素的暴力, 枚举集合的内容和子集直接统计, 时间复杂度 \(\mathcal{O}(4^n)\).
时间复杂度十分之高, 我们发现大多时间都浪费在寻找子集上, 故我们需要一种方法只枚举子集(这里定义子集 \(\mathbb{T} \subseteq \mathbb{S}\), 是指 \(\forall i, t_i \le s_i\)).
\(\tt{Solution}. 2\) 优化后的暴力
假设现在有一个二进制串 \(\mathbb{S} = 1011\), 那么它有子集 \(1011, 1010, 1001, 1000, 0011, 0010, 0001, 0000\).
容易发现 \(\mathbb{S}\) 的子集数量其实是 \(2^{\rm{popcount}(\mathbb{S})}\).
当 \(\mathbb{T}\) 从 \(1011 \to 1010\), 可以发现直接减一即可.
但是对于 \(1000 \to 0011\), 似乎就不能这么干了, 因为 \(1000\) 减一会得到 \(0111\).
充分发挥人类智慧, 可以发现 \(\mathbb{S}\ \rm{and}\ 0111 = 0011\) ?!
为什么会这样? 原因是本来该是 0 的一位出现了 1, 所以我们进行一次与操作即可, 也就是 \(\mathbb{T} \gets (\mathbb{T} - 1) \ \rm{and}\ \mathbb{S}\).
时间复杂度是多少呢? 因为我们只枚举了有用的子集状态, 假设 \(\rm{popcount}(\mathbb{S}) = k\), 那么我们就会枚举 \(2^k\) 次, 那么状态总共有 \(\binom{n}{k}\) 个, 所以总的迭代次数为 \(\sum_{k = 0}^n \binom{n}{k} 2^k = (2 + 1)^n = 3^n\) 个, 因此时间复杂度是 \(\mathcal{O}(3^n)\).
\(\tt{Solution}. 3\) 正解
SOS DP.
设 \(f_{\mathbb{S}}\) 表示集合为 \(\mathbb{S}\) 时元素之和的总和.
直接对于整体求解很难, 考虑从局部到整体: 令 \(f_{\mathbb{S}, i}\) 为在集合 \(\mathbb{S}\) 后 \(i\) 位确定时的总和 (假设最低位为 \(0\), 最高位为 \((n - 1)\)).
初始化 \(f_{S, -1} = a_S\), 对于每一个 \(S\), 对于其子集 \(T\), 有 \(\forall i \in [1, n - 1], T_i \le S_i\).
形式化地:
计算贡献:
可以结合图片理解下.
这样, 时间复杂度被我们成功优化成了 \(\mathcal{O}(n \times 2^n)\).
相关例题:
P6442 [COCI2011-2012#6] KOŠARE.
Maximum And Queries (hard version).
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现