BZOJ_2734_[HNOI2012]集合选数_构造+状压DP

时间:2023-03-08 15:39:56
BZOJ_2734_[HNOI2012]集合选数_构造+状压DP

BZOJ_2734_[HNOI2012]集合选数_构造+状压DP

题意:《集合论与图论》这门课程有一道作业题,要求同学们求出{1, 2, 3, 4, 5}的所有满足以 下条件的子集:若 x 在该子集中,则 2x 和 3x 不能在该子集中。同学们不喜欢这种具有枚举性 质的题目,于是把它变成了以下问题:对于任意一个正整数 n≤100000,如何求出{1, 2,..., n} 的满足上述约束条件的子集的个数(只需输出对 1,000,000,001 取模的结果),现在这个问题就 交给你了。

分析:

我们构造出一个矩阵

$\begin{matrix}
    1&2^03^1&2^03^2\\
    2^13^0&2^13^1&2^13^2\\
    2^23^0&2^23^1&2^23^2\\
\end{matrix}
$

发现矩阵的相邻两个格子的数不能同时取

状压DP一下

要把所有不在矩阵中的数当作1重新构造,比如5,7等等

每个矩阵的结果乘起来就是答案

代码:

#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define LL long long
LL p=1000000001,A,f[18][1<<12];
int vis[100050],s[18],mat[18][18];
LL ans=1;
void build(int x){
int n=1,m=1,now=x;
while(now*3<=A)m++,now*=3;
now=x;
while(now*2<=A)n++,now<<=1;
int mask=(1<<m)-1;
memset(s,0,sizeof(s));
memset(f,0,sizeof(f));
memset(mat,0,sizeof(mat));
mat[1][1]=x;vis[x]=1;
for(int i=2;i<=m;i++){
mat[1][i]=mat[1][i-1]*3;
vis[mat[1][i]]=1;
}
s[1]=mask;
for(int i=2;i<=n;i++){
mat[i][1]=mat[i-1][1]*2;
vis[mat[i][1]]=1;
for(int j=2;j<=m;j++){
mat[i][j]=mat[i-1][j]*2;
if(mat[i][j]>A){
s[i]=mask^((1<<m-j+1)-1);
break;
}
vis[mat[i][j]]=1;
}
if(!s[i])s[i]=mask;
}
f[0][0]=1;
s[0]=mask;
for(int i=0;i<n;i++){
for(int j=0;j<=mask;j++){
if((j|s[i])!=s[i])continue;
if(j&(j<<1))continue;
for(int k=0;k<=mask;k++){
if((k|s[i+1])!=s[i+1])continue;
if(k&(k<<1))continue;
if(j&k)continue;
f[i+1][k]+=f[i][j];
f[i+1][k]%=p;
}
}
}
LL re=0;
for(int i=0;i<=mask;i++)re+=f[n][i],re%=p;
ans=re*ans%p; /*for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
printf("%d ",mat[i][j]);
}
puts("");
}*/ /*for(int i=1;i<=n;i++){
printf("%d\n",s[i]);
}*/
}
int main(){
scanf("%lld",&A);
for(int i=1;i<=A;i++){
if(!vis[i])build(i);
}
printf("%lld",ans);
}