【学习笔记】SOS DP/高维前缀和
参考文献:https://codeforces.com/blog/entry/45223
名字来源
Sum over Subsets
例题引入
考虑一个这样的问题:给定一个 \(2^n\) 集合,对于每一个状态 \(s\),求:
方法介绍
对于传统的暴力方法,考虑对于每一个状态 \(s\),枚举其子集,时间复杂度为 \(O(3^n)\),显然超时。
于是就有了 \(SOS DP\)。
设计状态 \(f_{s,j}\) 表示固定前 \(j\) 位与 \(s\) 相同,后 \(n-j\) 位为 \(s\) 后 \(n-j\) 为的子集。
举个例子,\(f_{10101,3}\) 就囊括 10100 和 10101。
考虑如何转移:
- 若第 \(j\) 位是 \(0\),则只有可能往 \(0\) 转移,即 \(f_{s,j} = f_{s,j-1}\).
- 若第 \(j\) 位是 \(1\),则有两种情况,一种是往 \(1\) 转移,一种是往 \(0\) 转移,则有 \(f_{s,j} = f_{s,j-1} + f_{s \oplus (1<<j),j-1}\).
code
for(int s=0;s<(1<<n);s++){
for(int i=1;i<=n;i++)
f[i][s]=f[i-1][s];
if(s&(1<<i-1))f[i][s]+=f[i-1][s^(1<<i-1)];
}
F[s]=f[n][s];
}
更简洁的写法:
for(int i=1;i<=n;i++)
for(int s=0;s<(1<<n);s++)
if(s&(1<<i-1))F[s]+=F[s^(1<<i-1)];
例题
[ARC100E] Or Plus Max
题意
给定 \(2^{n}\) 个数,对于每一个 \(k\),求满足 \(i|j \leq k\) 的两个数 \(i,j\) 的 \(a_i+a_j\) 最大。
思路
令 \(i|j = s\), \(s\) 满足 \(s \leq k\),且 \(i \subseteq s\) 且 \(j \subseteq s\)。
于是就可以使用 \(SOS DP\),维护满足 \(\subseteq s\) 的数的最大值和次大值。
code
#include<bits/stdc++.h>
#define ll long long
#define x first
#define y second
using namespace std;
const int N = 20;
int n;
ll a[1<<N];
pair<ll,ll> f[1<<N];
void update(ll u,ll s){
if(f[u].y<s)f[u].y=s;
if(f[u].y>f[u].x)swap(f[u].x,f[u].y);
}
int main() {
cin>>n;
for(int i=0;i<(1<<n);i++)
cin>>a[i],f[i].x=a[i];
for(int i=1;i<=n;i++){
for(int j=(1<<n)-1;j>=0;j--){
if(j&(1<<i-1))
update(j,f[j^(1<<i-1)].x),update(j,f[j^(1<<i-1)].y);
}
}
ll ans=0;
for(int i=1;i<(1<<n);i++){
ans=max(ans,f[i].x+f[i].y);
cout<<ans<<"\n";
}
return 0;
}
CF165E Compatible Numbers
题意
给定一个数组 \(a\),对于每个元素求与它兼容的下标最大的数。
定义兼容为 \(x \And y = 0\).
思路
\(x\And y=0\) 可以转化为 \(y \subseteq \complement_U x\).
于是可以转化为 \(SOSDP\).
code
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 23;
int n;
int a[1<<N];
int f[1<<N];
int main() {
cin>>n;
memset(f,-1,sizeof(f));
for(int i=1;i<=n;i++)
cin>>a[i],f[a[i]]=max(i,f[a[i]]);
for(int i=0;i<22;i++)
for(int j=(1<<22)-1;j>=0;j--)
if(j&(1<<i))f[j]=max(f[j],f[j^(1<<i)]);
for(int i=1;i<=n;i++){
ll id=f[((1<<22)-1)^a[i]];
cout<<(id==-1 ? -1 : a[id])<<" ";
}
return 0;
}
CF449D Jzzhu and Numbers
题意
给定一个数组 \(a\),求有多少种选数方案使得所有数的和与起来为 \(0\).
思路
不妨把 \(a_i\) 全部取反,则所要求的就变成了选定的所有数或起来为全集。
强制要求为全集肯定不好做,考虑容斥,钦定某几位或起来一定为 \(0\),其他不管。
接下来的问题就是求一个集合的所有子集的数有多少个,这可以用 \(SOSDP\) 做。
code
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N = 20,mod = 1e9+7;
int n;
int a[1<<N],b[1<<N];
ll f[1<<N];
ll qpow(ll a,ll b){
ll res=1;
while(b){
if(b&1)res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
int main() {
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i],b[i]=(1<<20)-1-a[i],f[b[i]]++;
for(int i=0;i<20;i++){
for(int j=(1<<20)-1;j;j--)
if(j&(1<<i))f[j]+=f[j^(1<<i)],f[j]%=mod;
}
ll ans=0;
for(int i=0;i<(1<<20);i++){
ll res=19-__builtin_popcount(i);
if(res&1)ans+=qpow(2,f[i]);
else ans-=qpow(2,f[i]);
ans%=mod,ans=(ans+mod)%mod;
}
cout<<ans;
return 0;
}
CF1208F Bits And Pieces
题意
给定一个数组 \(a\),对于 \(i<j<k\),求所有 \(a_i|(a_j \And a_k)\) 的最大值。
思路
变一下题目,就变成了求 \(a_i + a_j \And a_k \And \complement_U a_i\).
三个数与起来并不好做,可以先把 \(a_i\) 的补集提出来,考虑另外两个怎么做,对于两个数 \(&\) 起来的结果,且这两个数的下标均要大于 \(i\).
所以就可以记录最大值和次大值,然后算 \(a_i\) 的时候考虑贪心,从最高位往下选,判断选这一位合不合法的条件就是判断加上这一位后的状态记录的次大值是否会不小于 \(i\).
code
#include<bits/stdc++.h>
#define ll long long
#define x first
#define y second
using namespace std;
const int N = 22;
ll n;
pair<ll,ll> f[1<<N];
ll a[1<<N],b[1<<N];
void update(int u,int mx){
if(f[u].y<mx)f[u].y=mx;
if(f[u].y>f[u].x)swap(f[u].x,f[u].y);
}
int main() {
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
update(a[i],i);
}
for(int i=0;i<21;i++){
for(int j=0;j<(1<<21);j++){
if(!(j&(1<<i))){
update(j,f[j^(1<<i)].x);
update(j,f[j^(1<<i)].y);
}
}
}
ll ans=0;
for(int i=1;i<=n-2;i++) {
int c=((1<<21)-1)^a[i];
int res=0;
for(int j=21;j>=0;j--){
if(!((c>>j)&1))continue;
res+=(1<<j);
if(f[res].y<=i)res-=(1<<j);
}
ans=max(ans,res+a[i]);
}
cout<<ans;
return 0;
}

浙公网安备 33010602011771号