欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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%的数据,1<=n<=20
对于100%的数据1<=n<=100000

Solution

不难发现,其实很多数之间都是相互独立的,我们就把这些数单独取出来

对于每个数x来说,2x,3x都不能选,那么就搞一个x2q3p的集合出来单独选就好了
对于每个集合,最多有log2nlog3n个元素
然后对于每个矩阵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;
}
相关标签: 状态压缩