2

组合数学笔记-排列与组合 - 空白菌

 1 year ago
source link: https://www.cnblogs.com/BlankYang/p/17165559.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

排列与组合

排列的定义与基本性质

定义 设一个集合 S 中有 n 个元素,从中有序地取出 m(0≤m≤n) 个元素排成一列, 称为 S 的一个 m 排列。两个排列相同,当且仅当元素相同且顺序相同。我们记 Pmn 、 Amn 或 P(n,m) 表示 S 中 m 排列的总数。

约定 当 m>n 时,Pmn=0 。

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

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

性质1 Pmn=n(n−1)(n−2)⋯(n−m+1)=n!(n−m)! ,其中 0≤m≤n。

性质2 Pmn=nPm−1n−1 。

性质3 Pmn=mPm−1n−1+Pmn−1 。

性质1的证明:

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

性质2的证明:

考虑乘法原理,第一个数有 n 种选法,再从剩下的 n−1 个数里选 m−1 个有 Pm−1n−1 种排列,所以 Pmn=nPm−1n−1 。

性质3的证明:

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

  1. 先指定选一个元素,有 m 个位置可放,再从剩下 n−1 元素中选 m−1 个有 Pm−1n−1 种排列。
  2. 不选1指定的元素,从其他 n−1 元素里选 m 个,共 Pm−1n 种排列。

因此 Pmn=mPm−1n−1+Pmn−1 。

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

定义 设 P=(p1,p2,⋯,pn) 是 S={1,2,⋯,n} 的一个排列,若对于任意的 i∈[1,n] 满足 pi≠i ,则称 P 是 S 的一个错位排列。我们记 Dn 表示 S 的错位排列的总数。

性质1 Dn 有递推公式

Dn={1,n=00,n=11,n=2(n−1)(Dn−1+Dn−2),n≥3

性质2 Dn 有通项公式 Dn=n!n∑k=0(−1)kk! 。

性质3 Dn 有简单表达式 Dn=⌊n!e⌋ 。

性质4 {1,2,⋯,n} 的排列是错位排列的概率 Pn 有渐进 limn→∞Pn=limn→∞Dnn!=1e ,表明 Dn 的增长率与 n! 仅相差常数倍。

性质1的证明:

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

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

因此再根据乘法原理,有 Dn=(n−1)(Dn−1+Dn−2) 。

性质2的证明:

考虑减法原理,设全集 U 为 [1,n] 的全排列,则 |U|=n! ,设满足 pi≠i 的排列的集合为 Si ,那么满足 pi=i 的集合为 ¯Si ,那么有 |n⋂i=1Si|=|U|−|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| ,其中 |k⋂j=1Sij| 为所有 i∈T 满足 pi=i 的排列数,显然为 (n−k)! ,对于 1≤i1<i2<⋯<ik<n 共有 (nk) 种方案,因此 |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! 。

圆排列的定义与基本性质

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

性质1 Qmn=Pmnm 。

性质1的证明:

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

多重集排列

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

定义 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} ,从中任选 m 个元素组成排列,称为 S 的 m 排列。

多重组合数的定义 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} , n=k∑i=1ni ,则 S 的 n 排列(即全排列)的总数称为多重组合数,记为 (nn1,n2,⋯,nk) 。

性质1 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} ,n=k∑i=1ni ,则全排列数为 (nn1,n2,⋯,nk)=n!k∏i=1ni! 。

性质2 设多重集 S={n1⋅a1,n2⋅a2,⋯,nk⋅ak} ,若 m 满足 m≤n≤+∞(1≤i≤k) ,则 m 排列数为 km 。

性质1的证明:

不考虑元素重复的全排列为 n! ,再对每个元素的全排列 ni!(1≤i≤k) 去重,所以 (nn1,n2,⋯,nk)=n!k∏i=1ni! 。

性质2的证明:

因为选 m 个小于等于任意元素的个数,所以每次都能选 k 个元素,因此总数为 km 。

组合的定义与基本性质

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

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

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

性质2 (nm)=(nn−m) 。

性质3 (nm)=n−m+1m(nm−1) 。

性质4 (nm)=nm(n−1m−1) 。

性质5(杨辉三角) (nm)=(n−1m)+(n−1m−1) 。

性质6 n∑i=0(im)=(n+1m+1) 。

性质7 (nm)(mk)=(nk)(n−km−k) 。

性质8 n∑i=0(n−ii)=Fn+1 ,其中 F 是斐波那契数列。

性质1的证明:

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

性质2的证明:

由性质1易得。

n 个里选 m 个组合,等价于 n 个里选 n−m 不在组合。

性质5的证明:

由性质1易得。

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

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

性质6的证明:

根据性质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)

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

性质7的证明:

由性质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)

左式是正着选,从 n 个元素中选 m 个,对于每个 m 组合再选 k 个的 k 组合的总数的总和。

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

性质8的证明:

设 Gn=n∑i=0(n−ii) ,则 G0=1=F1,G1=1=F2 。

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

那么当 n=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+2

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

二项式定理

定理1(二项式定理) (x+y)n=n∑i=0(ni)xn−iyi 。

  • 推论1(定理1的推论) n∑i=0(ni)=2n 。

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

  • 推论3(推论2的推论) (n0)+(n2)+⋯=(n1)+(n3)+⋯=2n−1 ,其中 n≥1 。

定理2(扩展二项式定理) (k∑i=1xi)n=∑n1+n2+⋯+nk=n(nn1,n2,⋯,nk)k∏i=1xnii 。

广义组合数的定义 设 α∈R,m∈N ,则 (αm)=m∏i=0(α−i)m! 。

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

定理1的证明:

当 n=0 时显然得证。

假设当 n=k 时, (x+y)k=k∑i=0(ki)xk−iyi 成立。

当 n=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 的指数 i,j 满足 i+j=n 的情况,等价于枚举 i(0≤i≤n) 得到 j=n−i 。对于一个 i 的情况,需要求在 n 个 (x+y) 括号中有 i 个选 x 和 n−i 个选 y 的方案数。

我们可以 n 选 i 后 n−i 选 n−i 共 (ni)(n−in−i)=(ni) 种方案。

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

因此 (x+y)n=n∑i=0(ni)xn−iyi 。

定理2的证明:

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

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

我们设多重集有 ni(1≤i≤k) 个选 xi 的 (x1+x2+⋯+xk) , 其全排列数为 (nn1,n2,⋯,nk) 。

因此 (k∑i=1xi)n=∑n1+n2+⋯+nk=n(nn1,n2,⋯,nk)k∏i=1xnii 。

范德蒙德卷积

定理1(范德蒙德卷积) k∑i=0(ni)(mk−i)=(n+mk) 。

  • 推论1(定理1的推论) r∑i=−s(ni+s)(mr−i)=(n+ms+r) 。
  • 推论2(定理1的推论) n∑i=0(ni)(ni−1)=(2nn−1) 。
  • 推论3(定理1的推论) n∑i=0(ni)2=(2nn) 。
  • 推论4(定理1的推论) m∑i=0(ni)(mi)=(n+mm) 。

定理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)xk

根据待定系数法 xk 的系数相同,可得 k∑i=0(ni)(mk−i)=(n+mk) 。

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

其等价于,将集合分成 n,m 两部分,从 n 中选 i(0≤i≤k) 个再从 m 中选 k−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)

于是得证。

推论2的证明:

由定理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)

于是得证。

推论4的证明:

由定理1简单变换可得

n∑i=0(ni)(mi)=n∑i=0(ni)(mm−i)=(n+mm)

于是得证。

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

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

其等价于,将 n+m 步分为 n,m 步,在 n 步中选 i(0≤i≤m) 步向右,然后在 m 步选 m−i 步向右,有 m∑i=0(ni)(mi) 种方案。

卢卡斯定理

定理1(卢卡斯定理) 若 p 是质数,则 (nm)≡(nmodpmmodp)(⌊n/p⌋⌊m/p⌋)(modp) 。

  • 推论1(定理1的推论) 若 p 是质数,整数 n,m(0≤m≤n) 的 p 进制表达分别为 n=akak−1⋯a0,m=bkbk−1⋯b0 ,其中 ai,bi∈[0,p)(0≤i≤k) ,则 (nm)≡k∏i=0(aibi)(modp) 。

定理1的证明:

先证明 (x+y)p≡xp+yp(modp) ,其中 p 为素数。

考虑二项式 (x+y)p ,根据二项式定理有 (x+y)p=p∑i=0(pi)xp−iyi ,其中 (pi)=p!i!(p−i)! ,显然当且仅当 i=0 或 i=p 时 (pi) 没有质因子 p 且此时值为 1 ,因此 (pi)≡[i=0∨i=p](modp) ,所以 (x+y)p≡xp+yp(modp) 。

现在我们证明定理。

考虑二项式 (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)

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

根据二项式定理 (1+x)n=n∑i=0(ni)xi , xm 的系数为 (nm) 。在模 p 下,令 ip+j=m 有 i=⌊mp⌋,j=mmodp , xm 的系数为 (nmodpmmodp)(⌊n/p⌋⌊m/p⌋) 。因此,根据待定系数法可得, (nm)≡(nmodpmmodp)(⌊n/p⌋⌊m/p⌋)(modp) 。

推论1的证明:

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

组合数的求法

根据性质5可得加法递推公式 (nm)=(n−1m)+(n−1m−1) ,依次递推可得 0≤m≤n 的所有组合数。

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

时间复杂度 O(n2)

空间复杂度 O(n2)

const int P = 1e9 + 7;namespace CNM { const int N = 2e3 + 7; int c[N][N]; void init(int n) { for (int i = 0;i <= n;i++) for (int j = 0;j <= i;j++) c[i][j] = 0 < j && j < i ? (c[i - 1][j - 1] + c[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 c[n][m]; }}

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

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

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

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

时间复杂度 O(n)

空间复杂度 O(n)

namespace CNM { const int N = 34; int n, Cn[N]; void init(int _n) { n = _n; Cn[0] = Cn[n] = 1; for (int i = 1;2 * i <= n;i++) Cn[i] = Cn[n - i] = 1LL * (n - i + 1) * Cn[i - 1] / i; } int C(int m) { if (n < m || m < 0) return 0; return Cn[m]; }}

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

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

时间复杂度 O(n)

空间复杂度 O(n)

const int P = 1e9 + 7;namespace Number_Theory { 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 init(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; }}namespace CNM { using namespace Number_Theory; 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; }}

卢卡斯定理

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

时间复杂度 O(p+logn)

空间复杂度 O(n)

const int P = 1e5 + 3;namespace Number_Theory { 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 init(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; }}namespace CNM { using namespace Number_Theory; 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; }}namespace Lucas { int C(int n, int m) { int ans = 1; while (n) { ans = 1LL * ans * CNM::C(n % P, m % P) % P; n /= P, m /= P; } return ans; }}

扩展卢卡斯定理

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

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

{(nm)≡a1(modpα11)(nm)≡a2(modpα22)⋮(nm)≡ak(modpαkk)

接下来我们要求出 k 个方程中的 ai 。

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

注意到阶乘可能含有因子 p ,可能导致结果为 0 或者无法求逆元,但实际上在分式中因子 p 会被消除,因此我们可以考虑先将 p 因子全部提出,再对除去所有 p 因子的阶乘求模,于是有 n!vp(n!)m!vp(m!)⋅(n−m)!vp((n−m)!)⋅px−y−z≡a(modpα) 。

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

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

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

时间复杂度 O(∑i(logpin+logpi+pαii))

空间复杂度 O(max{pαii})

namespace Number_Theory { 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; }}namespace CRT { using namespace Number_Theory; ll solve(int k, const vector<int> &a, const vector<int> &p) { 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; }}namespace exLucas { using namespace Number_Theory; 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 C(ll n, ll m, int P) { if (n == m && m == -1) return 1; //* 隔板法特判 if (n < m || m < 0) return 0; int k = 0; vector<int> a(20), p(20); for (int i = 2;i * i <= P;i++) { if (!(P % i)) { p[++k] = 1; while (!(P % i)) p[k] *= i, P /= i; a[k] = Cpr(n, m, i, p[k]); } } if (P > 1) p[++k] = P, a[k] = Cpr(n, m, P, p[k]); return CRT::solve(k, a, p); }}

枚举质因子重数

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

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

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

空间复杂度 O(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; }}; namespace Number_Theory { 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; } } }}namespace Legendre { using namespace Number_Theory; int fact_pexp(int n, int p) { int cnt = 0; while (n) { cnt += n / p; n /= p; } return cnt; }}namespace CNM { using namespace Number_Theory; 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_pexp(n, prime[i]) - Legendre::fact_pexp(m, prime[i]) - Legendre::fact_pexp(n - m, prime[i]); int p = prime[i]; while (k) { if (k & 1) ans *= p; k >>= 1; p *= p; } } return ans; }}

多重集的组合

排列组合技巧


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK