[HNOI2012]集合选数

洛咕

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

分析:构建如下的矩阵:

\(\begin{matrix} 1 & 3 & 9 & 27 & 81 &...\\ 2 & 6 & 18 & 54 & 162 & ...\\ 4 & 12 & 36 & 108 & 324 &...\\ 8 & 24 & 72 & 216 & 648 &...\\ ... &... &... &... &...\end{matrix}\)

容易发现,矩阵里面不能取相邻的两个数,这样就可以状压\(DP\)了.设\(f[i][j]\)表示第i行取数的状态为j时的方案数.\(f[i][j]+=f[i-1][k]\)(当\(j\)&\(k\)=0且\(j\)&\(j<<1=0\),\(k\)&\(k<<1=0\):前式表示相邻两行中没有相邻的数都被取到,后式表示同一行中相邻两列没有相邻的数被取到.)

因为\(n<=100000\),\(2^{16}=65536,2^{17}=131072\),所以最多只有17行,同理,\(3^{10}=59049\),\(3^{11}=177147\),所以最多只有11列.

然后因为一个矩阵不可能包括\(1\)\(n\)之内的所有数,所以我们要记录每个数是否在矩阵中出现过,若没出现过就以该数为矩阵的第一行第一列上的元素来构建矩阵,并状压\(DP\)计算方案.

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#include<set>
#define ll long long
using namespace std;
inline int read(){
    int x=0,o=1;char ch=getchar();
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-')o=-1,ch=getchar();
    while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
    return x*o;
}
const int mod=1000000001;
const int N=20;
const int M=100005;
int Base[N],limit[N],visit[M],jz[N][N];
int n;ll ans=1,f[N][M];
inline int solve(int x){
    jz[1][1]=x;//矩阵的第一行第一列的元素
    for(int i=2;i<=17;++i){
        if(jz[i-1][1]*2<=n)jz[i][1]=jz[i-1][1]*2;
        else jz[i][1]=n+1;
    }//构建矩阵的第一列,限制数字最大为n+1
    for(int i=1;i<=17;++i)
        for(int j=2;j<=11;++j){
            if(jz[i][j-1]*3<=n)jz[i][j]=jz[i][j-1]*3;
            else jz[i][j]=n+1;
        }//根据每一行第一列上的数,构建矩阵的每一行
    for(int i=1;i<=17;++i){
        limit[i]=0;//状压枚举每一行状态的最大限制
        for(int j=1;j<=11;++j){
            if(jz[i][j]<=n){
                limit[i]+=Base[j-1];
                visit[jz[i][j]]=1;//标记该数在矩阵中出现过
            }
            else break;//之后的数也都会比n大
        }
    }
    for(int i=1;i<=17;++i)
        for(int j=0;j<=limit[i];++j)
            f[i][j]=0;
    f[0][0]=1;//初始化
    for(int i=1;i<=17;++i)
        for(int j=0;j<=limit[i];++j){
            if(j&(j<<1))continue;//同一行相邻两数不能都取
            for(int k=0;k<=limit[i-1];++k){
                if((j&k)||(k&(k<<1)))continue;
                f[i][j]=(f[i][j]+f[i-1][k])%mod;//累加方案数
            }
        }
    ll cnt=0;
    for(int j=0;j<=limit[17];++j)cnt+=f[17][j];//最后一行每个状态的方案数之和就是此次的贡献
    return cnt;
}
int main(){
    n=read();
    Base[0]=1;for(int i=1;i<=12;++i)Base[i]=Base[i-1]<<1;//预处理出2的次幂
    for(int i=1;i<=n;++i)//对于每一个没有出现过的数计算共吸纳
        if(!visit[i])ans=(1ll*ans*solve(i))%mod;
    printf("%lld\n",ans);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/PPXppx/p/11589024.html