[HNOI2012]集合选数(思维构造 + 状压dp)

problem

题目链接

solution

从最小一个数 x x x 开始,将其 2 x , 3 x 2x,3x 2x,3x 放入,再将 2 ( 2 x ) , 3 ( 2 x ) , 2 ( 3 x ) , 3 ( 3 x ) 2(2x),3(2x),2(3x),3(3x) 2(2x),3(2x),2(3x),3(3x) 放入,以此类推 … \dots 将其合并为一个集合。重复又找一个最小未进入集合的数 x x x

显然答案为若干个互不相交,并集为 [ 1 , n ] [1,n] [1,n] 的集合答案的乘积。

一个集合里面的数字是相互制约的,不难发现集合大小不超过 log ⁡ 2 n ∗ log ⁡ 3 n \log_2n*\log_3n log2nlog3n,大概就是 17 × 11 17\times 11 17×11

考虑怎么计算一个独立集合的可选方案数。

很巧妙的是这个限制只有两个(两倍和三倍),在平面内可以构造出一个矩阵。

矩阵中 ( i , j ) (i,j) (i,j) 的右边位置放其数值的三倍,下面位置放其数值的两倍。

1 3 9 ...

2 6 18 ...

4 12 36 ...

那么答案就等价为在矩阵中随机选任意个数,要求数两两不相邻。

矩阵行列数都比较小,可以采取状压 d p dp dp 求解。

显然某一行是否合法与矩阵真实长相无关,至于选择的位置有关。

所以提前预处理一行的选择状态,判断是否合法。不合法当且仅当有相邻两列都选了,用 i<<1&i 判断。

之后枚举当前行及操作状态,在合法的基础上,枚举上一行的操作状态,再判断两行之间没有同一列都操作了,用 s&t=0 判断。

能构造矩阵,完全是因为限制一个数的条件只有两个,可以放在二维平面内。这完全就是很投巧的想法。

code

#include <bits/stdc++.h>
using namespace std;
#define mod 1000000001
#define int long long
int N, n, m, ans = 1;
bool vis[100005], g[1 << 20];
int lim[20], a[20][20], f[20][1 << 20];

void init( int x ) {
    for( int i = 1;;i ++ ) {
        if( i == 1 ) a[i][1] = x;
        else a[i][1] = a[i - 1][1] << 1;
        if( a[i][1] > N ) break;
        else n = i;
        vis[a[i][1]] = 1;
        lim[i] = 2;
        for( int j = 2;;j ++ ) {
            a[i][j] = a[i][j - 1] * 3;
            if( a[i][j] > N ) break;
            else vis[a[i][j]] = 1, lim[i] = 1 << j;
        }
    }
}

int solve() {
    for( int i = 0;i < lim[1];i ++ ) f[1][i] = g[i];
    for( int i = 2;i <= n;i ++ )
        for( int s = 0;s < lim[i];s ++ ) {
            if( ! g[s] ) continue;
            f[i][s] = 0;
            for( int t = 0;t < lim[i - 1];t ++ )
                if( g[t] and ! (s & t) )
                    f[i][s] = ( f[i][s] + f[i - 1][t] ) % mod;
        }
    int ret = 0;
    for( int i = 0;i < lim[n];i ++ )
        ret = ( ret + f[n][i] ) % mod;
    return ret;
}

signed main() {
    for( int i = 0;i < (1 << 19);i ++ )
        g[i] = ! (i << 1 & i);
    scanf( "%lld", &N );
    for( int i = 1;i <= N;i ++ )
        if( ! vis[i] ) init( i ), ans = ans * solve() % mod;
    printf( "%lld\n", ans );
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值