【学习笔记】数学知识-组合计数

加法原理(分类计数原理)

  • 若完成一件事的方法有 n 类,其中第 i(1in) 类方法包括 ai 种不同的方法,且这些方法互不重合,则完成这件事共有 i=1nai 种不同的方法。

乘法原理(分步计数原理)

  • 若完成一件事的步骤有 n 个,其中第 i(1in) 个步骤包括 ai 种不同的方法,且这些方法互不重合,则完成这件事共有 i=1nai 种不同的方法。

排列

  • n 个不同元素中依次取出 m 个不同的元素按照一定的顺序排成一列,产生的不同排列的数量称作排列数,记为 AnmPnm 。递推式为 Anm=Pnm=n(n1)(n2)(nm+1)=n!(nm)!
    • 相同的排列指元素相同且顺序相同。
    • m<n 时称作选排列;当 m=n 时称作全排列,由于 0!=1 ,有 Ann=n! ;当 m>nm<0 时,规定 Anm=0
    • 通常情况下使用 Anm 代替 Pnm
  • 性质
    • Anm=Anm1×(nm+1)
  • 应用
    • n 个不同元素可以重复地取出 m 个不同的元素按照一定的顺序排成一列,考虑顺序,产生的不同排列的数量为 nm ,称作相异元素的可重复选排列。
    • n 个不同元素依次取出 m 个不同的元素按照一定的顺序排成一个环形,考虑顺序,产生的不同环的数量为 Qnm=Anmm ,称作相异元素的圆排列。
      • 相同的环指元素相同且在环上的顺序相同。
      • 证明
        • m=n 时,考虑对于其中已经排好的一个环,从不同的位置断开,便可得到不同的排列,即 Qnn×n=Ann=n! ,解得 Qnn=(n1)!=Annn
        • mn 时,先从 n 个元素中选出 m 个元素,再对选出的 m 个元素进行圆排列,即 Qnm=(nm)Qmm=n!m!(nm)!×(m1)!=n!m×(nm)!=Anmm
      • 例题
    • 多重集是指包含重复元素的广义集合。设 S={n1×a1,n2×a2,,nk×ak} 是由 n1a1n2a2  nkak 组成的多重集。则 S 的全排列个数为 Ai=1knii=1knii=1kAnini=(i=1kni)!i=1kni!
  • 例题

组合

  • n 个不同元素中依次取出 m 个不同的元素组成一个集合,不考虑顺序,产生的不同集合的数量称作组合数,记为 Cnm(nm) ,递推式为 Cnm=(nm)=AnmAmm=n!m!(nm)!
    • 相同的组合指元素相同。
    • m>nm<0 时,规定 (nm)=0
  • 性质
    • pP ,则对于任意 i(1ip1) 均满足 p|(pi)

      • 证明:首先由于 1ip1 ,有 (pi)=j=pi+1pji! 为正整数,又因为 gcd(p,i!)=1 ,则 i!|j=pi+1p1j ,此时有 p|(pi)
    • (nm)=(nnm)

    • (nm)=nm(n1m1)

      • 证明
        • 将式子拆开即可,有 nm(n1m1)=(n1)!n(m1)!(nm)!m=n!m!(nm)!=(nm)
    • (nm)=(n1m)+(n1m1)

      • 证明
        • 将式子拆开即可,有 (n1m)+(n1m1)=(n1)!m!(n1m)!+(n1)!(m1)!(nm)!=(n1)!(nm)m!(nm)!+(n1)!mm!(nm)!=n!m!(nm)!=(nm)
        • 从二项式定理和杨辉三角的角度考虑,此结论是显然的。
    • (nk)(km)=(nm)(nmkm)

      • 证明
        • 将式子拆开即可。
    • i=0n(ni)=2n

      • 证明:从二项式定理的角度考虑,取 a=b=1 的情况即可。
    • i=0n(1)i(ni)=[n=0]

      • 证明:从二项式定理的角度考虑,取 a=1,b=1 的情况即可。
    • i=0n[imod2=0](ni)=i=0n[imod2=1](ni)=2n1

      • luogu P3414 SAC#1
      • 证明
        • 由于 (ni)=(n1i)+(n1i1) ,故 i=0n[imod2=0](ni)=i=0n[imod2=1](ni)=i=0n1(ni)=2n1
    • i=0k(ni)(mki)=(n+mk)

      • 证明
        • 从组合意义的角度分析,此结论是显然的。
    • i=0n(ni)(ni)=(2nn)

    • i=1n(ni+1i)=Fibn+2

    • i=1n(nii)=Fibn+1

      • 证明
        • 推式子,有 i=1n(nii)=i=1n1(n1i+1i)+(nnn)=Fibn+1+0=Fibn+1
  • 组合数的求法
    • 利用杨辉三角求解,用来解决给定 p ,多组询问,每次给定较小的 n,m ,求 (nm)modp

    • 现算阶乘和阶乘的逆元,用来解决多组询问,每次给定 n,m,p ,求 (nm)modp ,常搭配 Lucas 一起使用。

      点击查看代码
      ll C(ll n,ll m,ll p)
      {
          ll a=1,b=1,i;
          if(n>=m&&n>=0&&m>=0)
          {
              for(i=n-m+1;i<=n;i++)
              {
                  a=a*i%p;
              }
              for(i=1;i<=m;i++)
              {
                  b=b*i%p;
              }
              return a*qpow(b,p-2,p)%p;
          }
          else
          {
              return 0;
          }
      }
      
    • 预处理逆元,阶乘,阶乘的逆元,用来解决给定 p ,多组询问,每次询问给定 n,m(nm)modp

      • luogu B3717 组合数问题

        点击查看代码
        ll C(ll n,ll m,ll p)
        {
            return (n>=m&&n>=0&&m>=0)?(jc[n]*jc_inv[m]%p)*jc_inv[n-m]%p:0;
        }
        inv[1]=1;
        jc[0]=jc_inv[0]=jc[1]=jc_inv[1]=1;
        for(i=2;i<=N;i++)
        {
            inv[i]=(p-p/i)*inv[p%i]%p;
            jc[i]=jc[i-1]*i%p;
            jc_inv[i]=jc_inv[i-1]*inv[i]%p;
        }
        
    • 给定较大数 n,m ,求 (nm) ,且需要高精度时。预处理素数,将分子、分母快速分解质因数,并保存各项质因子的指数,然后将分子、分母各个质因子的指数对应相减,即把分母消去,最后把剩余质因子乘起来。

  • 应用
    • 错位排列
      • 错位排列是指没有任何元素出现在其有序位置的排列。即对于 1n 的排列 P ,如果不存在一个 i(1in) 满足 Pi=i ,则称 Pn 的错位排列。记 Dn 表示 n 个元素的错位排列个数。递推式为 Dn={0n=11n=2(n1)(Dn1+Dn2)otherwise 。特别的,当 n1 时,有 Dn=nDn1+(1)n

        • 证明
          • n=1n=2 时,显然。
          • n1n2 时,首先,设把第 n 个元素放在第 k(kn) 个位置,一共有 (n11)=n1 种不同的方法。其次,第 k 个元素放到第 n 个位置时,一共有 Dn2 种不同的方法;第 k 个元素不放到第 n 个位置时,一共有 Dn1 种不同的方法。故有 Dn=(n1)(Dn1+Dn2)
          • n1 时,设 1kn2 ,有 DnnDn1=(n1)(Dn1+Dn2)nDn1=(1)1(Dn1(n1)Dn2)=(1)2(Dn2(n2)Dn3)=(1)k(Dnk(nk)Dnk1)=(1)n2(D22D1) =(1)n ,移项得 Dn=nDn1+(1)n
      • 代码实现

        点击查看代码
        d[1]=0;
        d[2]=1;
        for(i=3;i<=n;i++)
        {
        	d[i]=(i-1)*(d[i-1]+d[i-2]);
        }
        cout<<d[n]<<endl;
        
      • 例题

        • luogu P1595 信封问题
        • UVA12024 Hats
        • luogu P3182 [HAOI2016]放棋子
          • 将障碍转化成错排挺显然的。 Dn 即为所求。

            点击查看代码
            ll d[201][200],len[201],ans[200];
            void jia(ll c[],ll &lenc,ll a[],ll &lena,ll b[],ll &lenb)
            {
                ll i,x=0;
                lenc=max(lena,lenb);
                for(i=1;i<=lenc;i++)
                {
                    c[i]=a[i]+b[i]+x;
                    if(c[i]>=1000000000000000)
                    {
                        x=c[i]/1000000000000000;
                        c[i]%=1000000000000000;
                    }
                    else
                    {
                        x=0;
                    }
                }
                lenc++;
                c[lenc]=x;
                while(lenc>0&&c[lenc]==0)
                {
                    lenc--;
                }
            }
            void cheng(ll a[],ll &lena,ll b)
            {
                ll i,x=0;
                for(i=1;i<=lena;i++)
                {
                    ans[i]=a[i]*b+x;
                    if(ans[i]>=1000000000000000)
                    {
                        x=ans[i]/1000000000000000;
                        ans[i]%=1000000000000000;
                    }
                    else
                    {
                        x=0;
                    }
                }
                lena++;
                ans[lena]=x;
                while(lena>0&&ans[lena]==0)
                {
                    lena--;
                }
                if(lena==0)
                {
                    lena++;
                    ans[lena]=1;
                }
                for(i=1;i<=lena;i++)
                {
                    a[i]=ans[i];
                }
            }
            int main()
            {
                ll n,i;
                cin>>n;
                len[1]=len[2]=1;
                d[1][1]=0;
                d[2][1]=1;
                for(i=3;i<=n;i++)
                {
                    jia(d[i],len[i],d[i-1],len[i-1],d[i-2],len[i-2]);
                    cheng(d[i],len[i],i-1);
                }
                cout<<d[n][len[n]];
                for(i=len[n]-1;i>=1;i--)
                {
                    printf("%015lld",d[n][i]);
                }   
                return 0;
            }
            
        • luogu P4071 [SDOI2016] 排列计数
          • (mn)Dnm 即为所求。

            • 特别地,本题要求 D0=1
            点击查看代码
            ll inv[1000001],jc[1000001],jc_inv[1000001],d[1000001];
            ll C(ll n,ll m,ll p)
            {
                return (n>=m&&n>=0&&m>=0)?((jc[n]*jc_inv[m]%p)*jc_inv[n-m]%p):0;
            }
            int main()
            {
                ll t,n,m,p=1000000007,i;
                scanf("%lld",&t);
                inv[1]=1;
                jc[0]=jc_inv[0]=jc[1]=jc_inv[1]=1;
                d[0]=1;
                d[1]=0;
                d[2]=1;
                for(i=3;i<=1000000;i++)
                {
                    d[i]=((i-1)%p)*((d[i-1]+d[i-2])%p)%p;
                }
                for(i=2;i<=1000000;i++)
                {
                    inv[i]=(p-p/i)*inv[p%i]%p;
                    jc[i]=jc[i-1]*i%p;
                    jc_inv[i]=jc_inv[i-1]*inv[i]%p;
                }
                for(i=1;i<=t;i++)
                {
                    scanf("%lld%lld",&n,&m);
                    printf("%lld\n",C(n,m,p)*d[n-m]%p);
                }
                return 0;
            }
            
        • CF340E Iahub and Permutations
    • S={n1×a1,n2×a2,,nk×ak} 是由 n1a1n2a2  nkak 组成的多重集。设整数 mmini=1k{ni} ,则从 S 中选出 m 个元素组成一个多重集(不考虑元素顺序),产生的不同多重集的数量为 (k+m1k1)
    • 卡特兰数
  • 例题
    • SP28304 ADATEAMS - Ada and Teams
    • luogu P1771 方程的解
      • i=1nxi=m 的正整数解的数量为 (m1n1)

        • 等价于求把 m 个不同的小球分成 n 个部分,使得每个部分至少有一个小球的方案数。
      • i=1nxi=m 的非负整数解的数量为 (n+m1n1)

        • 等价于求 i=1nxi=n+m 的正整数解的数量。
        点击查看代码
        ll num[1001],a[1001],ans[1001],lena=1;
        void cheng(ll a[],ll &lena,ll b)
        {
            ll i,x=0;
            for(i=1;i<=lena;i++)
            {  
                ans[i]=a[i]*b+x;
                if(ans[i]>=1000000000000000)
                {
                    x=ans[i]/1000000000000000;
                    ans[i]%=1000000000000000;
                }
                else
                {
                    x=0;
                }
            }
            lena++;
            ans[lena]=x;
            while(lena>0&&ans[lena]==0)
            {
                lena--;
            }
            if(lena==0)
            {
                lena++;
                ans[lena]=1;
            }
            for(i=1;i<=lena;i++)
            {
                a[i]=ans[i];
            }
        }
        void divide(ll n,ll pd)
        {
            ll i,sum=0;
            for(i=2;i<=sqrt(n);i++)
            {
                if(n%i==0)
                {
                    sum=0;
                    while(n%i==0)
                    {
                        n/=i;
                        sum++;
                    }
                    num[i]+=pd*sum;
                }
            }
            if(n>1)
            {
                num[n]+=pd;
            }
        }
        ll qpow(ll a,ll b,ll p)
        {
            ll ans=1;
            while(b>0)
            {
                if(b&1)
                {
                    ans=ans*a%p;
                }
                b>>=1;
                a=a*a%p;
            }
            return ans;
        }
        void C(ll n,ll m)
        {
            ll i,j;
            if(n>=m)
            {
                for(i=n-m+1;i<=n;i++)
                {
                    divide(i,1);
                }
                for(i=1;i<=m;i++)
                {
                    divide(i,-1);
                }
                for(i=1;i<=n;i++)
                {
                    if(num[i]!=0)
                    {
                        for(j=1;j<=num[i];j++)
                        {
                            cheng(a,lena,i);
                        }
                    }
                }
            }
        }
        int main()
        {
            ll k,x,p=1000,i;
            cin>>k>>x;
            a[1]=1;
            C(qpow(x,x,p)-1,k-1);
            cout<<a[lena];
            for(i=lena-1;i>=1;i--)
            {
                printf("%015lld",a[i]);
            }   
            return 0;
        }
        
    • luogu P1350 车的放置
      • 将棋盘分割为 a×(b+d)c×d 的矩阵,因为 a×(b+d)c×d 的矩阵放置的车有冲突情况,故 i=0n(ai)(b+d(ki)i)Aii(cki)(dki)Akiki 即为所求。
    • luogu P2606 [ZJOI2010] 排列计数
      • 对于常见的计数 DP ,记录是从哪个元素转移来的会耗费大量的空间,且无法确定是否合法。因此就需要利用离散化的思想来实现“等效代换”,记录其相对的大小关系。

      • 容易发现满足性质的序列是一个小根堆。

      • si 表示以 i 为根的小根堆的大小, fi 表示以 i 为根的小根堆的合法方案数。

      • 因为要求自己是最小的,故需要在 si1 个中选出 s2i 个给左儿子,剩下的 s2i+1 个给有右儿子。

      • 故得到转移方程 si=s2i+s2i+1+1,fi=(s2isi1)f2if2i+1 。最后, f1 即为所求。

        • 此时该小根堆内记录的不是实际元素,而是其之间的大小关系。
          • 这里如果不理解,可以自己带几组样例模拟一下。
        点击查看代码
        ll inv[3000001],jc[3000001],jc_inv[3000001],f[3000001],s[3000001];
        ll C(ll n,ll m,ll p)
        {
            if(n>=m&&n>=0&&m>=0)
            {
                return (jc[n]*jc_inv[m]%p)*jc_inv[n-m]%p;
            }
            else
            {
                return 0;
            }
        }
        ll lucas(ll n,ll m,ll p)
        {
            return m?C(n%p,m%p,p)*lucas(n/p,m/p,p)%p:1;
        }
        int main()
        {
            ll n,p,i;
            cin>>n>>p;
            inv[1]=1;
            jc[0]=jc_inv[0]=jc[1]=jc_inv[1]=1;
            for(i=2;i<=min(n,p-1);i++)
            {
                inv[i]=(p-p/i)*inv[p%i]%p;
                jc[i]=jc[i-1]*i%p;
                jc_inv[i]=jc_inv[i-1]*inv[i]%p;
            }
            for(i=1;i<=2*n+1;i++)
            {
                f[i]=1;
            }
            for(i=n;i>=1;i--)
            {
                s[i]=s[i*2]+s[i*2+1]+1;
                f[i]=(lucas(s[i]-1,s[i*2],p)*f[i*2]%p)*f[i*2+1]%p;
            }
            cout<<f[1]<<endl;
            return 0;
        }
        
    • luogu P6191 [USACO09FEB] Bulls And Cows S | HZOJ 720.种树
    • LibreOJ 10235. 「一本通 6.6 练习 6」序列统计
      • m=rl+1 ,设 0kn1 ,枚举长度 i ,等价于求 j=1mxj=i 的非负整数解的数量。

      • 推式子,有 i=1n(m+i1i)=(m0)1+i=1n(m+i1i)=(m1)+(m0)1+i=2n(m+i1i)=(m+11)1+i=2n(m+i1i)=(m+kk)1+i=k+1n(m+i1i)=(m+n1n1)+(m+n1n)1=(n+mn)1

        点击查看代码
        ll inv[1000010],jc[1000010],jc_inv[1000010];
        ll C(ll n,ll m,ll p)
        {
            if(n>=m&&n>=0&&m>=0)
            {
                return (jc[n]*jc_inv[m]%p)*jc_inv[n-m]%p;
            }
            else
            {
                return 0;
            }
        }
        ll lucas(ll n,ll m,ll p)
        {
            return m?C(n%p,m%p,p)*lucas(n/p,m/p,p)%p:1;
        }
        int main()
        {
            ll t,n,m,l,r,i,p=1000003;
            cin>>t;
            inv[1]=1;
            jc[0]=jc_inv[0]=jc[1]=jc_inv[1]=1;
            for(i=2;i<=p-1;i++)
            {
                inv[i]=(p-p/i)*inv[p%i]%p;
                jc[i]=jc[i-1]*i%p;
                jc_inv[i]=jc_inv[i-1]*inv[i]%p;
            }
            for(i=1;i<=t;i++)
            {
                cin>>n>>l>>r;
                m=r-l+1;
                cout<<(lucas(n+m,n,p)+p-1)%p<<endl;
            }
            return 0;
        }
        
    • BZOJ3462 DZY Loves Math II
    • luogu P2154 [SDOI2009] 虔诚的墓主人
      • 观察到 n,m 极大,但 w 较小,于是考虑进行离散化。

      • 对于横坐标相等, yiyj 的两棵常青树 i,j ,其相隔的 yjyi 个墓地对答案产生的贡献为 (upxik)(downxik)h=yi+1yj1(lifthk)(righthk) ,其中 upl,downl,leftl,rightl 分别表示 (x,l) 上、下、左、右方常青树的个数。

      • (upxik)(downxik) 在枚举常青树的过程中即可处理。

      • 考虑用单点修改、区间查询的树状数组维护 h=yi+1yj1(lifthk)(righthk) 。注意枚举到一棵新的常青树时,要先减去其原来对答案的贡献,重新计算贡献。

      • 细节较多。

        点击查看代码
        struct node
        {
            ll x,y;
        }a[100010];
        ll x[100010],y[100010],c[100010][11],c2[100010][2],l[100010],sum[100010][2];
        bool cmp(node a,node b)
        {
            return (a.x==b.x)?(a.y<b.y):(a.x<b.x);
        }
        ll query(ll a[],ll x)
        {
            return lower_bound(a+1,a+1+a[0],x)-a;
        }
        ll lowbit(ll x)
        {
            return (x&(-x));
        }
        void add(ll n,ll x,ll key,ll p)
        {
            ll i;
            for(i=x;i<=n;i+=lowbit(i))
            {
                c2[i][0]=(c2[i][0]+key+p)%p;
            }
        }
        ll getsum(ll x,ll p)
        {
            ll ans=0,i;
            for(i=x;i>0;i-=lowbit(i))
            {
                ans=(ans+c2[i][0])%p;
            }
            return ans;
        }
        void C(ll n,ll m,ll p)
        {
            c[0][0]=c[1][0]=c[1][1]=1;
            for(ll i=2;i<=n;i++)
            {
                c[i][0]=1;
                for(ll j=1;j<=m;j++)
                {
                    c[i][j]=(c[i-1][j-1]+c[i-1][j])%p;
                }
            }
        }
        int main()
        {
            ll n,m,w,k,u,d=0,r,lr,i,ans=0,p=2147483648;
            cin>>n>>m>>w;
            a[0].x=a[0].y=0;
            for(i=1;i<=w;i++)
            {
                cin>>a[i].x>>a[i].y;
                a[i].x++;
                a[i].y++;
                x[i]=a[i].x;
                y[i]=a[i].y;
            }
            cin>>k;
            sort(x+1,x+1+w);
            sort(y+1,y+1+w);
            x[0]=unique(x+1,x+1+w)-(x+1);
            y[0]=unique(y+1,y+1+w)-(y+1);
            C(w,k,p);
            for(i=1;i<=w;i++)
            {
                a[i].x=query(x,a[i].x);
                a[i].y=query(y,a[i].y);
                sum[a[i].x][0]++;
                sum[a[i].y][1]++;
            }
            sort(a+1,a+1+w,cmp);
            for(i=1;i<=w-1;i++)
            {
                d=((a[i-1].x==a[i].x)?d:0)+1;
                u=sum[a[i].x][0]-d;
                if(d>=k&&u>=k&&a[i].x==a[i+1].x)
                {
                    lr=(a[i+1].y-1>=a[i].y+1)?(getsum(a[i+1].y-1,p)-getsum(a[i].y+1-1,p)+p)%p:0;
                    ans=(ans+(((c[d][k]*c[u][k])%p)*lr)%p)%p;
                }
                add(y[0],a[i].y,-c2[a[i].y][1],p);
                l[a[i].y]++;
                r=sum[a[i].y][1]-l[a[i].y];
                c2[a[i].y][1]=(l[a[i].y]>=k&&r>=k)?(c[l[a[i].y]][k]*c[r][k])%p:0;
                add(y[0],a[i].y,c2[a[i].y][1],p);
            }
            cout<<ans<<endl;
            return 0;
        }
        
    • luogu P2467 [SDOI2010] 地精部落
      • fi 表示长度为 i 的山脉且第一个位置是山谷的合法方案数, gi 表示长度为 i 的山脉且第一个位置是山峰的合法方案数。

        • 此时该山脉内记录的不是实际元素,而是其之间的大小关系。
      • 容易有 fifi1 转移过来的过程中,将 i 加入其中,一定是放在山峰的位置,故枚举插入的位置 j(jmod2=0) ,从 i1 个里面选出 j1 个放在插入的 i 的左边,剩下的 (i1)(j1) 个放在插入的 i 的右边。 gi 同理。

      • 故得到转移方程 {fi=j=1i[jmod2=0](i1j1)fj1f(i1)(j1)gi=j=1i[jmod2=1](i1j1)gj1f(i1)(j1) 。最终,有 fn+gn 即为所求。

        点击查看代码
        ll c[2][4400],f[4400],g[4400];
        int main()
        {
            ll n,p,i,j;
            cin>>n>>p;
            c[0][0]=c[1][0]=c[1][1]=1;
            f[0]=g[0]=f[1]=g[1]=1;
            for(i=2;i<=n;i++)
            {
                for(j=1;j<=i;j++) 
                {
                    c[i%2][0]=1;
                    if(j%2==0)
                    {
                        f[i]=(f[i]+(((c[(i-1)%2][j-1]*f[j-1])%p)*f[(i-1)-(j-1)])%p)%p;
                    }
                    else
                    {
                        g[i]=(g[i]+(((c[(i-1)%2][j-1]*g[j-1])%p)*f[(i-1)-(j-1)])%p)%p;
                    }
                    c[i%2][j]=(c[(i-1)%2][j-1]+c[(i-1)%2][j])%p;
                }
            }
            cout<<(f[n]+g[n])%p<<endl;
            return 0;
        }
        
    • luogu P4910 帕秋莉的手环
    • luogu P3214 [HNOI2011] 卡农

二项式定理、二项式反演

  • 二项式定理
    • 二项式定理: (a+b)n=i=0n(ni)aibni
    • 证明
      • 数学归纳法
        • n=1 时, (a+b)1=(10)a0b1+(11)a1b0=a+b 成立。
        • 假设当 n=m 时命题成立,当 n=m+1 时,有 (a+b)m+1=(a+b)(a+b)m=(a+b)i=0m(mi)aibmi=i=0m(mi)ai+1bmi+i=0m(mi)aibmi+1=i=1m+1(mi1)aibmi+1+i=0m(mi)aibmi+1=(mm)am+1b0+(0m)a0bm+1+i=1m((mi1)+(mi))aibm+1i=(m+1m+1)am+1b0+(0m+1)a0bm+1+i=1m(m+1i)aibm+1i=i=0m+1(m+1i)aibm+1i
        • 证毕。
      • 组合意义
        • (a+b)n 展开时可以理解为从 n 个式子 a+b 中的 i(0in) 个里取 a ,剩下的 ni 个式子 a+b 里取 b ,组成一个乘积项 aibni ,其系数为 (ni)
    • 例题
  • 二项式反演
    • fn 表示恰好 n 个元素组成特定结构的方案数, gn 表示从 n 个元素中选出 i(i0) 个元素组成特定结构(虽然其他元素可能一并组成特定结构,但我们不去管它)的总方案数,即 gn=i=0n(ni)fi ,那么有 fn=i=0n(1)ni(ni)gi
    • 证明
      • 推式子,有 fn=i=0n(1)ni(ni)gi=i=0n(1)ni(ni)(j=0i(ij)fj)=i=0nj=0i(ni)(ij)(1)nifj=j=0ni=jn(ni)(ij)(1)nifj=j=0nfj(i=jn(ni)(ij)(1)ni)=j=0nfj(i=jn(nj)(njj)(1)ni)=j=0n(nj)fj(i=jn(njj)(1)ni)=j=0n(nj)fj(k=0nj(njk)(1)n(j+k))=j=0n(nj)fj(k=0nj(njk)(1)njk)=j=0n(nj)fj[nj=0]=j=0n(nj)fj[n=j]=fn
    • 其他形式
      • fi 表示 n 个元素中恰好有 i 个元素满足特定条件的方案数, gi 表示 n 个元素中至少有 i 个元素满足特定条件的方案数,即 gm=i=mn(im)fi ,那么有 fm=i=mn(1)im(im)gi
    • 例题

卢卡斯定理

  • 卢卡斯定理:若 p 是质数,则对于任意正整数 n,m ,有 n=(nknk1n1)p,m=(mkmk1m1)p ,其中对于每一个 i(1ik) 均满足 0ni,mip1,则 (nm)i=1k(nimi)(modp)

    • 另一种写法为 (nm)(nmodpmmodp)(npmp)(modp) .
  • 证明

    • 考虑对于多项式 (1+x)n ,展开后 xm 的系数为 (nm)
    • 此时有 (1+x)n=i=1k(1+x)pi1×ni(modp)=i=1k((1+x)pi1)ni(modp)=i=1k(j=0pi1(nj)xj)ni(modp)i=1k(1+xpi1)ni(modp)=i=1kj=0ni(nij)xpi1j(modp)
    • p 进制数的性质,将同余右边展开后有 xm 的系数为 i=1k(nimi)
    • 证毕。
  • 性质

    • (nm) 为奇数当且仅当在二进制表示下 n 的每一个数位的数都不小于 m 的相应数位的数,即 n&m=m
  • 代码实现

    点击查看代码
    ll lucas(ll n,ll m,ll p)
    {
        return (n>=m&&n>=0&&m>=0)?(m?C(n%p,m%p,p)*lucas(n/p,m/p,p)%p:1):0;
    }
    
  • 例题

扩展卢卡斯定理

容斥原理

  • 容斥原理:设 S={S1,S2,S3,,Sn} ,则有 |i=1nSi|=TS(1)|T|+1×|i=1|T|Ti|
  • 应用
    • S={n1×a1,n2×a2,,nk×ak} 是由 n1a1n2a2  nkak 组成的多重集。设整数 mi=1kni ,则从 S 中选出 m 个元素组成一个多重集(不考虑元素顺序),产生的不同多重集的数量为 (k+m1k1)T{n1,n2,,nk},|T|1(1)|T|+1×(k+m1i=1|T|(Ti+1)k1) ,合并得 T{n1,n2,,nk}(1)|T|×(k+m1i=1|T|(Ti+1)k1)
      • 证明
        • 等价于求 i=1kxi=m(xini) 的不同方案数。
        • 正难则反,考虑合法方案数等于总方案数减不合法方案数。
        • 总方案数同 luogu P1771 方程的解 ,即 (k+m1k1)
        • 难点在于如何求不合法方案数。设 Si(1ik) 表示至少包含 ni+1ai 的多重集,则 Si 的方案数为 (k+m1(ni+1)k1) 。接着容斥原理,得出 |i=1kSi|=T{n1,n2,,nk},|T|1(1)|T|+1×|i=1|T|Ti| ,其方案数为 T{n1,n2,,nk},|T|1(1)|T|+1×(k+m1i=1|T|(Ti+1)k1)
        • 二者相减即可得到原式。
  • 例题
posted @   hzoi_Shadow  阅读(111)  评论(5编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
扩大
缩小
点击右上角即可分享
微信分享提示