BZOJ 2734 [HNOI2012]集合选数 状压+思路

前言:

总觉得几天没写博客了。
感觉自己被sb题以及sb错误包围了…
那今天就精挑细选几个题写写博客吧。

题意:

在{1,2,3,…..,n}的集合中选出一个子集。
该子集满足一条约束条件:若x在该集合中,那么2*x以及3*x不能在这个集合中。
询问能选出的子集个数对1000000001取模的结果。

解析:

这题的思路蛮有趣的。
我们不妨写出来一个矩阵。

行\列 1 2 3 4 5
1 1 3 9 27 81
2 2 6 18 54 162
3 4 12 36 108 324
4 8 24 72 216 648
5 16 48 144 432 1296

该矩阵的每一个元素的右面的元素都是他的3倍,每一个元素的下面的元素都是他的2倍。
也就是说,如果我们选出的数在该矩阵中是不相邻的话,那么选出的一定是一个符合题意的子集。
因为 n100000 ,所以该矩阵的行和列一定都很小,最多大概是在17。
所以我们可以考虑在这个矩阵上状压每一行,然后统计一下该矩阵可取的方案数即可。
但是我们发现,5,7这种数并没有出现在该矩阵中。
所以这种矩阵可能有多个,在找完1为左上角的该类型矩阵后,我们只需要寻找1~n中的下一个没有出现在矩阵中的元素充当左上角,再次计算即可。
由于不同矩阵中的元素互不影响,所以我们需要把所有可能的矩阵的方案数利用乘法原理乘在一起即可。

后记:

当时好像还脑抽想了一下是不是只有质数能够充当矩阵的左上角,然而并不是这样,比如你想一想49就明白了。

代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define mod 1000000001
#define N 21
using namespace std;
typedef long long ll;
int n;
int a[N][N];
ll f[N][70010];
int vis[100100];
int line[N];
ll calc(int x)
{
    a[1][1]=x;
    int tmp;
    for(int i=1;;i++)
    {
        if(i!=1)
        {
            a[i][1]=a[i-1][1]*2;
            if(a[i][1]>n)
                {tmp=i-1;break;}
        }
        vis[a[i][1]]=1;
        for(int j=2;;j++)
        {
            a[i][j]=a[i][j-1]*3;
            if(a[i][j]>n)
                {line[i]=j-1;break;}
            vis[a[i][j]]=1;
        }
    }
    line[0]=1;
    for(int i=0;i<=tmp;i++)
        for(int j=0;j<(1<<line[i]);j++)
            f[i][j]=0;
    line[tmp+1]=0,f[tmp+1][0]=0;
    f[0][0]=1;
    for(int i=0;i<=tmp;i++)
    {
        for(int j=0;j<(1<<line[i]);j++)
        {
            if(f[i][j])
            {
                if(j&(j>>1))continue;
                for(int k=0;k<(1<<line[i+1]);k++)
                {
                    if(j&k)continue;
                    f[i+1][k]=(f[i+1][k]+f[i][j])%mod;
                }
            }
        }
    }
    return f[tmp+1][0];
}
int main()
{
    scanf("%d",&n);
    ll ans=1;
    for(int i=1;i<=n;i++)
        if(!vis[i])
            ans=ans*calc(i)%mod;
    printf("%lld\n",ans);
}

你可能感兴趣的:(bzoj)