codeforces 895C

(状压dp)
题意:给定一个集合,里面包括 n (1n105)个数字 a[i] (1a[i]70) ,求出这个集合中有多少子集,使得子集内部所有数字的乘积为平方数。

思路:观察到 a[i] 的范围比 n 要小很多,于是我们可以用一个小数组保存:大小为i的数字出现过几次,然后枚举 i (1i70)。计数问题很有可能是dp,枚举 i 时最关键是要想到怎么保存i之前数字的乘积有哪些?这里由于平方数的在质因子分解后,每个质因子的幂一定是偶数,于是想到只保存质因子取二进制最低位后的乘积,然而乘积太大也没法保存。于是观察到70以内的质因子只有不到20个,就可以考虑用状态压缩保存。然后dp方程就很好想了。(最后记得优化一下内存空间)
cnt[i]=0 时直接将 i1 转移到 i 即可。
cnt[i]0时:

{dp[i][j]=dp[i][j]+dp[i1][j]2cnt[i]1dp[i][jmask[i]]=dp[i][jmask[i]]+dp[i1][j]2cnt[i]1

代码:

#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
#define LL long long

using namespace std;
const int maxn = 100050;
const LL mod = 1e9 + 7;

LL dp[2][1<<19], pw[maxn];
int a[maxn], cnt[80], bitmask[80];
vector<int> prim;

void init() {
    // get 2^n
    pw[0] = 1;
    for(int i=1; i<maxn; i++)
        pw[i] = 2LL * pw[i-1] % mod;
    prim.clear();
    memset(cnt, 0, sizeof(cnt));
    memset(bitmask, 0, sizeof(bitmask));
    memset(dp, 0, sizeof(dp));
    // get prime number
    for(int i=2; i<71; i++) {
        bool flag = 1;
        for(int j=2; j*j<=i; j++)
            if(i%j == 0) flag = 0;
        if(flag) prim.push_back(i);
    }
    // get bitmask for each i
    for(int i=2; i<71; i++) {
        int t = i;
        for(int j=0; j<(int)prim.size(); j++) {
            int num = 0, div = prim[j];
            while(t%div == 0) {
                t /= div;
                num ++;
            }
            if(num&1)
                bitmask[i] += 1<<j;
        }
    }
}

int main() {
    //freopen("test.txt","r",stdin);
    init();
    int n;
    scanf("%d",&n);
    for(int i=0; i<n; i++) {
        scanf("%d",&a[i]);
        cnt[a[i]] ++;
    }
    // solve
    int sz = (int)prim.size();
    dp[0][0] = 1;
    for(int i=1; i<71; i++) {
        int mask = bitmask[i], cur = i&1, last = !cur;
        for(int j=0; j<(1<<sz); j++) {
            if(cnt[i] == 0)
                dp[cur][j] = dp[last][j];
            else {
                dp[cur][j] = (dp[cur][j] + dp[last][j]*pw[cnt[i]-1]%mod) % mod;
                dp[cur][j^mask] = (dp[cur][j^mask] + dp[last][j]*pw[cnt[i]-1]%mod) % mod;
            }
        }
        for(int j=0; j<(1<<sz); j++)
            dp[last][j] = 0;
        //printf("%d : %I64d\n",i,dp[i][0]);
    }
    printf("%I64d\n",(dp[0][0]-1+mod)%mod);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值