一、排列数:有序的选择
排列数 AnmA_n^mAnm(也记作 P(n,m)P(n,m)P(n,m))表示从 nnn 个不同元素中按顺序 取出 mmm 个排成一列的方法数。
第一位有 nnn 种选法,第二位剩 n−1n-1n−1 种......第 mmm 位有 n−m+1n-m+1n−m+1 种。根据乘法原理:
AnmA_n^mAnm = n×(n−1)×⋯×(n−m+1)n \times (n-1) \times \cdots \times (n-m+1)n×(n−1)×⋯×(n−m+1)
用阶乘表示即为:
AnmA_n^mAnm = n!(n−m)!\frac{n!}{(n-m)!}(n−m)!n!
特殊情形:An0=1A_n^0 = 1An0=1(空排列),Ann=n!A_n^n = n!Ann=n!(全排列)。
例:{1,2,3}\{1,2,3\}{1,2,3} 选两个排列,(1,2)(1,2)(1,2) 与 (2,1)(2,1)(2,1) 视为不同,因此 A32=3×2=6A_3^2 = 3 \times 2 = 6A32=3×2=6。
排列注重顺序。
二、组合数:无序的选择
组合数 CnmC_n^mCnm(或 (nm)\binom{n}{m}(mn))表示从 nnn 个不同元素中不考虑顺序 地取出 mmm 个的方法数。
排列数中,同一批元素的不同排列被重复计算了 m!m!m! 次,因此组合数需将这重复部分除去:
CnmC_n^mCnm = Anmm!=n!m! (n−m)!\frac{A_n^m}{m!} = \frac{n!}{m!\,(n-m)!}m!Anm=m!(n−m)!n!
基本性质:
- Cn0=Cnn=1C_n^0 = C_n^n = 1Cn0=Cnn=1,Cn1=nC_n^1 = nCn1=n。
- 对称性:Cnm=Cn n−mC_n^m = C_n^{\,n-m}Cnm=Cnn−m。
- 递推关系:Cnm=Cn−1m−1+Cn−1mC_n^m = C_{n-1}^{m-1} + C_{n-1}^{m}Cnm=Cn−1m−1+Cn−1m(杨辉三角)。
例:{1,2,3}\{1,2,3\}{1,2,3} 选两个的组合只有 {1,2},{1,3},{2,3}\{1,2\},\{1,3\},\{2,3\}{1,2},{1,3},{2,3} 三种,C32=3C_3^2 = 3C32=3。
组合只关心元素集合,不关心顺序。
三、模运算下的困境
在许多算法题中,需要输出 Cnm mod pC_n^m \bmod pCnmmodp。若直接计算阶乘,数值会极大溢出;即使边乘边取模,也会遇到除法难题:
82 mod 5≠8 mod 52 mod 5\frac{8}{2} \bmod 5 \neq \frac{8 \bmod 5}{2 \bmod 5}28mod5=2mod58mod5
模运算中,除法并不成立。为此,需要引入逆元,将除法转化为乘法。
四、逆元:模意义下的倒数
若对于整数 aaa 与模数 ppp,存在整数 xxx 满足
a⋅x≡1(modp)a \cdot x \equiv 1 \pmod{p}a⋅x≡1(modp)
则称 xxx 为 aaa 在模 ppp 下的乘法逆元 ,记作 a−1a^{-1}a−1 或 inv(a)\text{inv}(a)inv(a)。
利用逆元,除法转化为乘法:
ba mod p=b⋅a−1 mod p\frac{b}{a} \bmod p = b \cdot a^{-1} \bmod pabmodp=b⋅a−1modp
例如模 555 下,2×3=6≡12 \times 3 = 6 \equiv 12×3=6≡1,所以 2−1≡32^{-1} \equiv 32−1≡3,于是 8/2≡8×3=24≡4(mod5)8/2 \equiv 8 \times 3 = 24 \equiv 4 \pmod{5}8/2≡8×3=24≡4(mod5),结果正确。
至此,组合数取模可表示为:
Cnm≡n!⋅(m!)−1⋅((n−m)!)−1(modp)C_n^m \equiv n! \cdot (m!)^{-1} \cdot ((n-m)!)^{-1} \pmod{p}Cnm≡n!⋅(m!)−1⋅((n−m)!)−1(modp)
现在的问题变为:如何高效计算逆元?
五、费马小定理------质数模数下的逆元公式
费马小定理 若 ppp 为质数,且 aaa 不被 ppp 整除,则
a p−1≡1(modp)a^{\,p-1} \equiv 1 \pmod{p}ap−1≡1(modp)
两边同除以 aaa(即乘上 a−1a^{-1}a−1)得:
a p−2≡a−1(modp)a^{\,p-2} \equiv a^{-1} \pmod{p}ap−2≡a−1(modp)
这就是模质数时求逆元的黄金公式:a−1≡ap−2(modp)a^{-1} \equiv a^{p-2} \pmod{p}a−1≡ap−2(modp)。
最常用的模数 109+710^9+7109+7、998244353998244353998244353 均为质数,因此该公式可以应对绝大多数场景。计算 ap−2a^{p-2}ap−2 需要用到快速幂。
快速幂函数 qpow(x,p)\text{qpow}(x, p)qpow(x,p)
快速幂采用"反复平方法",每次将指数折半,时间复杂度 O(logp)O(\log p)O(logp)。
cpp
// 快速幂:计算 x^p mod mod
ll qpow(ll x,ll p){//快速幂函数 x:底数 p:指数
if(p==0) return 1;
if(p==1) return x;
ll ans=qpow(x, p/2);
ans=ans*ans%mod;
if(p%2==1) ans=ans*x%mod; // 若指数为奇数,多乘一个 x
return ans;
}
逆元函数 inv(x)\text{inv}(x)inv(x)
直接应用费马小定理:x−1≡xmod−2(modmod)x^{-1} \equiv x^{mod-2} \pmod{mod}x−1≡xmod−2(modmod)。
cpp
// 求 x 在模 mod 下的逆元(mod 必须为质数)
ll inv(ll x,ll m){//逆元函数 x:进行逆元的数 m:模数(mod)
return qpow(x,m-2); // x^{-1} ≡ x^{mod-2} (mod mod)
}
有了这两个函数,任何数的逆元都可以在 O(logp)O(\log p)O(logp) 内求出。
六、基于逆元的排列组合函数
现在我们回到排列数和组合数的取模计算。只需预处理阶乘数组,再结合刚刚实现的逆元函数,即可 O(1) 查询(查询时需调用 inv,每次 O(log mod))。
阶乘数组 Ai=i! mod modAi = i! \bmod modAi=i!modmod
cpp
ll A[maxn]; // 全局数组,A[i] 表示 i! % mod
// 预处理
A[0]=1;
rep(i,1,n) A[i]=A[i-1]*i%mod;
组合数函数 C(n,m)C(n, m)C(n,m)
代入公式 Cnm≡n!⋅(m!)−1⋅((n−m)!)−1(modmod)C_n^m \equiv n! \cdot (m!)^{-1} \cdot ((n-m)!)^{-1} \pmod{mod}Cnm≡n!⋅(m!)−1⋅((n−m)!)−1(modmod)。
cpp
// 组合数 C(n, m) = n! / (m! * (n-m)!) mod mod
ll C(ll n,ll m)//组合数函数 n:C(m,n)中的n(下面的) m:C(m,n)中的m(上面的)
{
return A[n]*inv(A[m],mod)%mod*inv(A[n-m],mod)%mod;
}
排列数函数 AnmA_n^mAnm
排列数取模公式为 Anm≡n!⋅((n−m)!)−1(modmod)A_n^m \equiv n! \cdot ((n-m)!)^{-1} \pmod{mod}Anm≡n!⋅((n−m)!)−1(modmod)。
cpp
// 排列数 A(n, m) = n! / (n-m)! mod mod
ll Arr(ll n,ll m)
{
return A[n] * inv(A[n-m],mod) % mod;
}
七、完整模板
将上述所有函数整合,形成完整模板。
cpp
#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for(ll i=a;i<=n;++i)
#define per(i,a,n) for(ll i=a;i>=n;--i)
typedef long long ll;
const int maxn=2e5+10;
const int mod=1e9+7;
ll n,A[maxn];//A[i]:存储 i 的阶乘对 mod 取模的结果
ll qpow(ll x,ll p)//快速幂函数 x:底数 p:指数
{
if(p==0)return 1;//任何数的 0 次方都是1
if(p==1)return x;//任何数的 1 次方都是自己本身
ll ans=qpow(x,p/2);//递归计算 x^(p/2) 的值
ans=ans*ans%mod;//平方得到 x^p ,并取模
if(p%2==1)ans=ans*x%mod;//如果 p 是奇数,再乘一个 x 并取模
return ans;
}
ll inv(ll x,ll m)//逆元函数 x:进行逆元的数 m:模数(mod m需为质数)
{
return qpow(x,m-2);//根据费马小定理,x^{-1} ≡ x^{m-2} (mod m)
}
ll Arr(ll n,ll m)//排列数函数 n:C(m,n)中的n(下面的) m:C(m,n)中的m(上面的)
{
return A[n]*inv(A[n-m],mod)%mod;//分子 n! 乘以分母 (n-m)! 的逆元,并取模
}
ll C(ll n,ll m)//组合数函数 n:C(m,n)中的n(下面的) m:C(m,n)中的m(上面的)
{
return A[n]*inv(A[m],mod)%mod*inv(A[n-m],mod)%mod;//分子为 n!,分母用阶乘的逆元替代(分母比排列数多 * 一个 m!)
}
void sol()
{
cin>>n;
A[0]=1;
rep(i,1,n)A[i]=A[i-1]*i%mod;
cout<<C(5,2)<<endl;//10
cout<<Arr(5,2)<<endl;//20
}
int main()
{
sol();
return 0;
}
八、总结
- 排列数 Anm=n!(n−m)!A_n^m = \frac{n!}{(n-m)!}Anm=(n−m)!n!,关心顺序;组合数 Cnm=n!m!(n−m)!C_n^m = \frac{n!}{m!(n-m)!}Cnm=m!(n−m)!n!,不关心顺序。
- 模运算不能直接做除法,需用逆元 将除法转为乘法:ba≡b⋅a−1(modp)\frac{b}{a} \equiv b \cdot a^{-1} \pmod{p}ab≡b⋅a−1(modp)。
- 费马小定理 给出质数模数下的逆元公式:a−1≡ap−2(modp)a^{-1} \equiv a^{p-2} \pmod{p}a−1≡ap−2(modp)。
- 快速幂 qpow(x,p)\text{qpow}(x,p)qpow(x,p) 高效计算幂次,支撑逆元求值。
- 组合数取模实现为 C(n,m)=n!⋅inv(m!)⋅inv((n−m)!)C(n,m) = n! \cdot \text{inv}(m!) \cdot \text{inv}((n-m)!)C(n,m)=n!⋅inv(m!)⋅inv((n−m)!)。
- 这些算是比较偏的数论知识,但是在我们的编程过程中还是可能会遇见的,认真学习本文,一定会有所收获。