【题解】BZOJ 2734 [HNOI2012]集合选数

12 篇文章 0 订阅

Description D e s c r i p t i o n

传送门

对于任意一个正整数 n100000 n ≤ 100000 ,如何求出 {1,2,...,n} { 1 , 2 , . . . , n } 的满足只要 x x 在子集中,2x 3x 3 x 就不在子集中的子集的个数(只需输出对 1,000,000,001 1 , 000 , 000 , 001 取模的结果)

Solution S o l u t i o n

非常巧妙的状压DP(一看还真没看出来)。

考虑这样一个矩阵:

124361291836 1 3 9 ⋯ 2 6 18 ⋯ 4 12 36 ⋯ ⋮ ⋮ ⋮ ⋱

这个矩阵的规律是:数 x x 右侧的数是 3x 下方的数是 2x 2 x

这下问题可以转化为在这个矩阵中取数且任意两个数不相邻的方案数。

可能有人会问,像 5 5 7 这样的数没有保存在这里面,但先不要急,我们只需要将这种数放在左上角再制出一张完全不一样的表,这里说的完全不一样指的就是这些左上角数字不同的矩阵不可能有相同的元素。

掐指一算,这个矩阵至多有 log2100000=17 ⌈ log 2 ⁡ 100000 ⌉ = 17 行, log3100000=11 ⌈ log 3 ⁡ 100000 ⌉ = 11 列。

现在就可以用状压解决问题了,我们令 d(i,j) d ( i , j ) 表示进行到第 i i 行,上一行选取的状态为 j (状压)。

首先考虑状态 p p (二进制)。如何才能判定 p 是一个合法的状态呢?我们知道,矩阵取数不能有左右相邻的元素被同时选取,也就是说 p p 的中不可能会有两个连续的 1 。暴力枚举显然是一种下策,其实我们只需要将 p p 右移一位与 p & & 即可。原理是什么呢?我们令:

pp>>1(p>>1) & p===1010100110101000 p = 10101001 p >> 1 = 1010100 ( p >> 1 )   &   p = 0

又有:

pp>>1(p>>1) & p===10110101101101000010000 p = 10110101 p >> 1 = 1011010 ( p >> 1 )   &   p = 00010000

现在就可以看出来了,当且仅当状态 p p 满足 (p>>1) & p=0 时,它是合法的。

现在我们假设我们需要从合法状态 p p 推导到合法状态 q ,如何判定是可以推导的呢?这个就比较简单了, p p q 两个数的二进制表示法中每一位上不可能有两个 1 1 (上下两个相邻的数不可以同时选中),那么当且仅当 p & q=0 时才可以递推。

递推的方式很简单,就是 d(i,q)=d(i,q)+d(i1,p) d ( i , q ) = d ( i , q ) + d ( i − 1 , p )

Code C o d e

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define LL long long
#define R 20
#define C 12
#define N 100005
#define P 1000000001
int a[R][R], d[R][1 << C], c[R];
bool vis[N];
int n, r;
inline int work(int x) {
    vis[a[1][1] = x] = true;
    r = 1;
    while (1) {
        c[r] = 1;
        int tmp = a[r][c[r]] * 3;
        while (tmp <= n && !vis[tmp]) {
            a[r][++c[r]] = tmp;
            vis[tmp] = true;
            tmp *= 3;
        }
        tmp = a[r][1] << 1;
        if (tmp <= n && !vis[tmp])
            vis[ a[++r][1] = tmp ] = true;
        else break;
    }
    d[0][0] = 1;
    c[0] = 0; c[++r] = 0;
    for (int i = 0; i <= r + 1; i++)    
        c[i] = 1 << c[i];
    for (int i = 1; i <= r; i++)
        for (int j = 0; j < c[i]; j++)
            d[i][j] = 0;
    for (int i = 1; i <= r; i++)
        for (int j = 0; j < c[i - 1]; j++) {
            if ((j >> 1 & j) || !d[i - 1][j]) continue;
            for (int k = 0; k < c[i]; k++) {
                if ((k >> 1 & k) || (j & k)) continue;
                (d[i][k] += d[i - 1][j]) %= P;
            }
        }
    return d[r][0];
}
int main() {
    scanf("%d", &n);
    LL ans = 1;
    for (int i = 1; i <= n; i++)
        if (!vis[i]) 
            ans = 1ll * ans * work(i) % P;
    printf("%lld\n", ans);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值