u p d upd upd o n on on 2024.09.08 : 2024.09.08 : 2024.09.08: 修改了 [ A R C 100 E ] O r P l u s M a x [ARC100E]Or Plus Max [ARC100E]OrPlusMax 的题解部分。
u p d upd upd o n on on 2024.09.22 : 2024.09.22 : 2024.09.22: 增加了 苹果树 的题面。
它是一种基于前缀和的子集求和;
他是属于状压 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}。
所以可以得到:
具体的转化可以由下图得知:
我们的问题是
S
(
10110
,
4
)
S(10110,4)
S(10110,4) ,它可以通过一步步递推求解。
通过上述图片,就可以得到一个更优化的程序!
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(N∗2N) ,很多题目我们都是用这个复杂度的做法来实现子集求和!!!
以上图片参考这篇文章!
题目
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
2N−1
非法方案数:只要选出的数二进制中某一位都是
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=2N−1− 非法方案数 。
#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
2N−1 ,不是
2
M
−
1
2^M-1
2M−1 !!!
#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
iorj≤K ” ,说明
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. 回文子串
题目如下:
这道题是一道比较难的子集DP题了。(对于我这种弱鸡,花了三个多小时才做出来)
对于一道题,如何入手很关键。 ——LGJ
这道题在SOSDP这一专题内,但如果直接想SOSDP,是很难想到的,尤其是在考场上。
这一题,咋一看,毫无头绪。那要是在考场上遇到,怎么办? 凉拌 ,那就暴力呗!那怎么暴力?这是这道题的关键。入口对了,暴力想对了,再把它优化,就是是正解了。
我们看到这种算回文串数量的题,就可以枚举子串,计算出每个子串对答案的贡献即可,这题亦可如此。我们可以设子串的左右端点分别为
i
i
i ,
j
j
j (
i
≤
j
i \le j
i≤j ) ,那根据这题,子串中可能有
?
?
? ,我们可以枚举对答案有贡献的子串(举个例子):
若有一个字符串为
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 的贡献为
3
4
3^4
34,因为
?
?
? 的选择与它无关,所以
?
?
? 可以随便填;
当子串为
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 优化。(此题仅供思考)