「学习笔记」SOS DP
SOS DP 学习笔记
0.0 前言
本文大部分译自 CF 博客上的原文。Link here
0.1 前置知识
- 状压 DP
1.0 简介
SOS DP,全称 Sum over Subsets dynamic programming,意为子集和 DP,用来解决一些涉及子集和计算的问题。
1.1 例题引入
给定一个含 个整数的集合 A,我们需要计算:对于每个集合 ,求 中所有元素 的 的和,即求:
1.2 解决方案
1.2.1 朴素算法
直接按照题意模拟即可,复杂度为 。
for(int sta=0;sta<(1<<N);sta++)
for(int i=0;i<(1<<N);i++)
if((sta&i)==i)F[sta]+=A[i];
1.2.2 枚举子集
对于每个状态,我们只遍历它的子集而去除了无关状态。如果一个状态的二进制位上只有 个 ,我们只需枚举它的 个子集。这样的状态一共有 个,因此总迭代次数 ,时间复杂度即为 。
for(int sta=0;sta<(1<<N);sta++)
{
F[sta]=A[0];
for(int i=sta;i>0;i=(i-1)&sta)
F[sta]+=A[i];
}
1.2.3 SOS DP
上面枚举子集的方法有明显的缺陷:当一个状态的二进制位上有 个 时,它将在其他(不包含本身)状态迭代时被访问 次,存在重复的计算。
而产生这种现象的原因就是:我们没有在 被不同 利用时建立一定的联系。我们应添加另一个状态来避免上述的重复计算。
定义状态 。现在我们把这个集合划分为不相交的组。
。我们将二进制位数从 开始从低位向高位表示,那集合 就表示只有第 位以及更低位与 不同的 的集合。
举个例子:10110101011010101001010110001010000 。其中 中的 1010 即为 的第 至第 位,集合中的元素的加粗部分都与 保持一致。
让我们尝试将 与 建立联系。
-
的第 位是 。
显而易见地, 与 的第 位均为 。因此 仅有 至 位与 不同,故有 。
-
的第 位是 。
那么 就有两部分组成:
第一部分: 的第 位为 ,即为 。
第二部分: 的第 位为 ,即为 。
下图描述了如何将 集合相互关联。任何集合 的元素都是其子树中的叶子。前缀表示的这一部分对其所有子结点都是公共的,而部分允许不同。
请注意,这些关系形成一个有向无环图,而不一定是有根树(请考虑当 不同但 相同时)
在实现了这些关系之后,我们可以很容易地写出相应的动态规划。
for(int sta=0;sta<(1<<N);sta++)
{
dp[sta][-1]=A[sta];// 叶结点
for(int i=0;i<N;i++)
{
if(sta&(1<<i))
dp[sta][i]=dp[sta][i-1]+dp[sta^(1<<i)][i-1];
else dp[sta][i]=dp[sta][i-1];
}
F[sta]=dp[sta][N-1];
}
上述的算法浪费了较多空间,注意到每次 这一维都由 转移而来,因此可以采取滚动数组的方式优化空间。
for(int i=0;i<(1<<N);i++)
F[i]=A[i];
for(int i=0;i<N;i++)
for(int sta=(1<<N)-1;sta>=0;sta--)
if(sta&(1<<i))
F[sta]+=F[sta^(1<<i)];
值得注意的是,在此处 这一维可以采用正序枚举的方式。
原因:……
上述算法的时间复杂度即为 。
1.3 应用范围
其实 sosdp 不仅可以求子集和,还可以求超集和。
超集和的核心代码如下:
for(int i=0;i<N;i++)
for(int sta=(1<<N)-1;sta>=0;sta--)
if(!(sta&(1<<i)))
F[sta]+=F[sta^(1<<i)];
细心的读者会注意到,求子集和与求超集和之间的区别仅差一个 !
。
如何理解呢?这相当于我们把二进制中的每一个 当作 看待, 当作 看待。比如 ,那 就会从 转移而来。而 为 的子集, 为 的超集。
2.0 推荐习题
我在 vjudge 上组了一个题单,大家可以去练习一下。
点击此处 密码为 sosdpiseasyforme
2.1 习题简析
2.1.1 A - Compatible Numbers
题意简述
给定 的元素 ,求对于每个 是否存在 满足 且 ,并输出 。
数据范围:,。
题目分析
sosdp 可以用来求子集和,而本题中满足条件的 与 不存在同一集合中。怎么办?想到将 取反,取反后得到的数 与 的关系即为 。在 dp 过程中将求和改为记录从谁转移而来,问题迎刃而解。
其实这道题本质上还是状压,如果将题目改为求个数,那才有 sosdp 的感觉。
代码展示
#include<bits/stdc++.h>
using namespace std;
const int M=1e6+5;
int all=(1<<22)-1;
int n,a[M],F[(1<<22)];
inline int read()
{
int x=0,f=1;char ch;
while(ch=getchar(),ch<48)if(ch==45)f=0;
do x=(x<<1)+(x<<3)+(ch^48);
while(ch=getchar(),ch>=48);
return f?x:-x;
}
int main()
{
memset(F,-1,sizeof(F));
n=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
F[a[i]]=a[i];
}
for(int sta=0;sta<=all;sta++)
{
if(F[sta]!=-1)continue;
for(int i=0;i<22;i++)
if(sta&(1<<i))
if(F[sta^(1<<i)]!=-1)
{
F[sta]=F[sta^(1<<i)];
break;
}
}
for(int i=1;i<=n;i++)
{
int t=all&(~a[i]);
printf("%d ",F[t]);
}
return 0;
}
2.1.2 B - Or Plus Max
题意简述
给定 个数:。对于 ,求 的最大值,同时满足 。
数据范围:,。
题目分析
此题需要我们对问题进行转化。
我们先对每个 求出满足 的 的最大值,然后对答案求前缀最大值即可。但仍难以处理。
换一种求法,对于每个 求出 的 的最大值,然后求前缀最大值。对状态 维护单个元素的最大值和次大值。
套上 sosdp,每次转移时注意一下合并时最大值和次大值的变化。还需注意 的初值为 。
代码展示
#include<bits/stdc++.h>
#define FM first
#define SM second
using namespace std;
typedef pair<int,int> pii;
const int inf=1e9+1;
const int M=(1<<18);
int n,a[M],ans;
pii F[M];
inline int read()
{
int x=0,f=1;char ch;
while(ch=getchar(),ch<48)if(ch==45)f=0;
do x=(x<<1)+(x<<3)+(ch^48);
while(ch=getchar(),ch>=48);
return f?x:-x;
}
pii Merge(pii A,pii B)
{
if(A.FM<B.FM)swap(A,B);
if(B.FM>A.SM)A.SM=B.FM;
return A;
}
int main()
{
n=read();
for(int i=0;i<(1<<n);i++)
{
a[i]=read();
F[i]=make_pair(a[i],-inf);
}
for(int i=0;i<n;i++)
for(int sta=(1<<n)-1;sta>=0;sta--)
if(sta&(1<<i))
F[sta]=Merge(F[sta],F[sta^(1<<i)]);
for(int i=1;i<(1<<n);i++)
{
int fi=F[i].FM,se=F[i].SM;
ans=max(ans,fi+se);
printf("%d\n",ans);
}
return 0;
}
2.1.3 C - Bits And Pieces
题意简述
给定 个元素 ,对于每个三元组 ,找出 ,其中 。
数据范围:,。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架