• 集合计数 :容斥原理


    zkt大神的同题题解:  https://www.cnblogs.com/hzoi-DeepinC/articles/11102979.html

    Description

    一个有N个元素的集合有2^N个不同子集(包含空集),现在要在这2^N个集合中取出若干集合(至少一个),使得它们的交集的元素个数为K,求取法的方案数,答案模1000000007。(是质数喔~)

    Input

    一行两个整数N,K

    Output

    一行为答案。

    Sample Input

    3 2

    Sample Output

    6

    Sample Explanation

    假设原集合为{A,B,C}

    则满足条件的方案为:{AB,ABC},{AC,ABC},{BC,ABC},{AB},{AC},{BC}

    Hint

    对于100%的数据,1≤N≤1000000;0≤K≤N;

    写题解不只是为了写题解,重在讲思路,想直接看正解的兄台自行跳跃阅读。

    抱歉我脑子略笨,的确是没能一下想到正解,第一思路是一个n2的暴力

    70%算法

    虽说是暴力,但其实思路并不太好想,因为乍一下没有上正轨,所以最后的思维量几乎比std还大

    然而只有70分,没人会体恤你有多笨QAQ

    假如原问题的含义以函数f(n,k)表示,即n元素交集为k的方案数

    我们可以认为这个过程分为两步:f(n,k)=C(n,k)*f(n-k,0)

    第一步是从n个元素中选出k个C(n,k),其次要任意选集合使它们的交集恰好为这k个元素

    那么我们可以确定,最后被选出的集合中,每个集合都含有这k个元素,而其余元素的交集为空集:f(n-k,0)

    如:{a,b,c}三个元素,k=1时,假如你选定了a,那么可能出现的集合有:{a},{a,b},{a,c},{a,b,c}

    去掉你选中的元素a,剩下的是Ø,{b},{c},{b,c},你要在这几个集合中任选,使它们交集为空

    一种可能的选法是{a},{a,b},{a,b,c},去掉a后剩下Ø,{b},{b,c},它们的交集的确为空

    看起来“其余元素交集为空”这个问题会更好解一些,即f(s,0)。

    假如真的是这样,那么问题就化简成了f(n,k)=C(n,k)*f(n-k,0)

    这样所有的问题都可以依赖f(n-k,0)来求解了。我们只需要求出f(x,0)这个数列

    我一直在尝试找到这个数列的规律,于是我手模了许多点,得到了f(n,k)的表格

    //Update:感谢starsing大神更正数据,f(4,0)应该是64594

    这个表格里貌似规律很多,但是f(x,0)真的有规律么:2,10,218,64594...

    我没发现。。。但是我发现了最后一行:所有选法是22^n-1,因为一共2n个集合,每一个集合都可以选或不选

    但不能都不选,所以要-1。

    现在我们就能得到f(x,0)了,假如我们已知所有的f(1,0),f(2,0)....f(x-1,0),用总数减去∑i=1->xf(x,i)即∑i=1->xC(x,x-i)*f(x-i,0)

    这样是n2的,在求f(n,k)的过程中我们可以求出所有的f(m,s)值(m<=n)

    但是大多数f值都没有用,存下它们会爆内存,于是用完直接覆盖就行,保留f(x,0)即可

     1 #include<cstdio>
     2 #define int long long
     3 const long long mod=1000000007;
     4 long long fac[1000005],inv[1000005],invv[1000005],x,y,n,k,f0[1000005],f[1000005];
     5 long long pow(long long b,long long t,long long modd,long long ans=1){
     6     for(;t;t>>=1,b=b*b%modd)if(t&1)ans=ans*b%modd;
     7     return ans;
     8 }
     9 signed main(){
    10     scanf("%lld%lld",&n,&k);
    11     fac[0]=inv[0]=invv[0]=f0[0]=invv[1]=inv[1]=fac[1]=1;f0[1]=2;
    12     for(int i=2;i<=n;++i)fac[i]=fac[i-1]*i%mod,invv[i]=(-mod/i*invv[mod%i])%mod,inv[i]=inv[i-1]*invv[i]%mod;
    13     for(int i=1;i<=n;++i){
    14         f0[i]=(pow(2,pow(2,i,mod-1),mod)-1+mod)%mod;
    15         for(int j=1;j<=i;++j)f[j]=fac[i]*inv[j]%mod*inv[i-j]%mod*f0[i-j]%mod,f0[i]=(f0[i]-f[j]+mod)%mod;
    16         f[0]=f0[i];
    17     }
    18     printf("%lld
    ",(f[k]+mod)%mod);
    19 }
    愚蠢的TLE70代码

    100%算法

    在求f(n,k)的过程当中出现了太多的冗余运算量,肯定不是正解,死了~

    不知道有没有大神能优化那个我认为没救了的算法,我是抛弃它了

    然后我们还是好好看看题目吧:又是交集又是子集的,能想到什么?

    呃。。这个悬念有点无趣,因为我在题目里就已经说了。。容斥嘛。。。

    为什么刚开始想不到啊啊啊啊(笨,不解释)

    每次我都喜欢举这个例子:4个圈的venn图

    这是一张让我第二次受益匪浅的图

    现在考虑n=4,k=2

    每个圈分别叫{a,b,c,d},它们包含的部分都表示这些选法的交集含有这个元素

    那么两个圈的公共部分就表示他们的交集含有两个元素

    仔细想想每个图里要填些什么,我不方便把我的草稿纸照下来。。

    假如只被一个圈包含的颜色最浅的部分叫做1级部分,两个圈的公共部分而不被其它圈包含的叫做2级部分,以此类推

    最后可以发现,1级部分上都写着218,2级上是10,3级是2,4级是1。

    p级是f(n-p,0)。。。唉。。。看似没什么进展,还是不会求啊

    但是我们再找找别的规律:

    重新定义级数,1级表示含有1个特定元素的部分(可以不是恰好只含有它),如图中3个完整的圆都是1级部分,整个红色区域也是1个1级的

    对于每个4级部分,是1

    对于每个3级部分,是2+1=3

    对于每个2级部分,是10+2+1+2=15

    对于每个1级部分,是10+2+1+2+2+10+10+218=255

    对于每个s级部分,是2n-s-1

    我们现在要求f(4,2),尝试用这些部分把它表示出来吧

    1级部分我们貌似用不到

    2级部分用得到,一共有6个2级部分,是C(4,2),把它们加上,ans=6*15=90

    然而在加2级部分的时候我们误加了一些3级部分,把它们减去

    有几个呢?每个2级部分都包括2个3级部分,那么一共12个,ans=ans-12*3=54

    然而又一次地,我们减去过多的4级部分了,该加回来多少呢?

    在加2级部分时,每个2级部分包含1个4级部分,6个2级部分一共加了6个4级部分

    在减3级部分时,每个3级部分包含1个4级部分,12个3级部分一共减了12个4级部分

    所以一共应该加回来6个4级部分,ans=ans+6×1=60 对了!

    现在只剩下了两个问题:怎么求22^n?怎么获得一个k级部分应该被加减几次?

    对于第一个问题,显然不能直接把指数对1e9+7取模,答案不对,那应该怎么办?

    费马小定理:ap-1Ξp(mod p)  p是质数

    所以说指数是可以对p-1取模,即1e9+6

    对于第二个问题就没那么好想了,至少我想的超麻烦

    求f(n,k):显然k级部分需要被加C(n,k)次,为了方便研究(我懒得打),把所有数缩小这么多倍,最后再乘回来

    k级:1

    k+1级:-C(n-k,1)=-1(n-k)

    k+2级:-C(n-k,2)+C(n-k-1,1)*C(n-k,1)=-(n-k)(n-k-1)/2+(n-k)(n-k-1)=(1/2)*(n-k)(n-k-1)

    k+3级:-C(n-k,3)+C(n-k-1,2)*C(n-k,1)+C(n-k-2,1)*(-C(n-k,2)+C(n-k-1,1)*C(n-k,1))=(-1/6+1/2-1/2)(n-k)(n-k-1)(n-k-2)=(-1/6)(n-k)(n-k-1)(n-k-2)

    k+4级:-C(n-k,4)+...=(-1/24+1/6-1/4+1/6)(n-k)(n-k-1)(n-k-2)(n-k-3)=(1/24)(n-k)(n-k-1)(n-k-2)(n-k-3)

    红色部分的系数,是正负交替的,而其绝对值是阶乘的倒数,用逆元解决

    而后面的部分显然是一段连乘,用阶乘和阶乘的逆元解决。代码实现自行处理。

     1 #include<cstdio>
     2 const long long mod=1000000007;
     3 long long fac[1000005],inv[1000005],invv[1000005],x,y,n,k,ans,res;
     4 inline long long pow(long long b,long long t,long long modd,long long ans=1){
     5     for(;t;t>>=1,b=b*b%modd)if(t&1)ans=ans*b%modd;
     6     return ans;
     7 }
     8 inline long long c(long long b,long long t){return fac[b]*inv[t]%mod*inv[b-t]%mod;}
     9 signed main(){
    10     scanf("%lld%lld",&n,&k);
    11     fac[0]=inv[0]=invv[0]=invv[1]=inv[1]=fac[1]=1;
    12     for(int i=2;i<=n;++i)fac[i]=fac[i-1]*i%mod,invv[i]=(-mod/i*invv[mod%i])%mod,inv[i]=inv[i-1]*invv[i]%mod;res=c(n,k);
    13     for(int i=k;i<=n;++i)
    14         ans=(ans+(((k-i)&1)?-1:1)*res*inv[i-k]%mod*fac[n-k]%mod*inv[n-i]%mod*(pow(2,pow(2,n-i,mod-1),mod)-1))%mod;
    15     printf("%lld
    ",(ans+mod)%mod);
    16 }
    码量很小^_^
  • 相关阅读:
    Yii2的View中JS代码添加
    Yii2命名规则
    Yii2 Redis的使用
    win7下php5.6安装redis扩展
    Ubuntu安装cuda
    Ubuntu 安装显卡驱动
    TensorFlow 图片resize方法
    anaconda的kernel对jupyter可见
    cuda和显卡驱动版本
    jupyter修改根目录
  • 原文地址:https://www.cnblogs.com/hzoi-DeepinC/p/11102929.html
Copyright © 2020-2023  润新知