《集合论与图论》
这门课程有一道作业题,要求同学们求出
的所有满足以 下条件的子集:若
在该子集中,则
和
不能在该子集中。
同学们不喜欢这种具有枚举性质的题目,于是把它变成了以下问题:对于任意一个正整数 ,如何求出 的满足上述约束条件的子集的个数(只需输出对 取模的结果),现在这个问题就 交给你了。
一道构造
+状压dp
的好题目。
我们可以构造这么一个矩阵,第 行第 个数是 ,其后,每行的第 个元素是该行第 个数的 倍,每列的第 个数是该列的第 个数的 倍。比如这样:
1 3 9 27 81...
2 6 18 54 162...
4 12 36 108 324...
然后,问题就变成了在这个矩阵中选数,且相邻的数不能同时选的方案数。
考虑到其行数和列数不是很大,所以我们可以用状压dp
轻松解决。
记第 行的数取或不取的状态用 表示, 表示第 行状态为 时的答案。转移具体看代码。
注意因为从 开始构造矩阵不能把所有的数包含在内,需要把所有可能的矩阵构造出来,然后根据乘法原理,答案相乘即可。
const ll mod=1000000001;
const int N=100100,M=20;
ll line[M],f[2][1<<18],n;
bool chose[N],g[(1<<18)+5];
int a[M][M],lim[M];ll ans=1ll,t;
inline void initialization(){
for(int i=0;i<(1<<18);i++)
if ((i&(i<<1))==0)
g[i]=true;
else g[i]=false;
}//预处理g数组(g[i]:i是否为可行集合)
inline void make_rectangle(int x){
for(int i=1;i<12;i++){
if (i==1) a[i][1]=x;
else a[i][1]=a[i-1][1]*3;
if (a[i][1]>n) break;
t=i;chose[a[i][1]]=true;line[i]=1;
for(int j=2;j<19;j++){
a[i][j]=a[i][j-1]*2;
if (a[i][j]>n) break;
line[i]=j;chose[a[i][j]]=true;
}
lim[i]=(1<<line[i])-1;
}
}//以x为首个数字构造我们需要的矩阵
inline ll dp(){
register int i,j,k;
for(i=0;i<=lim[1];i++)
f[1][i]=g[i];
for(i=2;i<=t;i++)
for(j=0;j<=lim[i];j++)
if (g[j]){
f[i&1][j]=0ll;//不能用memset
for(k=0;k<=lim[i-1];k++)
if (g[k]&&((j&k)==0))
f[i&1][j]=(f[i&1][j]+f[(i&1)^1][k])%mod;
}
register ll res=0ll;
for(i=0;i<=lim[t];i++)
if (g[i]) res=(res+f[t&1][i])%mod;
return res;
}//状压dp求解子问题
int main(){
scanf("%lld",&n);
initialization();
for(int i=1;i<=n;i++)
if (!chose[i]){
make_rectangle(i);
ans=(ans*dp())%mod;
}
printf("%lld",ans);
return 0;
}
- 一般在数据范围不大的
dp
题中,我们会优先地考虑状压dp
。 - 如果状态可以转化为一个东西取或不取,放或不放,就可以用二进制表示这个状态,然后把它放入状态的定义中,使用二进制运算符把这个问题变成
状压dp
求解的题目。 - 二进制运算不是很好理解,在上一篇博客中我们讲了二进制运算符,所以大家可以根据状态的定义和二进制运算符的运算规矩画图理解。