Typesetting math: 100%

组合数学笔记-排列与组合

排列与组合

排列

排列的定义与基本性质

定义 设一个集合 SS 中有 nn 个元素,从中有序地取出 m(0mn)m(0mn) 个元素排成一列, 称为 SS 的一个 mm 排列。两个排列相同,当且仅当元素相同且顺序相同。我们记 PmnPmnAmnAmnP(n,m)P(n,m) 表示 SSmm 排列的总数。

约定m>nm>n 时,Pmn=0Pmn=0

全排列的定义 设一个集合 SS 中有 nn 个元素,其 nn 排列称为全排列。

  • C++中,next_permutation 函数可以按字典序从小到大遍历数据的全排列,prev_permutation 函数与之相反。

性质1 Pmn=n(n1)(n2)(nm+1)=n!(nm)!Pmn=n(n1)(n2)(nm+1)=n!(nm)! ,其中 0mn0mn

性质2 Pmn=nPm1n1Pmn=nPm1n1

性质3 Pmn=mPm1n1+Pmn1Pmn=mPm1n1+Pmn1

性质1的证明:

考虑乘法原理,按顺序选数,第 i(1im)i(1im) 个数有 ni+1ni+1 种选法,乘在一起可得原式。

性质2的证明:

考虑乘法原理,第一个数有 nn 种选法,再从剩下的 n1n1 个数里选 m1m1 个有 Pm1n1Pm1n1 种排列,所以 Pmn=nPm1n1Pmn=nPm1n1

性质3的证明:

考虑加法原理,考虑分两类:

  1. 先指定选一个元素,有 mm 个位置可放,再从剩下 n1n1 元素中选 m1m1 个有 Pm1n1Pm1n1 种排列。
  2. 不选1指定的元素,从其他 n1n1 元素里选 mm 个,共 Pm1nPm1n 种排列。

因此 Pmn=mPm1n1+Pmn1Pmn=mPm1n1+Pmn1

错位排列

错位排列的定义与基本性质

定义P=(p1,p2,,pn)P=(p1,p2,,pn)S={1,2,,n}S={1,2,,n} 的一个排列,若对于任意的 i[1,n]i[1,n] 满足 piipii ,则称 PPSS 的一个错位排列。我们记 DnDn 表示 SS 的错位排列的总数。

性质1 DnDn 满足

Dn={1,n=00,n=11,n=2(n1)(Dn1+Dn2),n3Dn=⎪ ⎪ ⎪⎪ ⎪ ⎪1,n=00,n=11,n=2(n1)(Dn1+Dn2),n3

性质2 Dn=n!nk=0(1)kk!Dn=n!nk=0(1)kk!

性质3 Dn=n!e+12Dn=n!e+12

性质4 {1,2,,n}{1,2,,n} 的排列是错位排列的概率 PnPn 有渐进 limnPn=limnDnn!=1elimnPn=limnDnn!=1e ,表明 DnDn 的增长率与 n!n! 仅相差常数倍。

性质1的证明:

考虑加法原理,设 nn 出现在位置 k(1kn1)k(1kn1) ,有两种情况:

  1. kk 一定出现在位置 nn ,那么除去 k,nk,n 剩下 n2n2 个数错排即可,共计 Dn2Dn2
  2. kk 不能出现在位置 nn ,那么可把位置 nn 视作 kk 的新位置与除了 nn 的数进行错排,共计 Dn1Dn1

因此再根据乘法原理,有 Dn=(n1)(Dn1+Dn2)Dn=(n1)(Dn1+Dn2)

性质2的证明:

考虑减法原理,设全集 UU[1,n][1,n] 的全排列,则 |U|=n!|U|=n! ,设满足 piipii 的排列的集合为 SiSi ,那么满足 pi=ipi=i 的集合为 ¯Si¯¯¯¯¯Si ,那么有 |ni=1Si|=|U||ni=1¯Si|∣ ∣ni=1Si∣ ∣=|U|∣ ∣ni=1¯¯¯¯¯Si∣ ∣ ,问题转换为求 |ni=1¯Si|∣ ∣ni=1¯¯¯¯¯Si∣ ∣

考虑容斥原理,有 |ni=1Si|=nk=1(1)k11i1<i2<<ikn|kj=1Sij|∣ ∣ni=1Si∣ ∣=nk=1(1)k11i1<i2<<ikn∣ ∣kj=1Sij∣ ∣ ,其中 |kj=1Sij|∣ ∣kj=1Sij∣ ∣ 为所有 iTiT 满足 pi=ipi=i 的排列数,显然为 (nk)!(nk)! ,对于 1i1<i2<<ik<n1i1<i2<<ik<n 共有 (nk)(nk) 种方案,因此 |ni=1Si|=nk=1(1)k1(nk)(nk)!=nk=1(1)k1n!k!∣ ∣ni=1Si∣ ∣=nk=1(1)k1(nk)(nk)!=nk=1(1)k1n!k!

最后,错位排列数 |ni=1Si|=|U||ni=1¯Si|=n!nk=1(1)k1n!k!=n!nk=0(1)kk!∣ ∣ni=1Si∣ ∣=|U|∣ ∣ni=1¯¯¯¯¯Si∣ ∣=n!nk=1(1)k1n!k!=n!nk=0(1)kk!

圆排列

圆排列的定义与基本性质

定义 设一个集合 SS 中有 nn 个元素,从中有序地取出 m(0mn)m(0mn) 个元素排成不分首尾围成一圈, 称为 SS 的一个 mm 圆排列。两个圆排列相同,当且仅当元素相同且不分首位地顺序相同。我们记 QmnQmn 表示 SSmm 圆排列的总数。

性质1 Qmn=PmnmQmn=Pmnm

性质1的证明:

对于每一种圆排列,我们可以规定首尾使其变成标准排列,共有 mm 种首尾方案,因此有 Qmnm=PmnQmnm=Pmn ,所以 Qmn=PmnmQmn=Pmnm

多重集排列

多重集排列的定义与基本性质

定义 设多重集 S={n1a1,n2a2,,nkak}S={n1a1,n2a2,,nkak} ,从中任选 mm 个元素组成排列,称为 SSmm 排列。

多重组合数的定义 设多重集 S={n1a1,n2a2,,nkak}S={n1a1,n2a2,,nkak}n=ki=1nin=ki=1ni ,则 SSnn 排列(即全排列)的总数称为多重组合数,记为 (nn1,n2,,nk)(nn1,n2,,nk)

性质1 设多重集 S={n1a1,n2a2,,nkak}S={n1a1,n2a2,,nkak}n=ki=1nin=ki=1ni ,则全排列数为 (nn1,n2,,nk)=n!ki=1ni!(nn1,n2,,nk)=n!ki=1ni!

性质2 设多重集 S={n1a1,n2a2,,nkak}S={n1a1,n2a2,,nkak} ,若 mm 满足 mni+(1ik)mni+(1ik) ,则 mm 排列数为 kmkm

性质1的证明:

不考虑元素重复的全排列为 n!n! ,再对每个元素的全排列 ni!(1ik)ni!(1ik) 去重,所以 (nn1,n2,,nk)=n!ki=1ni!(nn1,n2,,nk)=n!ki=1ni!

性质2的证明:

因为选 mm 个小于等于任意元素的个数,所以每次都能选 kk 个元素,因此总数为 kmkm

组合

组合的定义与基本性质

定义 设一个集合 SS 中有 nn 个元素,从中无序地取出 m(0mn)m(0mn) 个元素组成集合, 称为 SS 的一个 mm 组合。两个组合相同,当且仅当元素相同。我们记 CmnCmn(nm)(nm)C(n,m)C(n,m) 表示 SSmm 组合的总数。

约定m>nm>n 时,(nm)=0(nm)=0

性质1 (nm)=Pmnm!=n!m!(nm)!(nm)=Pmnm!=n!m!(nm)!

性质2 (nm)=(nnm)(nm)=(nnm)

性质3 (nm)=nm+1m(nm1)(nm)=nm+1m(nm1)

性质4 (nm)=nm(n1m1)(nm)=nm(n1m1)

性质5(杨辉三角) (nm)=(n1m)+(n1m1)(nm)=(n1m)+(n1m1)

性质6 ni=0(im)=(n+1m+1)ni=0(im)=(n+1m+1)

性质7 (nm)(mk)=(nk)(nkmk)(nm)(mk)=(nk)(nkmk)

性质8 ni=0(nii)=Fn+1ni=0(nii)=Fn+1 ,其中 FF 是斐波那契数列。

性质1的证明:

先考虑顺序得到的总数为 mm 排列数 PmnPmn ,而对于一种 mm 组合有 m!m! 种排列方式,所以 m!(nm)=Pmnm!(nm)=Pmn ,因此根据排列性质1可得 (nm)=Pmnm!=n!m!(nm)!(nm)=Pmnm!=n!m!(nm)!

性质2的证明:

方法1:

由性质1易得。

方法2:

nn 个里选 mm 个组合,等价于 nn 个里选 nmnm 不在组合。

性质5的证明:

方法1:

由性质1易得。

方法2:

画出杨辉三角,由数学归纳法易得。

方法3:

指定一个元素,如果一定不选它则有 (n1m)(n1m) 种组合,如果一定选它则有 (n1m1)(n1m1) 种组合,考虑加法原理,得证。

性质6的证明:

方法1:

根据性质5可得

ni=0(im)=ni=m(im)=(m+1m+1)+(m+1m)+(m+2m)++(nm)=(m+2m+1)+(m+2m)++(nm)=(m+3m+1)++(nm)=(n+1m+1)ni=0(im)=ni=m(im)=(m+1m+1)+(m+1m)+(m+2m)++(nm)=(m+2m+1)+(m+2m)++(nm)=(m+3m+1)++(nm)=(n+1m+1)

方法2:

[0,n][0,n] 的整数中选出 m+1m+1 个数的组合数为 (n+1m+1)(n+1m+1) ,在这些组合中最大数为 i(0in)i(0in) 的组合数为 (im)(im) ,考虑加法原理得证。

性质7的证明:

方法1:

由性质1可得

(nm)(mk)=n!m!(nm)!m!k!(mk)!=n!k!1(mk)!(nm)!=n!k!(nk)!(nk)!(mk)!(nm)!=(nk)(nkmk)(nm)(mk)=n!m!(nm)!m!k!(mk)!=n!k!1(mk)!(nm)!=n!k!(nk)!(nk)!(mk)!(nm)!=(nk)(nkmk)

方法2:

左式是正着选,从 nn 个元素中选 mm 个,对于每个 mm 组合再选 kk 个的 kk 组合的总数的总和。

右式是倒着选,先从 nn 个元素中选 kk 个,对于每个 kk 组合从剩下 nknk 个元素中再选 mkmk 个元素,代表这个 kk 组合会被所有 mm 组合选到的次数,最后总和与左式意义等价。

性质8的证明:

Gn=ni=0(nii)Gn=ni=0(nii) ,则 G0=1=F1,G1=1=F2G0=1=F1,G1=1=F2

假设当 n=k+1(k0)n=k+1(k0) 时, Gk=Fk+1,Gk+1=Fk+2Gk=Fk+1,Gk+1=Fk+2 成立。

那么当 n=k+2n=k+2 时,由性质5可得

Gk+Gk+1=ki=0(kii)+k+1i=0(k+1ii)=k+1i=1(ki+1i1)+k+1i=1(k+1ii)+1=k+1i=1((k+1ii1)+(k+1ii))+1=k+1i=1(k+2ii)+(k+20)+(0k+2)=k+2i=0(k+2ii)=Gk+2Gk+Gk+1=ki=0(kii)+k+1i=0(k+1ii)=k+1i=1(ki+1i1)+k+1i=1(k+1ii)+1=k+1i=1((k+1ii1)+(k+1ii))+1=k+1i=1(k+2ii)+(k+20)+(0k+2)=k+2i=0(k+2ii)=Gk+2

同时有 Gk+Gk+1=Fk+1+Fk+2=Fk+3Gk+Gk+1=Fk+1+Fk+2=Fk+3 ,因此 Gk+1=Fk+2,Gk+2=Fk+3Gk+1=Fk+2,Gk+2=Fk+3 ,得证。

二项式定理

定理1(二项式定理) (x+y)n=ni=0(ni)xniyi(x+y)n=ni=0(ni)xniyi

  • 推论1(定理1的推论) ni=0(ni)=2nni=0(ni)=2n

  • 推论2(定理1的推论) ni=0(1)i(ni)=[n=0]ni=0(1)i(ni)=[n=0]

  • 推论3(推论2的推论) (n0)+(n2)+=(n1)+(n3)+=2n1(n0)+(n2)+=(n1)+(n3)+=2n1 ,其中 n1n1

定理2(扩展二项式定理) (ki=1xi)n=n1+n2++nk=n(nn1,n2,,nk)ki=1xnii(ki=1xi)n=n1+n2++nk=n(nn1,n2,,nk)ki=1xnii

广义组合数的定义αR,mNαR,mN ,则 (αm)=mi=0(αi)m!(αm)=mi=0(αi)m!

定理3(广义二项式定理) (x+y)α=i=0(αi)xαiyi(x+y)α=i=0(αi)xαiyi ,其中 αRαR

定理1的证明:

方法1:

n=0n=0 时显然得证。

假设当 n=kn=k 时, (x+y)k=ki=0(ki)xkiyi(x+y)k=ki=0(ki)xkiyi 成立。

n=k+1n=k+1 时,根据组合基本性质5有

(x+y)k+1=(x+y)ki=0(ki)xkiyi=xki=0(ki)xkiyi+yki=0(ki)xkiyi=ki=0(ki)xk+1iyi+k+1i=1(ki1)xki+1yi=(k0)xk+1+ki=1(ki)xk+1iyi+ki=1(ki1)xki+1yi+(kk)yk+1=(k0)xk+1+ki=1(k+1i)xk+1iyi+(k+1k+1)yk+1=k+1i=0(k+1i)xk+1iyi(x+y)k+1=(x+y)ki=0(ki)xkiyi=xki=0(ki)xkiyi+yki=0(ki)xkiyi=ki=0(ki)xk+1iyi+k+1i=1(ki1)xki+1yi=(k0)xk+1+ki=1(ki)xk+1iyi+ki=1(ki1)xki+1yi+(kk)yk+1=(k0)xk+1+ki=1(k+1i)xk+1iyi+(k+1k+1)yk+1=k+1i=0(k+1i)xk+1iyi

于是得证。

方法2:

我们枚举 x,yx,y 的指数 i,ji,j 满足 i+j=ni+j=n 的情况,等价于枚举 i(0in)i(0in) 得到 j=nij=ni 。对于一个 ii 的情况,需要求在 nn(x+y)(x+y) 括号中有 ii 个选 xxnini 个选 yy 的方案数。

我们可以 nniinininini(ni)(nini)=(ni)(ni)(nini)=(ni) 种方案。

当然,我们也可以理解为一个多重集的模型,有 ii 个选 xx(x+y)(x+y)nini 个选 yy(x+y)(x+y) ,选择相同的 (x+y)(x+y) 算相同的元素,不同的顺序算不同的方案,所以是求有限多重集的全排列数,为 (ni,ni)=(ni)(ni,ni)=(ni) 种方案。

因此 (x+y)n=ni=0(ni)xniyi(x+y)n=ni=0(ni)xniyi

定理2的证明:

同样可以使用归纳法,但比较复杂,我们使用定理1的证法2,即组合意义证明。

我们枚举 xi(1ik)xi(1ik) 的指数 nini 满足 n1+n2++nk=nn1+n2++nk=n 的情况。对于一组非负整数解 n1,n2,,nkn1,n2,,nk ,需要求在 nn(x1+x2++xk)(x1+x2++xk) 括号中有 ni(1ik)ni(1ik) 个括号内选了 xixi 的方案数。

我们设多重集有 ni(1ik)ni(1ik) 个选 xixi(x1+x2++xk)(x1+x2++xk) , 其全排列数为 (nn1,n2,,nk)(nn1,n2,,nk)

因此 (ki=1xi)n=n1+n2++nk=n(nn1,n2,,nk)ki=1xnii(ki=1xi)n=n1+n2++nk=n(nn1,n2,,nk)ki=1xnii

范德蒙德卷积

定理1(范德蒙德卷积) ki=0(ni)(mki)=(n+mk)ki=0(ni)(mki)=(n+mk)

  • 推论1(定理1的推论) ri=s(ni+s)(mri)=(n+ms+r)ri=s(ni+s)(mri)=(n+ms+r)
  • 推论2(定理1的推论) ni=0(ni)(ni1)=(2nn1)ni=0(ni)(ni1)=(2nn1)
  • 推论3(定理1的推论) ni=0(ni)2=(2nn)ni=0(ni)2=(2nn)
  • 推论4(定理1的推论) mi=0(ni)(mi)=(n+mm)mi=0(ni)(mi)=(n+mm)

定理1的证明:

方法1:

由二项式定理的定理1可得

n+mk=0(n+mk)xk=(1+x)n+m=(1+x)n(1+x)m=(ni=0(ni)xi)(mj=0(mj)xj)=ni=0mj=0(ni)(mj)xi+j=n+mk=0ki=0(ni)(mki)xkn+mk=0(n+mk)xk=(1+x)n+m=(1+x)n(1+x)m=(ni=0(ni)xi)(mj=0(mj)xj)=ni=0mj=0(ni)(mj)xi+j=n+mk=0ki=0(ni)(mki)xk

根据待定系数法 xkxk 的系数相同,可得 ki=0(ni)(mki)=(n+mk)ki=0(ni)(mki)=(n+mk)

方法2:

一个集合有 n+mn+m 个元素,从中选 kk 个元素的方案数为 (n+mk)(n+mk)

其等价于,将集合分成 n,mn,m 两部分,从 nn 中选 i(0ik)i(0ik) 个再从 mm 中选 kiki 个,共 ki=0(ni)(mki)ki=0(ni)(mki) 种方案。

推论1的证明:

由定理1简单变换可得

ki=0(ni)(mki)=kri=r(ni+r)(mkir)=si=r(ni+r)(msi)=(n+ms+r)ki=0(ni)(mki)=kri=r(ni+r)(mkir)=si=r(ni+r)(msi)=(n+ms+r)

于是得证。

推论2的证明:

由定理1简单变换可得

ni=0(ni)(ni1)=ni=0(ni)(nni+1)=(2nn+1)=(2nn1)ni=0(ni)(ni1)=ni=0(ni)(nni+1)=(2nn+1)=(2nn1)

于是得证。

推论3的证明:

由定理1简单变换可得

ni=0(ni)2=ni=0(ni)(nni)=(2nn)ni=0(ni)2=ni=0(ni)(nni)=(2nn)

于是得证。

推论4的证明:

方法1:

由定理1简单变换可得

ni=0(ni)(mi)=ni=0(ni)(mmi)=(n+mm)ni=0(ni)(mi)=ni=0(ni)(mmi)=(n+mm)

于是得证。

方法2:

n×mn×m 的网格图上,从 (0,0)(0,0) 走到 (n,m)(n,m) ,只能向右或者向下走,有多少方案。

显然会向右走 mm 步和向下走 nn 步共 n+mn+m 步,所以方案数是 (n+mm)(nn)=(n+mm)(n+mm)(nn)=(n+mm)

其等价于,将 n+mn+m 步分为 n,mn,m 步,在 nn 步中选 i(0im)i(0im) 步向右,然后在 mm 步选 mimi 步向右,有 mi=0(ni)(mi)mi=0(ni)(mi) 种方案。

卢卡斯定理

定理1(卢卡斯定理)pp 是质数,则 (nm)(nmodpmmodp)(n/pm/p)(modp)(nm)(nmodpmmodp)(n/pm/p)(modp)

  • 推论1(定理1的推论)pp 是质数,整数 n,m(0mn)n,m(0mn)pp 进制表达分别为 n=akak1a0,m=bkbk1b0n=akak1a0,m=bkbk1b0 ,其中 ai,bi[0,p)(0ik)ai,bi[0,p)(0ik) ,则 (nm)ki=0(aibi)(modp)(nm)ki=0(aibi)(modp)

定理1的证明:

先证明 (x+y)pxp+yp(modp)(x+y)pxp+yp(modp) ,其中 pp 为素数。

考虑二项式 (x+y)p(x+y)p ,根据二项式定理有 (x+y)p=pi=0(pi)xpiyi(x+y)p=pi=0(pi)xpiyi ,其中 (pi)=p!i!(pi)!(pi)=p!i!(pi)! ,显然当且仅当 i=0i=0i=pi=p(pi)(pi) 没有质因子 pp 且此时值为 11 ,因此 (pi)[i=0i=p](modp)(pi)[i=0i=p](modp) ,所以 (x+y)pxp+yp(modp)(x+y)pxp+yp(modp)

现在我们证明定理。

考虑二项式 (1+x)n(1+x)n ,我们有

(1+x)n(1+x)pn/p(1+x)nmodp(1+xp)n/p(1+x)nmodp(n/pi=0(n/pi)xip)(nmodpj=0(nmodpj)xj)n/pi=0nmodpj=0(n/pi)(nmodpj)xip+j(modp)(1+x)n(1+x)pn/p(1+x)nmodp(1+xp)n/p(1+x)nmodpn/pi=0(n/pi)xip(nmodpj=0(nmodpj)xj)n/pi=0nmodpj=0(n/pi)(nmodpj)xip+j(modp)

其中 j[0,nmodp],nmodp<pj[0,nmodp],nmodp<p ,那么不会有同一组 (i,j)(i,j) 使得 ip+jip+j 相等,因此和式里的系数就是 xip+jxip+j 的系数。

根据二项式定理 (1+x)n=ni=0(ni)xi(1+x)n=ni=0(ni)xixmxm 的系数为 (nm)(nm) 。在模 pp 下,令 ip+j=mip+j=mi=mp,j=mmodpi=mp,j=mmodpxmxm 的系数为 (nmodpmmodp)(n/pm/p)(nmodpmmodp)(n/pm/p) 。因此,根据待定系数法可得, (nm)(nmodpmmodp)(n/pm/p)(modp)(nm)(nmodpmmodp)(n/pm/p)(modp)

推论1的证明:

把定理1的 n,mn,m 持续递推,发现每次得到的都是 pp 进制下的数位,因此得证。

组合数的求法

加法递推

根据性质5可得加法递推公式 (nm)=(n1m)+(n1m1)(nm)=(n1m)+(n1m1) ,依次递推可得 0mn0mn 的所有组合数。

这个递推在模数为素数时,完全可以被公式法替代,其他情况可以考虑使用。

时间复杂度 O(n2)O(1)O(n2)O(1)

空间复杂度 O(n2)O(n2)

const int P = 1e9 + 7;
const int N = 2e3 + 7;
int comb[N][N];
void get_comb(int n) {
for (int i = 0;i <= n;i++)
for (int j = 0;j <= i;j++)
comb[i][j] = 0 < j && j < i ? (comb[i - 1][j - 1] + comb[i - 1][j]) % P : 1;
}
int C(int n, int m) {
if (n == m && m == -1) return 1; //* 隔板法特判
if (n < m || m < 0) return 0;
return comb[n][m];
}

乘法递推

根据性质3可得乘法递推公式 (nm)=nm+1m(nm1)(nm)=nm+1m(nm1) ,可以直接求确定 nn 的组合数,注意先乘法后除法。

可以利用性质2 (nm)=(nnm)(nm)=(nnm) 去掉一半计算量。

当我们无法保存加法递推的所有组合数,但只需要一行组合数时,可以考虑此法。

注意此法不可直接取模,并且取模情况可以由公式法直接替代。

时间复杂度 O(n)O(1)O(n)O(1)

空间复杂度 O(n)O(n)

const int N = 34;
int comb_n, comb[N];
void get_comb(int n) {
comb_n = n;
comb[0] = comb[comb_n] = 1;
for (int i = 1;2 * i <= comb_n;i++) comb[i] = comb[comb_n - i] = 1LL * (comb_n - i + 1) * comb[i - 1] / i;
}
int Cn(int m) {
if (comb_n == m && m == -1) return 1; //* 隔板法特判
if (comb_n < m || m < 0) return 0;
return comb[m];
}

公式法

公式法是组合数取素数模时最好的解法,其利用逆元处理除法求 (nm)=n!m!(nm)!(nm)=n!m!(nm)! ,可以在线性时间内处理出 0mn0mn 的所有组合数。

公式法在复杂度上优于加法递推,与乘法递推相同;在使用范围上与加法递推相同,优于乘法递推,可以完全替代前两个方法。

时间复杂度 O(n)O(1)O(n)O(1)

空间复杂度 O(n)O(n)

const int P = 1e9 + 7;
const int N = 1e7 + 7;
int qpow(int a, ll k) {
int ans = 1;
while (k) {
if (k & 1) ans = 1LL * ans * a % P;
k >>= 1;
a = 1LL * a * a % P;
}
return ans;
}
int fact[N], invfact[N];
void get_inverse(int n) {
fact[0] = 1;
for (int i = 1;i <= n;i++) fact[i] = 1LL * i * fact[i - 1] % P;
invfact[n] = qpow(fact[n], P - 2);
for (int i = n;i >= 1;i--) invfact[i - 1] = 1LL * invfact[i] * i % P;
}
int C(int n, int m) {
if (n == m && m == -1) return 1; //* 隔板法特判
if (n < m || m < 0) return 0;
return 1LL * fact[n] * invfact[n - m] % P * invfact[m] % P;
}

卢卡斯定理

在模数为不大的素数,但 nn 很大时,可以考虑用卢卡斯定理 (nm)(nmodpmmodp)(n/pm/p)(modp)(nm)(nmodpmmodp)(n/pm/p)(modp) 分解组合数,再利用公式法求解。

时间复杂度 O(p)O(logn)O(p)O(logn)

空间复杂度 O(p)O(p)

const int P = 1e5 + 3;
const int N = 1e5 + 7;
int qpow(int a, ll k) {
int ans = 1;
while (k) {
if (k & 1) ans = 1LL * ans * a % P;
k >>= 1;
a = 1LL * a * a % P;
}
return ans;
}
int fact[N], invfact[N];
void get_inverse(int n) {
fact[0] = 1;
for (int i = 1;i <= n;i++) fact[i] = 1LL * i * fact[i - 1] % P;
invfact[n] = qpow(fact[n], P - 2);
for (int i = n;i >= 1;i--) invfact[i - 1] = 1LL * invfact[i] * i % P;
}
int C(int n, int m) {
if (n == m && m == -1) return 1; //* 隔板法特判
if (n < m || m < 0) return 0;
return 1LL * fact[n] * invfact[n - m] % P * invfact[m] % P;
}
int Lucas(int n, int m) {
int ans = 1;
while (n) {
ans = 1LL * ans * C(n % P, m % P) % P;
n /= P, m /= P;
}
return ans;
}

扩展卢卡斯定理

扩展卢卡斯定理解决了卢卡斯定理无法解决的非素数模数情况,其主要利用了CRT将问题分解,又通过威尔逊定理相关解决了质数幂模数的组合数求模问题。

证明:

我们先将问题分解为多个质数幂的模,因为模数互质,所以最后可以用CRT合并,于是有

{(nm)a1(modpc11)(nm)a2(modpc22)(nm)ak(modpckk)⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪(nm)a1(modpc11)(nm)a2(modpc22)(nm)ak(modpckk)

接下来我们要求出 kk 个方程中的 aiai

不妨我们考虑其中一个方程 (nm)n!m!(nm)!a(modpc)(nm)n!m!(nm)!a(modpc) ,我们可以对各个阶乘取模后合并,其中分母取逆元。

注意到阶乘可能含有因子 pp ,可能导致结果为 00 或者无法求逆元,但实际上在分式中因子 pp 会被消除,因此我们可以考虑先将 pp 因子全部提出,再对除去所有 pp 因子的阶乘求模,于是有 n!pvp(n!)m!pvp(m!)(nm)!pvp((nm)!)pvp(n!)vp(m!)vp((nm)!)a(modpc)n!pvp(n!)m!pvp(m!)(nm)!pvp((nm)!)pvp(n!)vp(m!)vp((nm)!)a(modpc)

不妨考虑 n!pvp(n!)n!pvp(n!) 的求法,根据威尔逊定理的推论1有 n!pvp(n!)(±1)n/pαn/p!pvp(n/p!)((nmodpc)!)p(modpc)n!pvp(n!)(±1)n/pαn/p!pvp(n/p!)((nmodpc)!)p(modpc) ,其中 ±1±1 的判定根据威尔逊定理的定理5即可, ((nmodpc)!)p((nmodpc)!)p 可以预处理,于是我们可以递归求解。但这是尾递归,我们可以改为迭代形式。因此,我们将分式三部分的余数求完,对分母取逆元变为乘法, pvp(n!)vp(m!)vp((nm)!)pvp(n!)vp(m!)vp((nm)!) 利用快速幂求解,最后相乘即可求出 aa

最后,求完 kk 个方程的余数后,我们通过CRT合并。

这个过程证明相当复杂,初学者可以学个板子略过了。

时间复杂度 O(P+ipcii)O(logP+ilogn)O(P+ipcii)O(logP+ilogn)

空间复杂度 O(ipcii)O(ipcii)

ll exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) { x = 1, y = 0; return a; }
ll d = exgcd(b, a % b, x, y);
x -= (a / b) * y, swap(x, y);
return d;
}
ll inv(ll a, ll P) {
ll x, y;
exgcd(a, P, x, y);
return (x % P + P) % P;
}
int qpow(int a, ll k, int P) {
int ans = 1;
while (k) {
if (k & 1) ans = 1LL * ans * a % P;
k >>= 1;
a = 1LL * a * a % P;
}
return ans;
}
ll CRT(const vector<ll> &a, const vector<ll> &p) {
int k = a.size() - 1;
ll P = 1, ans = 0;
for (int i = 1;i <= k;i++) P *= p[i];
for (int i = 1;i <= k;i++) {
ll Pi = P / p[i], invPi = inv(Pi, p[i]);
(ans += (__int128_t)a[i] * Pi * invPi % P) %= P;
}
return ans;
}
int fpr(ll n, int p, int P) {
vector<int> f(P);
f[0] = 1;
for (int i = 1;i < P;i++)
f[i] = 1LL * f[i - 1] * (i % p ? i : 1) % P;
int ans = 1;
while (n) {
if ((p != 2 || P <= 4) && ((n / P) & 1)) ans = P - ans;
ans = 1LL * ans * f[n % P] % P;
n /= p;
}
return ans;
}
int Cpr(ll n, ll m, int p, int P) {
int cnt = 0;
for (ll i = n;i;i /= p) cnt += i / p;
for (ll i = m;i;i /= p) cnt -= i / p;
for (ll i = n - m;i;i /= p) cnt -= i / p;
return 1LL * qpow(p, cnt, P) * fpr(n, p, P) % P *
inv(fpr(m, p, P), P) % P * inv(fpr(n - m, p, P), P) % P;
}
int exLucas(ll n, ll m, int P) {
if (n == m && m == -1) return 1; //* 隔板法特判
if (n < m || m < 0) return 0;
vector<ll> a(1), p(1);
for (int i = 2;i * i <= P;i++) {
if (!(P % i)) {
p.push_back(1);
while (!(P % i)) p.back() *= i, P /= i;
a.push_back(Cpr(n, m, i, p.back()));
}
}
if (P > 1) p.push_back(P), a.push_back(Cpr(n, m, P, p.back()));
return CRT(a, p);
}

枚举质因子重数

对于不取模的大组合数,直接使用高精度乘除法的复杂度比较大,因此考虑先求出质因子的幂次,再高精度累乘即可。

注意先预处理 nn 以内的质数,复杂度玄学,估计下面这个。

时间复杂度 O(n)O(nlog(nm))O(n)O(nlog(nm))

空间复杂度 O(n+log(nm))O(n+log(nm))

///继承vector解决位数限制(当前最大位数是9倍整型最大值),操作方便(注意size()返回无符号长整型,尽量不要直接把size放入表达式)
struct Huge_Int :vector<long long> {
static const int WIDTH = 9;///压位数,压9位以下 比较安全
static const long long BASE = 1e9;///单位基
static const long long MAX_INT = ~(1 << 31);///最大整型
bool SIGN;
///初始化,同时也可以将低精度转高精度、字符串转高精度
///无需单独写高精度数和低精度数的运算函数,十分方便
Huge_Int(long long n = 0) { *this = n; }
Huge_Int(const string &str) { *this = str; }
///格式化,包括进位和去前导0,用的地方很多,先写一个
Huge_Int &format(int fixlen = 1) {//去0后长度必须大于等于fixlen,给乘法用的
while (size() > fixlen && !back()) pop_back();//去除最高位可能存在的0
if (!back()) SIGN = 0;
for (int i = 1; i < size(); ++i) {
(*this)[i] += (*this)[i - 1] / BASE;
(*this)[i - 1] %= BASE;
}//位内进位
while (back() >= BASE) {
push_back(back() / BASE);
(*this)[size() - 2] %= BASE;
}//位外进位
return *this;//为使用方便,将进位后的自身返回引用
}
///归零
void reset() {
clear();
SIGN = 0;
}
///重载等于,初始化、赋值、输入都用得到
Huge_Int operator=(long long n) {
reset();
SIGN = n < 0;
if (SIGN) n = -n;
push_back(n);
format();
return *this;
}
Huge_Int operator=(const string &str) {
reset();
if (str.empty()) push_back(0);
SIGN = str[0] == '-';
for (int i = str.length() - 1;i >= 0 + SIGN;i -= WIDTH) {
long long tmp = 0;
for (int j = max(i - WIDTH + 1, 0 + SIGN);j <= i;j++)
tmp = (tmp << 3) + (tmp << 1) + (str[j] ^ 48);
push_back(tmp);
}
format();
return *this;
}
///重载输入输出
friend istream &operator>>(istream &is, Huge_Int &tmp) {
string str;
if (!(is >> str)) return is;
tmp = str;
return is;
}
friend ostream &operator<<(ostream &os, const Huge_Int &tmp) {
if (tmp.empty()) os << 0;
else {
if (tmp.SIGN) os << '-';
os << tmp[tmp.size() - 1];
}
for (int i = tmp.size() - 2;i >= 0;i--) {
os << setfill('0') << setw(WIDTH) << tmp[i];
}
return os;
}
///重载逻辑运算符,只需要小于,其他的直接代入即可
///常量引用当参数,避免拷贝更高效
friend bool operator<(const Huge_Int &a, const Huge_Int &b) {
if (a.SIGN ^ b.SIGN) return a.SIGN;
if (a.size() != b.size()) return a.SIGN ? a.size() > b.size() :a.size() < b.size();
for (int i = a.size() - 1; i >= 0; i--)
if (a[i] != b[i])return a.SIGN ? a[i] > b[i] : a[i] < b[i];
return 0;
}
friend bool operator>(const Huge_Int &a, const Huge_Int &b) { return b < a; }
friend bool operator>=(const Huge_Int &a, const Huge_Int &b) { return !(a < b); }
friend bool operator<=(const Huge_Int &a, const Huge_Int &b) { return !(a > b); }
friend bool operator!=(const Huge_Int &a, const Huge_Int &b) { return a < b || b < a; }
friend bool operator==(const Huge_Int &a, const Huge_Int &b) { return !(a != b); }
///重载负号
friend Huge_Int operator-(Huge_Int a) { return a.SIGN = !a.SIGN, a; }
///绝对值函数
friend Huge_Int abs(Huge_Int a) { return a.SIGN ? (-a) : a; }
///加法,先实现+=,这样更简洁高效
friend Huge_Int &operator+=(Huge_Int &a, const Huge_Int &b) {
if (a.SIGN ^ b.SIGN) return a -= (-b);
if (a.size() < b.size()) a.resize(b.size());
for (int i = 0; i < b.size(); i++) a[i] += b[i];//被加数要最大位,并且相加时不要用未定义区间相加
return a.format();
}
friend Huge_Int operator+(Huge_Int a, const Huge_Int &b) { return a += b; }
friend Huge_Int &operator++(Huge_Int &a) { return a += 1; }
friend Huge_Int operator++(Huge_Int &a, int) {
Huge_Int old = a;
++a;
return old;
}
///减法,由于后面有交换,故参数不用引用
friend Huge_Int &operator-=(Huge_Int &a, Huge_Int b) {
if (a.SIGN ^ b.SIGN) return a += (-b);
if (abs(a) < abs(b)) {
Huge_Int t = a;
a = b;
b = t;
a.SIGN = !a.SIGN;
}
for (int i = 0; i < b.size(); a[i] -= b[i], i++) {
if (a[i] < b[i]) {//需要借位
int j = i + 1;
while (!a[j]) j++;
while (j > i) a[j--]--, a[j] += BASE;
}
}
return a.format();
}
friend Huge_Int operator-(Huge_Int a, const Huge_Int &b) { return a -= b; }
friend Huge_Int &operator--(Huge_Int &a) { return a -= 1; }
friend Huge_Int operator--(Huge_Int &a, int) {
Huge_Int old = a;
--a;
return old;
}
///乘法,不能先实现*=,因为是类多项式相乘,每位都需要保留,不能覆盖
friend Huge_Int operator*(const Huge_Int &a, const Huge_Int &b) {
Huge_Int n;
n.SIGN = a.SIGN ^ b.SIGN;
n.assign(a.size() + b.size() - 1, 0);//表示乘积后最少的位数(可能会被format消掉,因此添加了format参数)
for (int i = 0; i < a.size(); i++) {
for (int j = 0; j < b.size(); j++)
n[i + j] += a[i] * b[j];
n.format(n.size());//提前进位
}
return n;//最后进位可能会溢出
}
friend Huge_Int &operator*=(Huge_Int &a, const Huge_Int &b) { return a = a * b; }
///带余除法函数,方便除法和模运算,暂时写不出高效的高精与高精的除法
friend Huge_Int divmod(Huge_Int &a, const Huge_Int &b) {//O(logn),待修改
assert(b != 0);
Huge_Int n;
if (-MAX_INT - 1 <= b && b <= MAX_INT) {//除数小于等于整型才能用这个,不然会溢出
n = a;
n.SIGN = a.SIGN ^ b.SIGN;
long long rest = 0;
long long bl = 0;
for (int i = b.size() - 1;i >= 0;i--) bl = bl * BASE + b[i];
for (int i = n.size() - 1;i >= 0;i--) {
rest *= BASE;
n[i] += rest;
rest = n[i] % bl;
n[i] /= bl;
}
a = a.SIGN ? (-rest) : rest;
return n.format();
}
else {//考虑倍增或者二分优化
n.SIGN = a.SIGN ^ b.SIGN;
for (int i = a.size() - b.size(); abs(a) >= abs(b); i--) {//减法代替除法
Huge_Int c, d;
d.assign(i + 1, 0);
d.back() = 1;
d.SIGN = n.SIGN;
c = b * d;//提高除数位数进行减法
while (abs(a) >= abs(c)) a -= c, n += d;
d.pop_back();
if (!d.empty()) {//遍历压的位
d.back() = BASE / 10;
for (int i = 1;i < WIDTH;i++) {
c = b * d;
while (abs(a) >= abs(c)) a -= c, n += d;
d.back() /= 10;
}
}
}
return n;
}
}
friend Huge_Int operator/(Huge_Int a, const Huge_Int &b) { return divmod(a, b); }
friend Huge_Int &operator/=(Huge_Int &a, const Huge_Int &b) { return a = a / b; }
friend Huge_Int &operator%=(Huge_Int &a, const Huge_Int &b) { return divmod(a, b), a; }
friend Huge_Int operator%(Huge_Int a, const Huge_Int &b) { return a %= b; }
};
const int N = 1e6 + 7;
bool vis[N];
vector<int> prime;
void get_prime(int n) {
for (int i = 2;i <= n;i++) {
if (!vis[i]) prime.push_back(i);
for (auto j : prime) {
if (i * j > n) break;
vis[i * j] = 1;
if (!(i % j)) break;
}
}
}
int legendre_fact(int n, int p) {
int cnt = 0;
while (n) {
cnt += n / p;
n /= p;
}
return cnt;
}
Huge_Int C(int n, int m) {
if (n == m && m == -1) return 1; //* 隔板法特判
if (n < m || m < 0) return 0;
Huge_Int ans(1);
for (int i = 0;i < prime.size();i++) {
int k =
legendre_fact(n, prime[i]) -
legendre_fact(m, prime[i]) -
legendre_fact(n - m, prime[i]);
int p = prime[i];
while (k) {
if (k & 1) ans *= p;
k >>= 1;
p *= p;
}
}
return ans;
}

多重集组合

多重集组合的定义与基本性质

定义 设多重集 S={n1a1,n2a2,,nkak}S={n1a1,n2a2,,nkak} ,从中任选 mm 个元素组成集合,称为 SSmm 组合。

性质1 设多重集 S={n1a1,n2a2,,nkak}S={n1a1,n2a2,,nkak} ,若 mm 满足 mni+(1ik)mni+(1ik) ,则 mm 组合数为 (k+m1m)(k+m1m)

性质1的证明:

可以先将 mm 个物品排成一排,然后指定物品的是哪种物品。因为是无序的,我们只需要考虑每种元素有几个即可,因此可以按顺序划分 kk 个组别,一个组代表一个种类,同组的元素是同种的。我们用 k1k1 个隔板划分 kk 个组,如果把隔板也当成一个位置,那么物品和隔板一共 m+k1m+k1 个位置,我们从中选 mm 个位置放物品,其他放隔板即可,共 (k+m1m)(k+m1m) 种方案。这就是隔板法的一种应用。

该问题等价于 x1+x2++xk=nx1+x2++xk=n 的非负整数解个数,我们有 kk11 ,第 ii 种代表在 xixi11 。每种 11 都是无限的,这对应着 xixi 的范围是 [0,+)[0,+)

计数技巧

计数的方法与原则

方法 将目标方案合理分解为多个简单部分或步骤,利用计数原理合并。

原则 分解的方案应该不重不漏覆盖原方案,即新方案与原方案产生一一映射的关系。

捆绑法

描述 当要求某些元素相邻时,我们可以把它们先看作一个整体与其他元素计数,再对这个整体内部计数,用乘法原理合并。

应用1,2,3,4,51,2,3,4,5 五个人,其中 1,21,23,43,4 分别相邻,求排列数。我们把 1,21,23,43,4 分别捆绑在一起,先对 (1,2),(3,4),5(1,2),(3,4),5 排列,共 P33P33 种,再分别对两个整体内部排列,分别为 P22,P22P22,P22 种,因此共计 P33P22P22P33P22P22 种排列。

插空法

描述 当要求某些元素两两不相邻时,我们可以先把其他元素放好,再把要求不相邻的元素插入不同的空隙或两端。

应用1,2,3,4,5,6,71,2,3,4,5,6,7 七个人,其中 1,2,31,2,3 两两不相邻,求排列数。我们先把 4,5,6,74,5,6,7 先排好,共 P44P44 种,再将 1,2,31,2,3 插入其中,有 55 个可插入位置,共 P35P35 种,因此共计 P44P35P44P35 种排列。

隔板法

描述 当需要对相同物品分组时,可以采用隔板法,在物品之间插入隔板表示组与组的分别。

应用nn 个球放入 mm 个盒子,要求盒子不为空,求方案数。我们可以先给每个盒子一个球保证非空,再对剩下 nmnm 个球分成 mm 组,即在 nmnm 个球之间插入 m1m1 个隔板,共 (n1nm)(n1nm) 种方案。

posted @   空白菌  阅读(582)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
点击右上角即可分享
微信分享提示