SOS DP(子集DP)(高维前缀和)

它是一种基于前缀和的子集求和;

他是属于状压 D P DP DP的一个分支;

他是一个优化的子集运算的方法。

基本概念

S O S : SOS : SOS: S u m Sum Sum o v e r over over S u b s e t s Subsets Subsets

有时,我们会碰到一些问题:求一些数的所有集合之和 。(我好像不知道怎样表达)

下面来举个例子:

n n n 个数,这些数分别是 a 1 , a 2 , a 3 . . . . . . a n a_1 , a_2 , a_3 ...... a_n a1,a2,a3......an
n = 5 n = 5 n=5
a [ 5 ] = a [5] = a[5]= { 12 , 34 , 45 , 28 , 37 12 , 34 , 45 , 28 , 37 12,34,45,28,37 }
这些数的下标分别是 1 , 2 , 3 , 4 , 5 1 , 2 , 3, 4 , 5 1,2,3,4,5

这些数的下标可以形成的集合有:
{ 1 1 1} , { 2 2 2} , { 3 3 3} , { 4 4 4} , { 5 5 5} ,
{ 1 , 2 1,2 1,2} , { 2 , 3 2,3 2,3} , { 3 , 4 3,4 3,4} , { 4 , 5 4,5 4,5} ,
{ 1 , 2 , 3 1,2,3 1,2,3} , { 2 , 3 , 4 2,3,4 2,3,4} , { 3 , 4 , 5 3,4,5 3,4,5} ,
{ 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4} , { 2 , 3 , 4 , 5 2,3,4,5 2,3,4,5} ,
{ 1 1 1, 2 2 2, 3 3 3, 4 4 4, 5 5 5}

设每个集合中的数的和为 A i A_i Ai i i i 表示每个集合(用二进制数表示),
!!! f m a s k f_{mask} fmask 表示集合 m a s k mask mask 的子集之和 。(后面也会沿用此数组)
则最暴力的代码为:

	for(int mask=0;mask<(1<<n);mask++)
		for(int i=0;i<(1<<n);i++)
			if((mask&i)==i) f[mask]+=A[i];

以上的时间复杂度为 O ( 4 N ) O(4^N) O(4N) ,这样显然超时。
我们想到可以优化枚举 m a s k mask mask 的子集的时间:

	for(int mask=0;mask<(1<<n);mask++){
		f[mask]=A[0];
		for(int i=mask;i>0;i=(i-1)&mask)
			f[mask]+=A[i];
	}

时间复杂度为(通过二项式定理得): ( 1 + 2 ) N = 3 N (1+2)^N = 3^N (1+2)N=3N
这样时间复杂度就化为了 O ( 3 N ) O(3^N) O(3N),但这样依然会超时。

我们不妨设 S ( m a s k , i ) S(mask,i) S(mask,i) 表示 m a s k mask mask 在二进制下每次能修改后 i + 1 i+1 i+1 位的其中一位的方案。
例如: S ( 1011010 , 3 ) = S(1011010,3)= S(1011010,3)= { 1011010 , 1010010 , 1011000 , 1010010 1011010 , 1010010 , 1011000 , 1010010 1011010,1010010,1011000,1010010}。
所以可以得到:
SOS DP
具体的转化可以由下图得知:
我们的问题是 S ( 10110 , 4 ) S(10110,4) S(10110,4) ,它可以通过一步步递推求解。

SOS DP 2
通过上述图片,就可以得到一个更优化的程序!

	for(int mask=0;mask<(1<<n);mask++){
		f[mask][-1]=A[mask];
		for(int i=0;i<n;i++){
			if(mask&(1<<i))
				dp[mask][i]=dp[mask][i-1]+dp[mask^(1<<i)][i-1];
			else
				dp[mask][i]=dp[mask][i-1];
		}
		f[mask]=dp[mask][n-1];
	}

当然,有的时候二维数组是开不下的,我们就要优化空间。
我们发现,当前一层的运算只和他的上一层有关,我们可以考虑将二维转化为一维。
所以就变成了如下程序:

	for(int i=0;i<(1<<n);i++)
		f[i]=A[i];
	for(int i=0;i<n;i++)
		for(int mask=0;mask<(1<<n);mask++)
			f[mask]+=f[mask^(1<<i)];

这两个代码的时间复杂度都为 O ( N ∗ 2 N ) O(N * 2^N) O(N2N) ,很多题目我们都是用这个复杂度的做法来实现子集求和!!!

以上图片参考这篇文章

题目

1. Compatible Numbers

这道题目是一道比较简单的模板题。
我们很容易发现 ~ B B B A A A 的子集,我们只需要求 A A A 的子集的最大值就可以解决了。(把模板的求和改为求最大值

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5,maxs=(1<<22),S=(1<<22)-1;
int n,a[maxn],f[maxs];
int main(){
	scanf("%d",&n);
	for(int i=0;i<(1<<22);i++)
		f[i]=-1;
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		f[a[i]]=a[i];
	}
	for(int i=0;i<22;i++)
		for(int j=0;j<=S;j++)
			if(j&(1<<i)) f[j]=max(f[j],f[j^(1<<i)]);
	for(int i=1;i<=n;i++)
		printf("%d ",f[(~a[i])&S]);
	return 0;
}

2. Jzzhu and Numbers

这道题是求方案数,求方案数可以用到 组合数学,DP ,莫比乌斯反演,容斥原理 等。!!!
这道题可以从 容斥原理 入手,因为答案不好直接求,那我们可以 总方案数 − - 非法的方案数 ,从而得到答案
总方案数: 2 N − 1 2^N-1 2N1
非法方案数:只要选出的数二进制中某一位都是 1 1 1 ,那一定不合法。我们可以 + + + 只有一位都是是 1 1 1 的方案数, − - 有两位都是 1 1 1 的方案数, + + + 有三位是 1 1 1 的方案数, − - 有四位都是 1 1 1 的方案数……以此类推;
最后, a n s = 2 N − 1 − ans= 2^N-1- ans=2N1 非法方案数 。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5,maxs=(1<<20),S=(1<<20)-1;
const long long mod=1e9+7;
int n,one[maxs],f[maxs];
long long pow2[maxn],ans;
int main(){
	scanf("%lld",&n);
	for(int i=1,x;i<=n;i++){
		scanf("%d",&x);
		f[x]++;
	}
	pow2[0]=1;
	for(int i=1;i<=n;i++)
		pow2[i]=(pow2[i-1]*2)%mod;
	for(int i=0;i<20;i++)
		for(int j=0;j<=S;j++)
			if((1<<i)&j) f[j^(1<<i)]+=f[j];
	ans=pow2[n]-1;
	for(int i=1;i<=S;i++){
		one[i]=one[i>>1]+(i&1);
		if(one[i]&1) ans=(ans-(pow2[f[i]]-1)+mod)%mod;
		else ans=(ans+(pow2[f[i]]-1)+mod)%mod;
	}
	printf("%lld",ans);
	return 0;
}

3. [COCI2011-2012#6]KOŠARE

这道题和上面那题大致相同,上面是求选出来的数二进制 & 为 0 0 0 的方案数,这里是求选出的数二进制 | 为 1 1 1 的方案数。
所以只需要把开始输入的数二进制取反即可套上题程序。
注意:总方案数为 2 N − 1 2^N-1 2N1 ,不是 2 M − 1 2^M-1 2M1 !!!

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5,maxm=20,maxs=(1<<20);
const long long mod=1e9+7;
int n,m,one[maxs],f[maxs];
long long pow2[maxn],ans;
int main(){
	scanf("%d%d",&n,&m);
	int S=(1<<m)-1;
	for(int i=1,x;i<=n;i++){
		scanf("%d",&x);
		int tmp=0;
		for(int j=1,y;j<=x;j++){
			scanf("%d",&y);
			tmp|=(1<<(y-1));
		}
		f[S^tmp]++;
	}
	pow2[0]=1;
	for(int i=1;i<=n;i++)
		pow2[i]=(pow2[i-1]*2)%mod;
	for(int i=0;i<m;i++)
		for(int j=0;j<=S;j++)
			if((1<<i)&j) f[j^(1<<i)]+=f[j];
	ans=pow2[n]-1;
	for(int i=1;i<=S;i++){
		one[i]=one[i>>1]+(i&1);
		if(one[i]&1) ans=(ans-(pow2[f[i]]-1)+mod)%mod;
		else ans=(ans+(pow2[f[i]]-1)+mod)%mod;
	}
	printf("%lld",ans);
	return 0;
}

4. [ARC100E]Or Plus Max

这题要回归 S O S D P SOS DP SOSDP 本身。
因为题目说 “ i o r j ≤ K i \mathbin{\mathrm{or}} j \le K iorjK ” ,说明 i i i j j j K K K 的子集。那么我们就可以用 S O S D P SOS DP SOSDP 来完成这题。
在求 子集DP 的过程中,我们记录最大值和次最大值,询问时就可以求出 0 0 0~ l e n len len 的最大的最大值和次最大值之和。
注意:输入时的下标从 0 0 0 开始,不然会影响后面的计算!!!

#include<bits/stdc++.h>
using namespace std;
const int maxs=(1<<20);
int n,f[maxs][2];
int main(){
	scanf("%d",&n);
	int len=(1<<n),S=(1<<n);
	for(int i=0,a;i<len;i++){
		scanf("%d",&a);
		f[i][0]=a;
	}
	for(int i=0;i<n;i++)
		for(int j=0;j<=S;j++)
			if((1<<i)&j){
				int k=(j^(1<<i)),b[5]={f[j][0],f[j][1],f[k][0],f[k][1]};
				sort(b,b+4);
				f[j][0]=b[3];
				f[j][1]=b[2];
			}
	int ma=f[0][0]+f[0][1]; 
	for(int i=1;i<len;i++){
		ma=max(ma,f[i][0]+f[i][1]);
		printf("%d\n",ma);
	}
	return 0;
}

5. 回文子串

题目如下:
SOS DP 3
SOS DP 4
SOS DP 5
SOS DP 6
这道题是一道比较难的子集DP题了。(对于我这种弱鸡,花了三个多小时才做出来)

对于一道题,如何入手很关键。 ——LGJ

这道题在SOSDP这一专题内,但如果直接想SOSDP,是很难想到的,尤其是在考场上。
这一题,咋一看,毫无头绪。那要是在考场上遇到,怎么办? 凉拌 ,那就暴力呗!那怎么暴力?这是这道题的关键。入口对了,暴力想对了,再把它优化,就是是正解了。
我们看到这种算回文串数量的题,就可以枚举子串,计算出每个子串对答案的贡献即可,这题亦可如此。我们可以设子串的左右端点分别为 i i i j j j ( i ≤ j i \le j ij ) ,那根据这题,子串中可能有 ? ? ? ,我们可以枚举对答案有贡献的子串(举个例子):
若有一个字符串为 a ? b a b ? ? c ? b a?bab??c?b a?bab??c?b , 候选串为 a b c abc abc
当子串为 ? ? ? ,它对 a n s ans ans 的贡献是 3 4 3^4 34 ,因为不管 ? ? ? 填什么,都合法;
当子串为 a a a ,它对 a n s ans ans 的贡献为,因为 ? ? ? 的选择与它无关,所以 ? ? ? 可以随便填;
当子串为 a ? a? a? ,它只能是 a a a 才是回文串,候选串中有 a a a ,所以它对 a n s ans ans 的贡献为 3 3 3^3 33
当子串为 ? ? ?? ??,两个 ? ? ? 填的数必须相同,所以它对答案的贡献为 3 3 3^3 33
当子串为 b a b bab bab ,所有 ? ? ? 填的数都与这个字串无关,所以它对答案的贡献为 3 4 3^4 34
当子串为 b ? ? b?? b?? ,第二个 ? ? ? 一定是 b b b ,中间的没有限制,只要候选串有 b b b ,他对答案的贡献就为 3 3 3^3 33
……
从上面可以看出:有的子串要在某些条件下才会对答案有贡献,有些则不需要条件,总之对于每个子串只要满足一定的条件(有的不需要)我们就能直接算出答案。然而,这些条件一定是候选串的子串。这样的枚举子集,不就能用 S O S D P SOSDP SOSDP 优化吗!
所以,这道题我们可以先递推出字符串 S S S 的子串对答案的条件和贡献,然后枚举候选串的所有情况,计算每个候选串之下的答案,最后询问时 O ( 1 ) O(1) O(1) 就能出答案了。(对照代码理解会更清晰)

代码中的变量名释义:
设字符串从 1 1 1 开始。
w h [ i ] wh[i] wh[i] :字符串从 1 1 1 ~ i i i ? ? ? 个数;
c n t [ i ] [ j ] cnt[i][j] cnt[i][j] :子串 i , j i,j i,j 为回文串时对答案贡献的数量;
n e e d [ i ] [ j ] need[i][j] need[i][j] :子串 i , j i,j i,j 为回文串时需要的条件,将所需字母转化为二进制表示;
p o w [ x ] [ y ] pow_[x][y] pow[x][y] :预处理 x x x y y y 次方;
o k [ i ] [ j ] ok[i][j] ok[i][j] :记录子串 i , j i,j i,j 是否可能为回文串;

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5,maxs=(1<<17)+5,S=(1<<17)-1;
const long long mod=998244353;
int n,q,wh[maxn],cnt[maxn][maxn],need[maxn][maxn];
long long pow_[20][maxn],f[maxs][20],dp[maxs][20],ans[maxs][20];
bool ok[maxn][maxn];
char s[maxn],t[20];
vector<int>b[maxs];
int main(){
	scanf("%d%s",&n,s);
	for(int i=n;i>=1;i--)  //字符串从下标为1开始
		s[i]=s[i-1];
	for(int i=1;i<=n;i++){
		if(s[i]=='?') wh[i]=wh[i-1]+1;
		else wh[i]=wh[i-1];
	}
	for(int i=0;i<=n;i++)
		pow_[0][i]=1;
	for(int i=1;i<=17;i++){
		pow_[i][0]=1;
		for(int j=1;j<=n;j++)
			pow_[i][j]=(pow_[i][j-1]*i)%mod;
	}
	for(int i=0;i<=n+1;i++)  //ok数组初始化时从0~n+1!
		for(int j=0;j<=n+1;j++)
			ok[i][j]=true;
	for(int len=1;len<=n;len++){  //这里的i,j有可能>n,所以ok的初始化要到n+1
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1,x=i+1,y=j-1;
			if(ok[x][y]==false) ok[i][j]=false;
			else if(s[i]==s[j]&&s[i]=='?'){
				cnt[i][j]=cnt[x][y]+1;
				need[i][j]=need[x][y];
			}
			else if(s[i]==s[j]&&s[i]!='?'){
				cnt[i][j]=cnt[x][y];
				need[i][j]=need[x][y];
			}
			else if(s[i]!=s[j]&&s[i]=='?'){
				cnt[i][j]=cnt[x][y];
				need[i][j]=(need[x][y]|(1<<(s[j]-'a')));
			}
			else if(s[i]!=s[j]&&s[j]=='?'){
				cnt[i][j]=cnt[x][y];
				need[i][j]=(need[x][y]|(1<<(s[i]-'a')));
			}
			else ok[i][j]=false;
			if(ok[i][j]==true){
				int tmp=cnt[i][j]+wh[i-1]+(wh[n]-wh[j]);
				b[need[i][j]].push_back(tmp);
			}
		}
	}
	for(int i=1;i<=17;i++)
		for(int j=0;j<=S;j++)
			for(int k=0;k<b[j].size();k++)
				f[j][i]=(f[j][i]+pow_[i][b[j][k]])%mod;
	for(int i=1;i<=17;i++){
		for(int j=0;j<=S;j++)
			for(int k=0;k<=17;k++)
				dp[j][k]=0;
		for(int j=0;j<=S;j++){
			dp[j][0]=f[j][i];  //这里不能把dp数组合并成一维,中途会出错
			if(j&1) dp[j][0]=(dp[j][0]+f[j^1][i])%mod;
			for(int k=1;k<17;k++){
				dp[j][k]=(dp[j][k]+dp[j][k-1])%mod;
				if(j&(1<<k)) dp[j][k]=(dp[j][k]+dp[j^(1<<k)][k-1])%mod;
			}
			ans[j][i]=dp[j][16];
		}
	}
	scanf("%d",&q);
	while(q--){
		scanf("%s",t);
		int len=strlen(t),mask=0;
		for(int i=0;i<len;i++)
			mask+=(1<<(t[i]-'a'));
		printf("%lld\n",ans[mask][len]);
	}
	return 0;
}

6. 苹果树

用 点分治 做,以 S O S D P SOSDP SOSDP 优化。

  • 34
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值