Kor (数学题...)

kor

10.19

思路:
考虑维护cnt数组,cnt[i]表示是i的数有几个。
考虑维护从cnt1数组,cnt1[i]表示是i的二进制子集的数有几个。
显然cnt1可以从cnt转移过来,但是为了优化时间复杂度,我们选择把cnt和cnt1合并为一个数组用2^20*20的时间处理出来。
代码如下

void sumup() {
    for(int i=0; i<P; i++) {
        int s = ((1<<P) - 1) ^ (1<<i);
        for(int ss = s; ss >= 0; ss=(ss-1)&s) {
            cnt[ss | (1<<i)] += cnt[ss];
            //cnt[ss] += cnt[ ss| ( 1 << i ) ] ;
            fix(cnt[ss | (1<<i)]);
            if( !ss ) break;
        }
    }
}

枚举每一位,把每一位为0的cnt累加到此位为1的cnt中。
对于单个的数进行考虑(如15),cnt1[15] += cnt[i] i为15的子集。这里写图片描述
有关15的转移(每个数在加入15之前的转移,之后再怎么变化都跟15无关了)大致如上图,一看就很有道理有没有,怎么证明呢?
考虑我们for位数时是从低位往高位枚举的(枚举把哪一位的1消掉),15加上了所有在某一位上比它少1的数的cnt,如果每个数在枚举第i位时被加入了15(如11,i=3),那么在此之前它(11)一定加上了所有在某一位x(x<=i)上比它(11)少1的数的cnt(10, 9),而这些数(10, 9)是在第i-1(2)位被加入(11)的,那么在此之前它们(10, 9)一定加上了所有在某一位x(x<=i-1)上比它们(10, 9)少1的数的cnt(如cnt[9] += cnt[8])。
这样:
1.我们一定统计的数一定合法。
因为每个数统计的都是它的某一位上1变为0的cnt,所有统计到的一定是子集。
2.我们一定统计完了所有合法的数。
因为我们统计了每个数的所有后继状态(某一位上1变为0的状态)。
3.对于任意一个数我们一定没有重复统计。
我们按照1的个数将所有数进行分层操作:
15
14 13 11 7
12 10 9 6 5 3
8 4 2 1
那么显然不同层的数之间是不会相互影响的,每个数的cnt只会加到某一位上比它多1的数的cnt里。
考虑同一层的数,用第2层举例,它们加入15是有先后顺序的(从低位到高位某个1变为0)
14 1110
13 1101
11 1011
7 0111
而每一个数我们只考虑它加入15之前的变化,14在i=1(枚举的位数)时就加入15了,并没有任何的操作,13在i=2时加入15,所以x位(x < i)为0的情况已经被统计过了,容易发现一个数统计的数都是去掉某些x位上的0(x < i)得到的。也就是说y位上的数(y >= i)是不会改变的,也就是说13产生的数在第i位(i=2)上都是0,而比它大的数(14)产生的数在第i位(i=2)上都是1。而y位上的数(y >= i)一定是一样的,所以比它大的数(14)产生的数都大于它(13)产生的数,所以同层的不同数产生的数并无交集。而每个数显然不会加上多个相同的后继,所以对于任意一个数我们一定没有重复统计。
这样就能证明算法的正确性了。
可能有人觉得,15(1111)都是1太特殊,其实对于任意一个数,我们讨论子集的时候只需要管它为1的二进制位,(111100101)可直接看做(111111),只要之后的数都与它对应就好了。
还是给出关于14的转移看看吧。
这里写图片描述

处理完cnt之后,就要处理ans了。
C(cnt, k) 并不是ans。如(cnt[15], k)。
这些方案组成的(|)不只是15(1111),还会有15的子集如1101,0010等等。这里就要用到容斥的思想了。
(用二进制表示)
ans[1111] = cnt[1111] - cnt[1110] - cnt[1101] - cnt[1011] - cnt[0111] + cnt[1100] + cnt[1010] + cnt[1001] + cnt[0110] + cnt[0101] + cnt[0011] - cnt[1000] - cnt[0100] - cnt[0010] - cnt[0001]。
代码如下:

void sumdown() {
    for(int i=0; i<P; i++) {
        int s = ((1<<P) - 1) ^ (1<<i);
        for(int ss = s; ss >= 0; ss=(ss-1)&s) {
            cnt[ss | (1<<i)] -= cnt[ss];
            fix(cnt[ss | (1<<i)]);
            if( !ss ) break;
        }
    }
}

其实和上面是差不多的,只需要改成减法即可,就不再赘述啦。

还有一道kand的题目是&,只需要改成第一个代码片里//的部分就好啦。

纯手打呀~~~不容易不容易。。。(手残图丑,莫怪)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1e5 + 10;
const int Mod = 1e9 + 7;
const int P = 20;

int n, k, r;
int aa[N];
int cnt[1<<P];
int fac[N], vfac[N];

int mpow(int a, int b) {
    int rt;
    for(rt = 1; b; b>>=1,a=(1LL*a*a)%Mod)
        if(b&1) rt=(1LL*rt*a)%Mod;
    return rt;
}

void init(int n) {
    fac[0] = 1;
    for(int i = 1; i <= n; i++)
        fac[i] = 1LL * fac[i-1] * i % Mod;
    vfac[n] = mpow(fac[n], Mod - 2);
    for(int i = n - 1; i >= 0; i--)
        vfac[i] = 1LL * vfac[i+1] * (i + 1) % Mod;
}

int comb(int n, int m) {
    if(m > n) return 0;
    return 1LL * fac[n] * vfac[m] % Mod * vfac[n-m] % Mod;
}

void fix(int &a) {
    while(a >= Mod) a -= Mod;
    while(a < 0) a += Mod;
}

void sumup() {
    for(int i=0; i<P; i++) {
        int s = ((1<<P) - 1) ^ (1<<i);
        for(int ss = s; ss >= 0; ss=(ss-1)&s) {
            cnt[ss | (1<<i)] += cnt[ss];
            //cnt[ss] += cnt[ ss| ( 1 << i ) ] ;
            fix(cnt[ss | (1<<i)]);
            if( !ss ) break;
        }
    }
}

void sumdown() {
    for(int i=0; i<P; i++) {
        int s = ((1<<P) - 1) ^ (1<<i);
        for(int ss = s; ss >= 0; ss=(ss-1)&s) {
            cnt[ss | (1<<i)] -= cnt[ss];
            fix(cnt[ss | (1<<i)]);
            if( !ss ) break;
        }
    }
}

int main() {
    freopen("kor.in", "r", stdin);
    freopen("kor.out", "w", stdout);
    int T; scanf("%d", &T);
    init(1e5);
    while(T--) {
        memset(cnt, 0, sizeof(cnt));
        scanf("%d%d%d", &n, &k, &r);
        for(int i = 1; i <= n; i++) {
            scanf("%d", aa + i);
            cnt[aa[i]]++;
        }
        sumup();
        for(int s=0; s<(1<<P); s++) 
            cnt[s] = comb(cnt[s], k);
        sumdown();
        printf("%d\n", cnt[r]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值