BZOJ 2734 [HNOI2012] 集合选数
程序员文章站
2024-03-23 22:53:40
...
题目描述 Description
《集合论与图论》这门课程有一道作业题,要求同学们求出{1,2,3,4,5}的所有满足以下条件的子集:若x在该子集中,则2x和3x不能在该子集中.同学们不喜欢这种具有枚举性质的题目,于是把它变成了以下问题:对于任意一个正整数n,如何求出{1,2,…,n}的满足上述约束条件的子集的个数
输入描述 Input Description
只有一行,其中有一个正整数n
输出描述 Output Description
仅包含一个正整数,表示{1,2,…,n}有多少个满足上述约束条件的子集,答案对1,000,000,001取模
样例输入 Sample Input
4
样例输出 Sample Output
8
数据范围及提示 Data Size & Hint
对于30%的数据,
对于100%的数据
Solution
不难发现,其实很多数之间都是相互独立的,我们就把这些数单独取出来
对于每个数x来说,2x,3x都不能选,那么就搞一个的集合出来单独选就好了
对于每个集合,最多有个元素
然后对于每个矩阵DP,最后将不同矩阵利用乘法原理乘起来
矩阵DP细节在代码里,自己看看就好
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int mod = 1000000001;
long long tot=1;
int n;
int bin[20];
int a[20][20];
int b[20];//表示每一行有哪些数可以选
int f[20][2048];
bool vis[100005];
int read() {
int ans=0,flag=1;
char ch=getchar();
while((ch<'0' || ch>'9') && ch!='-') ch=getchar();
if(ch=='-') { flag=-1;ch=getchar(); }
while(ch>='0' && ch<='9') { ans=ans*10+ch-'0';ch=getchar(); }
return ans*flag;
}
int cal(int x) {
for(int i=1;i<=18;i++) b[i]=0;
a[1][1]=x;
for(int i=2;i<=18;++i) {
if((a[i-1][1]<<1)<=n) a[i][1]=(a[i-1][1]<<1);
else a[i][1]=n+1;//为什么要设为n+1而不是直接退出呢?因为后面计算b[i]的时候要用
}
for(int i=1;i<=18;++i)
for(int j=2;j<=11;++j) {
if(a[i][j-1]*3<=n) a[i][j]=a[i][j-1]*3;
else a[i][j]=n+1;//同上
}
for(int i=1;i<=18;++i)
for(int j=1;j<=11;++j)
if(a[i][j]<=n) {//若之前的a[i][j]直接退出,则后面的数都为0,都会被计算入<=n当中
//其实把bin做成前缀和然后特判0也可以QwQ
b[i]+=bin[j-1];
vis[a[i][j]]=1;
}
for(int i=0;i<=18;i++)
for(int j=0;j<=b[i];++j)
f[i][j]=0;
f[0][0]=1;
for(int i=0;i<18;++i)
for(int j=0;j<=b[i];++j)
if(f[i][j])
for(int k=0;k<=b[i+1];k++)
if(((j&k)==0) && ((k&(k>>1))==0))
f[i+1][k]=(f[i+1][k]+f[i][j])%mod;
//直接枚举两个状态(当前/下一行)进行转移
//非常的简单
return f[18][0];
}
int main() {
bin[0]=1;//bin[i]=(1<<i)
for(int i=1;i<20;i++)
bin[i]=(bin[i-1]<<1);
n=read();
for(int i=1;i<=n;++i)
if(!vis[i]) {
tot*=cal(i);
tot%=mod;
}
printf("%d\n",tot);
return 0;
}