Codeforces Round #257 (Div. 1)D. Jzzhu and Numbers || CounterCode 2015 Subset

D. Jzzhu and Numbers

题意:给你n(1<=n<=1e6)个数a1,a2,…an(0<=ai<=1e6);问有多少种方案满足 a i 1 & a i 2 & . . . & a i k   =   0 ( 1   ≤   k   ≤   n ) a_{i1} \& a_{i2} \& ... \& a_{ik} = 0 (1 ≤ k ≤ n) ai1&ai2&...&aik=0(1kn)

官方题解为:
定义 F ( x ) 为 x & a i = x 的 数 量 F(x) 为 x \& a_{i}=x的数量 F(x)x&ai=x(很明显,相当于x的父亲的数量)
定义 G ( x ) 为 x 二 进 制 下 的 1 的 数 量 G(x)为x二进制下的1的数量 G(x)x1
则答案为: ∑ i = 0 2 20 ( − 1 ) G ( x ) ∗ 2 F ( x ) \sum_{i=0}^{2^{20}}(-1)^{G(x)}*2^{F(x)} i=0220(1)G(x)2F(x)

然后没了,这让我等吃瓜群众就懵逼了。。。

学了Sosdp的话(没学先去学一手)很容易的计算出F(x),并且明白他的状态的意义(x的父亲的数量),这里不加以阐述了。

我们要求 a i 1 & a i 2 & . . . & a i k   =   0 ( 1   ≤   k   ≤   n ) a_{i1} \& a_{i2} \& ... \& a_{ik} = 0 (1 ≤ k ≤ n) ai1&ai2&...&aik=0(1kn) 2 F ( 0 ) 2^{F(0)} 2F(0) 代表着所有ai选还是不选的方案,所以我们还需要减去不合理的方案,哪些是不合理的方案呢,很明显了,所有的F[x]单独都是不合理的,全都需要减去.(因为所有的状态都是0的father)
但是这里就有一个问题了,比如 11(二进制) 也是 01(二进制) 的父亲,则我F[1]也是包含F[3]的,并且10也是包含11的,所以呢,这不就是容斥吗。明白之后可以直接写了。
在这里插入图片描述
注:因为我感觉那个式子应该为 ∑ i = 0 2 20 ( − 1 ) G ( x ) ∗ ( 2 F ( x ) − 1 ) \sum_{i=0}^{2^{20}}(-1)^{G(x)}*(2^{F(x)}-1) i=0220(1)G(x)(2F(x)1), 然后也AC了,然后我来口胡一下,[0,1<<M]一定有偶数位,+1,-1会抵消,end。

#include <bits/stdc++.h>
const int N=4e6+5;
const int M=21;
const int mod=1e9+7;
#define LL long long
using namespace std;
int n,F[N],a[N],p[N];
inline void add(int &x,int y){
    x+=y;
    if(x>=mod)x-=mod;
    if(x<0)x+=mod;
}
inline int count1(int x){
    int cnt=0;
    while(x){
        if(x&1)
            cnt++;
        x>>=1;
    }
    return cnt;
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        F[a[i]]++;
    }
    for(int i=0;i<M;i++)
        for(int j=0;j<(1<<M);j++)
            if(j&(1<<i))
                F[j^(1<<i)]+=F[j];
    p[0]=1;
    for(int i=0;i<(1<<M);i++)
        add(p[i],2*p[i-1]);
    int ans=0;
    for(int i=0;i<(1<<M);i++){
        if(count1(i)&1)
            add(ans,-(p[F[i]]-1));
        else
            add(ans,p[F[i]]-1);
    }
    cout<<ans;
    return 0;
}

另外一题同样有意思的题目:
CounterCode 2015 Subset

注:该题解借鉴于https://blog.csdn.net/weixin_38686780/article/details/100109753,我只用了自己的话加以描述。

给你N(n<=2e5)次操作,有以下三种操作。

  • add s
  • del s
  • cnt s

保证 s ∈ [ 0 , 2 16 ] s\in [0,2^{16}] s[0,216]
add s 表示增加一个数 s,
del s 表示删除一个数 s,
cnt s 表示查找满足 a&s=a 的数量。(相当找有多少个儿子)

分析:很容易想到当次操作 2 16 2^{16} 216 的做法,这是肯定不行的。
分析之后发现复杂度跟s的子集有关,相当于 s在二进制下1的数量有关,设cnt[s] 表示 s 在二进制下1的数量,当 cnt[s]<=8时,我们可以直接枚举子集,单次操作 O ( 2 8 ) O(2^{8}) O(28),当 cnt[s]>8 呢?
很容易先想到对 s 取反,设ss为s取反后的数,以前是为了求 a&s=a 的数量 ,现在变为了求 a&ss=0的数量,由于 ss 的 cnt[ss]<=8 ,我们可以枚举子集,然后用容斥算出 cnt[i]<=8的方案数(下面会讲为什么是这样容斥),那 cnt[i]>8的=部分答案我们如何计算呢?因为 cnt[s]>8,a&s=a 等价于算s的儿子数量,我们可以用一个数组F来计算当 cnt[i]>8时,F[i]的儿子数量<==>每个s对齐父亲的贡献为1,可以枚举父亲来实现。

现在讲如何容斥(数学不好,总是在这部分要理解很久。)
前面已经讲了,现在变为了求 a&ss=0的数量,由于 ss 的 cnt[ss]<=8。我们可以用一个数组E来表示: E[i]代表 cnt[s]<=8 并且s为i的父亲的数量,则E[0]肯定是所有的cnt[s]<=8的数量和,我们要减去不合法的,等价于减去 ((i&ss)!=0) 的部分,这个 i 则一定是ss的儿子(或者叫子集),则一路算过去,相当于答案 = ∑ i = 0 s s ( − 1 ) c n t [ i ] ∗ E [ i ] ( 前 提 是 i 是 s s 的 儿 子 ) \sum_{i=0}^{ss}(-1)^{cnt[i]}*E[i](前提是i是ss的儿子) i=0ss(1)cnt[i]E[i](iss)

总结一下:

  • 当 cnt[s]<=8,直接计算答案,我们用个数组D[i]来判断 i 出现了多少次
  • 当 cnt[s]> 8,设ss为s取反,然后容斥计算cnt<=8的的答案,对于cnt>8的答案直接加上F[s]就行。
  • 总的时间复杂度 O ( n ∗ 2 8 ) O(n*2^{8}) O(n28),我看大佬们还要*个8,我不知道为什么,可以我哪里不会算
#include <bits/stdc++.h>
const int N=4e6+5;
const int M=16;
const int mod=1e9+7;
#define LL long long
using namespace std;
int n,F[N],E[N],D[N];
/**
cnt[s]:s在二进制下1的数量
E[i]:表示cnt[s]<=8的父亲数量,表示:i&s=i
D[i]:表示cnt[s]<=8时i的数量
F[i]:表示cnt[s]> 8时儿子数量,表示:i&s=s
举个例子:
下面的数都是二进制(不足16位前面补0)
E[01111111] 表示值为01111111和11111111的数量
D[01111111] 表示值为01111111的数量
F[1111111111]表示值为1111111111,0111111111,1011111111,...,1111111110的数量

*/
int cnt(int x){
    int ans=0;
    while(x){
        if(x&1)ans++;
        x>>=1;
    }
    return ans;
}
void upd(int s,int val){
    int num=cnt(s);
    if(num<=8){
        D[s]+=val;
        for(int i=s;i;i=(i-1)&s)
            E[i]+=val;
        E[0]+=val; //因为枚举子集并不会算到0
    } else {
        for(int i=s;i<(1<<16);i=(i+1)|s)
            F[i]+=val;
    }
}
void query(int s){
    int num=cnt(s);
    int ans=0;
    if(num<=8){
        ans+=D[0];
        for(int i=s;i;i=(i-1)&s)
            ans+=D[i];
    } else {
        ans+=E[0]+F[s];
        int ss=((1<<16)-1)^s;
        for(int i=ss;i;i=(i-1)&ss)
            if(cnt(i)&1)
                ans-=E[i];
            else
                ans+=E[i];
    }
    printf("%d\n",ans);
}
int main(){
    cin>>n;
    while(n--){
        char s[10];
        int  id;
        scanf("%s%d",s,&id);
        if(s[0]=='a')
            upd(id,1);
        else if(s[0]=='d')
            upd(id,-1);
        else
            query(id);
    }
    return 0;
}



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值