组合数学笔记-排列与组合
排列与组合
排列
排列的定义与基本性质
定义 设一个集合 SS 中有 nn 个元素,从中有序地取出 m(0≤m≤n)m(0≤m≤n) 个元素排成一列, 称为 SS 的一个 mm 排列。两个排列相同,当且仅当元素相同且顺序相同。我们记 PmnPmn 、 AmnAmn 或 P(n,m)P(n,m) 表示 SS 中 mm 排列的总数。
约定 当 m>nm>n 时,Pmn=0Pmn=0 。
全排列的定义 设一个集合 SS 中有 nn 个元素,其 nn 排列称为全排列。
- C++中,
next_permutation
函数可以按字典序从小到大遍历数据的全排列,prev_permutation
函数与之相反。
性质1 Pmn=n(n−1)(n−2)⋯(n−m+1)=n!(n−m)!Pmn=n(n−1)(n−2)⋯(n−m+1)=n!(n−m)! ,其中 0≤m≤n0≤m≤n。
性质2 Pmn=nPm−1n−1Pmn=nPm−1n−1 。
性质3 Pmn=mPm−1n−1+Pmn−1Pmn=mPm−1n−1+Pmn−1 。
性质1的证明:
考虑乘法原理,按顺序选数,第 i(1≤i≤m)i(1≤i≤m) 个数有 n−i+1n−i+1 种选法,乘在一起可得原式。
性质2的证明:
考虑乘法原理,第一个数有 nn 种选法,再从剩下的 n−1n−1 个数里选 m−1m−1 个有 Pm−1n−1Pm−1n−1 种排列,所以 Pmn=nPm−1n−1Pmn=nPm−1n−1 。
性质3的证明:
考虑加法原理,考虑分两类:
- 先指定选一个元素,有 mm 个位置可放,再从剩下 n−1n−1 元素中选 m−1m−1 个有 Pm−1n−1Pm−1n−1 种排列。
- 不选1指定的元素,从其他 n−1n−1 元素里选 mm 个,共 Pm−1nPm−1n 种排列。
因此 Pmn=mPm−1n−1+Pmn−1Pmn=mPm−1n−1+Pmn−1 。
错位排列
错位排列的定义与基本性质
定义 设 P=(p1,p2,⋯,pn)P=(p1,p2,⋯,pn) 是 S={1,2,⋯,n}S={1,2,⋯,n} 的一个排列,若对于任意的 i∈[1,n]i∈[1,n] 满足 pi≠ipi≠i ,则称 PP 是 SS 的一个错位排列。我们记 DnDn 表示 SS 的错位排列的总数。
性质1 DnDn 满足
性质2 Dn=n!n∑k=0(−1)kk!Dn=n!n∑k=0(−1)kk! 。
性质3 Dn=⌊n!e+12⌋Dn=⌊n!e+12⌋ 。
性质4 {1,2,⋯,n}{1,2,⋯,n} 的排列是错位排列的概率 PnPn 有渐进 limn→∞Pn=limn→∞Dnn!=1elimn→∞Pn=limn→∞Dnn!=1e ,表明 DnDn 的增长率与 n!n! 仅相差常数倍。
性质1的证明:
考虑加法原理,设 nn 出现在位置 k(1≤k≤n−1)k(1≤k≤n−1) ,有两种情况:
- kk 一定出现在位置 nn ,那么除去 k,nk,n 剩下 n−2n−2 个数错排即可,共计 Dn−2Dn−2 。
- kk 不能出现在位置 nn ,那么可把位置 nn 视作 kk 的新位置与除了 nn 的数进行错排,共计 Dn−1Dn−1 。
因此再根据乘法原理,有 Dn=(n−1)(Dn−1+Dn−2)Dn=(n−1)(Dn−1+Dn−2) 。
性质2的证明:
考虑减法原理,设全集 UU 为 [1,n][1,n] 的全排列,则 |U|=n!|U|=n! ,设满足 pi≠ipi≠i 的排列的集合为 SiSi ,那么满足 pi=ipi=i 的集合为 ¯Si¯¯¯¯¯Si ,那么有 |n⋂i=1Si|=|U|−|n⋃i=1¯Si|∣∣ ∣∣n⋂i=1Si∣∣ ∣∣=|U|−∣∣ ∣∣n⋃i=1¯¯¯¯¯Si∣∣ ∣∣ ,问题转换为求 |n⋃i=1¯Si|∣∣ ∣∣n⋃i=1¯¯¯¯¯Si∣∣ ∣∣ 。
考虑容斥原理,有 |n⋃i=1Si|=n∑k=1(−1)k−1∑1≤i1<i2<⋯<ik≤n|k⋂j=1Sij|∣∣ ∣∣n⋃i=1Si∣∣ ∣∣=n∑k=1(−1)k−1∑1≤i1<i2<⋯<ik≤n∣∣ ∣∣k⋂j=1Sij∣∣ ∣∣ ,其中 |k⋂j=1Sij|∣∣ ∣∣k⋂j=1Sij∣∣ ∣∣ 为所有 i∈Ti∈T 满足 pi=ipi=i 的排列数,显然为 (n−k)!(n−k)! ,对于 1≤i1<i2<⋯<ik<n1≤i1<i2<⋯<ik<n 共有 (nk)(nk) 种方案,因此 |n⋃i=1Si|=n∑k=1(−1)k−1(nk)(n−k)!=n∑k=1(−1)k−1n!k!∣∣ ∣∣n⋃i=1Si∣∣ ∣∣=n∑k=1(−1)k−1(nk)(n−k)!=n∑k=1(−1)k−1n!k! 。
最后,错位排列数 |n⋂i=1Si|=|U|−|n⋃i=1¯Si|=n!−n∑k=1(−1)k−1n!k!=n!n∑k=0(−1)kk!∣∣ ∣∣n⋂i=1Si∣∣ ∣∣=|U|−∣∣ ∣∣n⋃i=1¯¯¯¯¯Si∣∣ ∣∣=n!−n∑k=1(−1)k−1n!k!=n!n∑k=0(−1)kk! 。
圆排列
圆排列的定义与基本性质
定义 设一个集合 SS 中有 nn 个元素,从中有序地取出 m(0≤m≤n)m(0≤m≤n) 个元素排成不分首尾围成一圈, 称为 SS 的一个 mm 圆排列。两个圆排列相同,当且仅当元素相同且不分首位地顺序相同。我们记 QmnQmn 表示 SS 的 mm 圆排列的总数。
性质1 Qmn=PmnmQmn=Pmnm 。
性质1的证明:
对于每一种圆排列,我们可以规定首尾使其变成标准排列,共有 mm 种首尾方案,因此有 Qmn⋅m=PmnQmn⋅m=Pmn ,所以 Qmn=PmnmQmn=Pmnm 。
多重集排列
多重集排列的定义与基本性质
定义 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak}S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} ,从中任选 mm 个元素组成排列,称为 SS 的 mm 排列。
多重组合数的定义 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak}S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} , n=k∑i=1nin=k∑i=1ni ,则 SS 的 nn 排列(即全排列)的总数称为多重组合数,记为 (nn1,n2,⋯,nk)(nn1,n2,⋯,nk) 。
性质1 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak}S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} ,n=k∑i=1nin=k∑i=1ni ,则全排列数为 (nn1,n2,⋯,nk)=n!k∏i=1ni!(nn1,n2,⋯,nk)=n!k∏i=1ni! 。
性质2 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak}S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} ,若 mm 满足 m≤ni≤+∞(1≤i≤k)m≤ni≤+∞(1≤i≤k) ,则 mm 排列数为 kmkm 。
性质1的证明:
不考虑元素重复的全排列为 n!n! ,再对每个元素的全排列 ni!(1≤i≤k)ni!(1≤i≤k) 去重,所以 (nn1,n2,⋯,nk)=n!k∏i=1ni!(nn1,n2,⋯,nk)=n!k∏i=1ni! 。
性质2的证明:
因为选 mm 个小于等于任意元素的个数,所以每次都能选 kk 个元素,因此总数为 kmkm 。
组合
组合的定义与基本性质
定义 设一个集合 SS 中有 nn 个元素,从中无序地取出 m(0≤m≤n)m(0≤m≤n) 个元素组成集合, 称为 SS 的一个 mm 组合。两个组合相同,当且仅当元素相同。我们记 CmnCmn 、 (nm)(nm) 或 C(n,m)C(n,m) 表示 SS 中 mm 组合的总数。
约定 当 m>nm>n 时,(nm)=0(nm)=0 。
性质1 (nm)=Pmnm!=n!m!(n−m)!(nm)=Pmnm!=n!m!(n−m)! 。
性质2 (nm)=(nn−m)(nm)=(nn−m) 。
性质3 (nm)=n−m+1m(nm−1)(nm)=n−m+1m(nm−1) 。
性质4 (nm)=nm(n−1m−1)(nm)=nm(n−1m−1) 。
性质5(杨辉三角) (nm)=(n−1m)+(n−1m−1)(nm)=(n−1m)+(n−1m−1) 。
性质6 n∑i=0(im)=(n+1m+1)n∑i=0(im)=(n+1m+1) 。
性质7 (nm)(mk)=(nk)(n−km−k)(nm)(mk)=(nk)(n−km−k) 。
性质8 n∑i=0(n−ii)=Fn+1n∑i=0(n−ii)=Fn+1 ,其中 FF 是斐波那契数列。
性质1的证明:
先考虑顺序得到的总数为 mm 排列数 PmnPmn ,而对于一种 mm 组合有 m!m! 种排列方式,所以 m!(nm)=Pmnm!(nm)=Pmn ,因此根据排列性质1可得 (nm)=Pmnm!=n!m!(n−m)!(nm)=Pmnm!=n!m!(n−m)! 。
性质2的证明:
方法1:
由性质1易得。
方法2:
nn 个里选 mm 个组合,等价于 nn 个里选 n−mn−m 不在组合。
性质5的证明:
方法1:
由性质1易得。
方法2:
画出杨辉三角,由数学归纳法易得。
方法3:
指定一个元素,如果一定不选它则有 (n−1m)(n−1m) 种组合,如果一定选它则有 (n−1m−1)(n−1m−1) 种组合,考虑加法原理,得证。
性质6的证明:
方法1:
根据性质5可得
n∑i=0(im)=n∑i=m(im)=(m+1m+1)+(m+1m)+(m+2m)+⋯+(nm)=(m+2m+1)+(m+2m)+⋯+(nm)=(m+3m+1)+⋯+(nm)=(n+1m+1)n∑i=0(im)=n∑i=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(0≤i≤n)i(0≤i≤n) 的组合数为 (im)(im) ,考虑加法原理得证。
性质7的证明:
方法1:
由性质1可得
(nm)(mk)=n!m!(n−m)!⋅m!k!(m−k)!=n!k!⋅1(m−k)!(n−m)!=n!k!(n−k)!⋅(n−k)!(m−k)!(n−m)!=(nk)(n−km−k)(nm)(mk)=n!m!(n−m)!⋅m!k!(m−k)!=n!k!⋅1(m−k)!(n−m)!=n!k!(n−k)!⋅(n−k)!(m−k)!(n−m)!=(nk)(n−km−k)方法2:
左式是正着选,从 nn 个元素中选 mm 个,对于每个 mm 组合再选 kk 个的 kk 组合的总数的总和。
右式是倒着选,先从 nn 个元素中选 kk 个,对于每个 kk 组合从剩下 n−kn−k 个元素中再选 m−km−k 个元素,代表这个 kk 组合会被所有 mm 组合选到的次数,最后总和与左式意义等价。
性质8的证明:
设 Gn=n∑i=0(n−ii)Gn=n∑i=0(n−ii) ,则 G0=1=F1,G1=1=F2G0=1=F1,G1=1=F2 。
假设当 n=k+1(k≥0)n=k+1(k≥0) 时, Gk=Fk+1,Gk+1=Fk+2Gk=Fk+1,Gk+1=Fk+2 成立。
那么当 n=k+2n=k+2 时,由性质5可得
Gk+Gk+1=k∑i=0(k−ii)+k+1∑i=0(k+1−ii)=k+1∑i=1(k−i+1i−1)+k+1∑i=1(k+1−ii)+1=k+1∑i=1((k+1−ii−1)+(k+1−ii))+1=k+1∑i=1(k+2−ii)+(k+20)+(0k+2)=k+2∑i=0(k+2−ii)=Gk+2Gk+Gk+1=k∑i=0(k−ii)+k+1∑i=0(k+1−ii)=k+1∑i=1(k−i+1i−1)+k+1∑i=1(k+1−ii)+1=k+1∑i=1((k+1−ii−1)+(k+1−ii))+1=k+1∑i=1(k+2−ii)+(k+20)+(0k+2)=k+2∑i=0(k+2−ii)=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=n∑i=0(ni)xn−iyi(x+y)n=n∑i=0(ni)xn−iyi 。
-
推论1(定理1的推论) n∑i=0(ni)=2nn∑i=0(ni)=2n 。
-
推论2(定理1的推论) n∑i=0(−1)i(ni)=[n=0]n∑i=0(−1)i(ni)=[n=0] 。
-
推论3(推论2的推论) (n0)+(n2)+⋯=(n1)+(n3)+⋯=2n−1(n0)+(n2)+⋯=(n1)+(n3)+⋯=2n−1 ,其中 n≥1n≥1 。
定理2(扩展二项式定理) (k∑i=1xi)n=∑n1+n2+⋯+nk=n(nn1,n2,⋯,nk)k∏i=1xnii(k∑i=1xi)n=∑n1+n2+⋯+nk=n(nn1,n2,⋯,nk)k∏i=1xnii 。
广义组合数的定义 设 α∈R,m∈Nα∈R,m∈N ,则 (αm)=m∏i=0(α−i)m!(αm)=m∏i=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=k∑i=0(ki)xk−iyi(x+y)k=k∑i=0(ki)xk−iyi 成立。
当 n=k+1n=k+1 时,根据组合基本性质5有
(x+y)k+1=(x+y)k∑i=0(ki)xk−iyi=xk∑i=0(ki)xk−iyi+yk∑i=0(ki)xk−iyi=k∑i=0(ki)xk+1−iyi+k+1∑i=1(ki−1)xk−i+1yi=(k0)xk+1+k∑i=1(ki)xk+1−iyi+k∑i=1(ki−1)xk−i+1yi+(kk)yk+1=(k0)xk+1+k∑i=1(k+1i)xk+1−iyi+(k+1k+1)yk+1=k+1∑i=0(k+1i)xk+1−iyi(x+y)k+1=(x+y)k∑i=0(ki)xk−iyi=xk∑i=0(ki)xk−iyi+yk∑i=0(ki)xk−iyi=k∑i=0(ki)xk+1−iyi+k+1∑i=1(ki−1)xk−i+1yi=(k0)xk+1+k∑i=1(ki)xk+1−iyi+k∑i=1(ki−1)xk−i+1yi+(kk)yk+1=(k0)xk+1+k∑i=1(k+1i)xk+1−iyi+(k+1k+1)yk+1=k+1∑i=0(k+1i)xk+1−iyi于是得证。
方法2:
我们枚举 x,yx,y 的指数 i,ji,j 满足 i+j=ni+j=n 的情况,等价于枚举 i(0≤i≤n)i(0≤i≤n) 得到 j=n−ij=n−i 。对于一个 ii 的情况,需要求在 nn 个 (x+y)(x+y) 括号中有 ii 个选 xx 和 n−in−i 个选 yy 的方案数。
我们可以 nn 选 ii 后 n−in−i 选 n−in−i 共 (ni)(n−in−i)=(ni)(ni)(n−in−i)=(ni) 种方案。
当然,我们也可以理解为一个多重集的模型,有 ii 个选 xx 的 (x+y)(x+y) 和 n−in−i 个选 yy 的 (x+y)(x+y) ,选择相同的 (x+y)(x+y) 算相同的元素,不同的顺序算不同的方案,所以是求有限多重集的全排列数,为 (ni,n−i)=(ni)(ni,n−i)=(ni) 种方案。
因此 (x+y)n=n∑i=0(ni)xn−iyi(x+y)n=n∑i=0(ni)xn−iyi 。
定理2的证明:
同样可以使用归纳法,但比较复杂,我们使用定理1的证法2,即组合意义证明。
我们枚举 xi(1≤i≤k)xi(1≤i≤k) 的指数 nini 满足 n1+n2+⋯+nk=nn1+n2+⋯+nk=n 的情况。对于一组非负整数解 n1,n2,⋯,nkn1,n2,⋯,nk ,需要求在 nn 个 (x1+x2+⋯+xk)(x1+x2+⋯+xk) 括号中有 ni(1≤i≤k)ni(1≤i≤k) 个括号内选了 xixi 的方案数。
我们设多重集有 ni(1≤i≤k)ni(1≤i≤k) 个选 xixi 的 (x1+x2+⋯+xk)(x1+x2+⋯+xk) , 其全排列数为 (nn1,n2,⋯,nk)(nn1,n2,⋯,nk) 。
因此 (k∑i=1xi)n=∑n1+n2+⋯+nk=n(nn1,n2,⋯,nk)k∏i=1xnii(k∑i=1xi)n=∑n1+n2+⋯+nk=n(nn1,n2,⋯,nk)k∏i=1xnii 。
范德蒙德卷积
定理1(范德蒙德卷积) k∑i=0(ni)(mk−i)=(n+mk)k∑i=0(ni)(mk−i)=(n+mk) 。
- 推论1(定理1的推论) r∑i=−s(ni+s)(mr−i)=(n+ms+r)r∑i=−s(ni+s)(mr−i)=(n+ms+r) 。
- 推论2(定理1的推论) n∑i=0(ni)(ni−1)=(2nn−1)n∑i=0(ni)(ni−1)=(2nn−1) 。
- 推论3(定理1的推论) n∑i=0(ni)2=(2nn)n∑i=0(ni)2=(2nn) 。
- 推论4(定理1的推论) m∑i=0(ni)(mi)=(n+mm)m∑i=0(ni)(mi)=(n+mm) 。
定理1的证明:
方法1:
由二项式定理的定理1可得
n+m∑k=0(n+mk)xk=(1+x)n+m=(1+x)n(1+x)m=(n∑i=0(ni)xi)(m∑j=0(mj)xj)=n∑i=0m∑j=0(ni)(mj)xi+j=n+m∑k=0k∑i=0(ni)(mk−i)xkn+m∑k=0(n+mk)xk=(1+x)n+m=(1+x)n(1+x)m=(n∑i=0(ni)xi)(m∑j=0(mj)xj)=n∑i=0m∑j=0(ni)(mj)xi+j=n+m∑k=0k∑i=0(ni)(mk−i)xk根据待定系数法 xkxk 的系数相同,可得 k∑i=0(ni)(mk−i)=(n+mk)k∑i=0(ni)(mk−i)=(n+mk) 。
方法2:
一个集合有 n+mn+m 个元素,从中选 kk 个元素的方案数为 (n+mk)(n+mk) 。
其等价于,将集合分成 n,mn,m 两部分,从 nn 中选 i(0≤i≤k)i(0≤i≤k) 个再从 mm 中选 k−ik−i 个,共 k∑i=0(ni)(mk−i)k∑i=0(ni)(mk−i) 种方案。
推论1的证明:
由定理1简单变换可得
k∑i=0(ni)(mk−i)=k−r∑i=−r(ni+r)(mk−i−r)=s∑i=−r(ni+r)(ms−i)=(n+ms+r)k∑i=0(ni)(mk−i)=k−r∑i=−r(ni+r)(mk−i−r)=s∑i=−r(ni+r)(ms−i)=(n+ms+r)于是得证。
推论2的证明:
由定理1简单变换可得
n∑i=0(ni)(ni−1)=n∑i=0(ni)(nn−i+1)=(2nn+1)=(2nn−1)n∑i=0(ni)(ni−1)=n∑i=0(ni)(nn−i+1)=(2nn+1)=(2nn−1)于是得证。
推论3的证明:
由定理1简单变换可得
n∑i=0(ni)2=n∑i=0(ni)(nn−i)=(2nn)n∑i=0(ni)2=n∑i=0(ni)(nn−i)=(2nn)于是得证。
推论4的证明:
方法1:
由定理1简单变换可得
n∑i=0(ni)(mi)=n∑i=0(ni)(mm−i)=(n+mm)n∑i=0(ni)(mi)=n∑i=0(ni)(mm−i)=(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(0≤i≤m)i(0≤i≤m) 步向右,然后在 mm 步选 m−im−i 步向右,有 m∑i=0(ni)(mi)m∑i=0(ni)(mi) 种方案。
卢卡斯定理
定理1(卢卡斯定理) 若 pp 是质数,则 (nm)≡(nmodpmmodp)(⌊n/p⌋⌊m/p⌋)(modp)(nm)≡(nmodpmmodp)(⌊n/p⌋⌊m/p⌋)(modp) 。
- 推论1(定理1的推论) 若 pp 是质数,整数 n,m(0≤m≤n)n,m(0≤m≤n) 的 pp 进制表达分别为 n=akak−1⋯a0,m=bkbk−1⋯b0n=akak−1⋯a0,m=bkbk−1⋯b0 ,其中 ai,bi∈[0,p)(0≤i≤k)ai,bi∈[0,p)(0≤i≤k) ,则 (nm)≡k∏i=0(aibi)(modp)(nm)≡k∏i=0(aibi)(modp) 。
定理1的证明:
先证明 (x+y)p≡xp+yp(modp)(x+y)p≡xp+yp(modp) ,其中 pp 为素数。
考虑二项式 (x+y)p(x+y)p ,根据二项式定理有 (x+y)p=p∑i=0(pi)xp−iyi(x+y)p=p∑i=0(pi)xp−iyi ,其中 (pi)=p!i!(p−i)!(pi)=p!i!(p−i)! ,显然当且仅当 i=0i=0 或 i=pi=p 时 (pi)(pi) 没有质因子 pp 且此时值为 11 ,因此 (pi)≡[i=0∨i=p](modp)(pi)≡[i=0∨i=p](modp) ,所以 (x+y)p≡xp+yp(modp)(x+y)p≡xp+yp(modp) 。
现在我们证明定理。
考虑二项式 (1+x)n(1+x)n ,我们有
(1+x)n≡(1+x)p⌊n/p⌋(1+x)nmodp≡(1+xp)⌊n/p⌋(1+x)nmodp≡(⌊n/p⌋∑i=0(⌊n/p⌋i)xip)(nmodp∑j=0(nmodpj)xj)≡⌊n/p⌋∑i=0nmodp∑j=0(⌊n/p⌋i)(nmodpj)xip+j(modp)(1+x)n≡(1+x)p⌊n/p⌋(1+x)nmodp≡(1+xp)⌊n/p⌋(1+x)nmodp≡⎛⎝⌊n/p⌋∑i=0(⌊n/p⌋i)xip⎞⎠(nmodp∑j=0(nmodpj)xj)≡⌊n/p⌋∑i=0nmodp∑j=0(⌊n/p⌋i)(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=n∑i=0(ni)xi(1+x)n=n∑i=0(ni)xi , xmxm 的系数为 (nm)(nm) 。在模 pp 下,令 ip+j=mip+j=m 有 i=⌊mp⌋,j=mmodpi=⌊mp⌋,j=mmodp , xmxm 的系数为 (nmodpmmodp)(⌊n/p⌋⌊m/p⌋)(nmodpmmodp)(⌊n/p⌋⌊m/p⌋) 。因此,根据待定系数法可得, (nm)≡(nmodpmmodp)(⌊n/p⌋⌊m/p⌋)(modp)(nm)≡(nmodpmmodp)(⌊n/p⌋⌊m/p⌋)(modp) 。
推论1的证明:
把定理1的 n,mn,m 持续递推,发现每次得到的都是 pp 进制下的数位,因此得证。
组合数的求法
加法递推
根据性质5可得加法递推公式 (nm)=(n−1m)+(n−1m−1)(nm)=(n−1m)+(n−1m−1) ,依次递推可得 0≤m≤n0≤m≤n 的所有组合数。
这个递推在模数为素数时,完全可以被公式法替代,其他情况可以考虑使用。
时间复杂度 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)=n−m+1m(nm−1)(nm)=n−m+1m(nm−1) ,可以直接求确定 nn 的组合数,注意先乘法后除法。
可以利用性质2 (nm)=(nn−m)(nm)=(nn−m) 去掉一半计算量。
当我们无法保存加法递推的所有组合数,但只需要一行组合数时,可以考虑此法。
注意此法不可直接取模,并且取模情况可以由公式法直接替代。
时间复杂度 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!(n−m)!(nm)=n!m!(n−m)! ,可以在线性时间内处理出 0≤m≤n0≤m≤n 的所有组合数。
公式法在复杂度上优于加法递推,与乘法递推相同;在使用范围上与加法递推相同,优于乘法递推,可以完全替代前两个方法。
时间复杂度 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/p⌋⌊m/p⌋)(modp)(nm)≡(nmodpmmodp)(⌊n/p⌋⌊m/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!(n−m)!≡a(modpc)(nm)≡n!m!(n−m)!≡a(modpc) ,我们可以对各个阶乘取模后合并,其中分母取逆元。
注意到阶乘可能含有因子 pp ,可能导致结果为 00 或者无法求逆元,但实际上在分式中因子 pp 会被消除,因此我们可以考虑先将 pp 因子全部提出,再对除去所有 pp 因子的阶乘求模,于是有 n!pvp(n!)m!pvp(m!)⋅(n−m)!pvp((n−m)!)⋅pvp(n!)−vp(m!)−vp((n−m)!)≡a(modpc)n!pvp(n!)m!pvp(m!)⋅(n−m)!pvp((n−m)!)⋅pvp(n!)−vp(m!)−vp((n−m)!)≡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((n−m)!)pvp(n!)−vp(m!)−vp((n−m)!) 利用快速幂求解,最后相乘即可求出 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={n1⋅a1,n2⋅a2,⋯,nk⋅ak}S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} ,从中任选 mm 个元素组成集合,称为 SS 的 mm 组合。
性质1 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak}S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} ,若 mm 满足 m≤ni≤+∞(1≤i≤k)m≤ni≤+∞(1≤i≤k) ,则 mm 组合数为 (k+m−1m)(k+m−1m) 。
性质1的证明:
可以先将 mm 个物品排成一排,然后指定物品的是哪种物品。因为是无序的,我们只需要考虑每种元素有几个即可,因此可以按顺序划分 kk 个组别,一个组代表一个种类,同组的元素是同种的。我们用 k−1k−1 个隔板划分 kk 个组,如果把隔板也当成一个位置,那么物品和隔板一共 m+k−1m+k−1 个位置,我们从中选 mm 个位置放物品,其他放隔板即可,共 (k+m−1m)(k+m−1m) 种方案。这就是隔板法的一种应用。
该问题等价于 x1+x2+⋯+xk=nx1+x2+⋯+xk=n 的非负整数解个数,我们有 kk 种 11 ,第 ii 种代表在 xixi 的 11 。每种 11 都是无限的,这对应着 xixi 的范围是 [0,+∞)[0,+∞) 。
计数技巧
计数的方法与原则
方法 将目标方案合理分解为多个简单部分或步骤,利用计数原理合并。
原则 分解的方案应该不重不漏覆盖原方案,即新方案与原方案产生一一映射的关系。
捆绑法
描述 当要求某些元素相邻时,我们可以把它们先看作一个整体与其他元素计数,再对这个整体内部计数,用乘法原理合并。
应用 有 1,2,3,4,51,2,3,4,5 五个人,其中 1,21,2 和 3,43,4 分别相邻,求排列数。我们把 1,21,2 和 3,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 个盒子,要求盒子不为空,求方案数。我们可以先给每个盒子一个球保证非空,再对剩下 n−mn−m 个球分成 mm 组,即在 n−mn−m 个球之间插入 m−1m−1 个隔板,共 (n−1n−m)(n−1n−m) 种方案。
本文来自博客园,作者:空白菌,转载请注明原文链接:https://www.cnblogs.com/BlankYang/p/17165559.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)