数论:排列数、组合数、费马小定理、逆元、同余定理

一、排列数:有序的选择

排列数 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(log⁡p)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(log⁡p)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)!)。
  • 这些算是比较偏的数论知识,但是在我们的编程过程中还是可能会遇见的,认真学习本文,一定会有所收获。
相关推荐
xqqxqxxq1 小时前
LeetCode Hot100 双指针专项题解笔记
笔记·算法·leetcode
闪电悠米1 小时前
力扣hot100-54.螺旋矩阵-模拟边界控制详解
算法·leetcode·矩阵
变量未定义~2 小时前
虚拟节点-星石传送阵(4星)、强连通分量
数据结构·算法
IT方大同2 小时前
C语言分支与循环语句
c语言·开发语言·算法
库克克2 小时前
【C++】STL 库
c++
待磨的钝刨2 小时前
深入理解主成分分析(PCA)
人工智能·线性代数·算法·机器学习
星恒随风3 小时前
C++ 继承进阶:默认成员函数、多继承、虚继承与组合设计
开发语言·c++·笔记·学习
呜喵王阿尔萨斯3 小时前
C/C++ const -- 多义混乱
c语言·开发语言·c++
海清河晏1113 小时前
Qt实战:从零构建美化登录界面
开发语言·c++·qt