领略算法真谛:求组合数
嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的
passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let’s go!
我的博客:yuanManGan
我的专栏:C++入门小馆 C言雅韵集 数据结构漫游记 闲言碎语小记坊 进阶数据结构 走进Linux的世界 题山采玉 领略算法真谛
求组合数
先来回归一下高中知识:
从 n 个不同的元素中,任取 m 个元素排成⼀列。所有取法的个数就是排列数,记作 AnmA_n^mAnm。
Anm=n!(n−m)!=n⋅(n−1)⋅(n−2)⋅⋯⋅(n−m+1)A_n^m = \frac{n!}{(n - m)!} = n \cdot (n - 1) \cdot (n - 2) \cdot \dots \cdot (n - m + 1)Anm=(n−m)!n!=n⋅(n−1)⋅(n−2)⋅⋯⋅(n−m+1)
从 n 个不同的元素中,任取 m 个元素。所有取法的个数就是组合数,记作 CnmC_n^mCnm。
Cnm=n!m!⋅(n−m)!=n×(n−1)×(n−2)×⋯×(n−m+1)m×(m−1)×(m−2)×⋯×2×1C_n^m = \frac{n!}{m! \cdot (n - m)!} = \frac{n \times (n - 1) \times (n - 2) \times \dots \times (n - m + 1)}{m \times (m - 1) \times (m - 2) \times \dots \times 2 \times 1} \quad \text{} Cnm=m!⋅(n−m)!n!=m×(m−1)×(m−2)×⋯×2×1n×(n−1)×(n−2)×⋯×(n−m+1)
下面就来写写几种求组合数的方法吧!
方法一循环
问题:单次查询 CnmmodpC_n^m\mod pCnmmodp(其中 1≤n≤1061 \leq n \leq 10^61≤n≤106,ppp 为质数且 p>np > np>n)
查询: O(m)
直接用公式循环求解即可,注意m!
要使用逆元
#include<iostream>
using namespace std;typedef long long LL;
//快速幂
LL qpow(LL a, LL b, LL p)
{LL ret = 1;while (b){if (b & 1) ret = ret * a % p;a = a * a % p;b >>= 1; }return ret;
}
LL C(int n, int m, int p)
{LL up = 1, down = 1;for (int i = n; i >= n - m + 1; i--) up = up * i % p;for (int i = 2; i <= m; i++) down = down * i % p;return up * qpow(down, p - 2, p) % p;
}
int main()
{cout << C(4, 2, 9) << endl;return 0;
}
方法二 杨辉三角
0 ⾏: 1
1 ⾏: 1 1
2 ⾏: 1 2 1
3 ⾏: 1 3 3 1
4 ⾏: 1 4 6 4 1
其中第 i ⾏就是 n = i 展开后的各项系数。
通过杨辉三⻆可以得出⼀个公式:
Cnk=Cn−1k−1+Cn−1kC_n^k = C_{n-1}^{k-1} + C_{n-1}^kCnk=Cn−1k−1+Cn−1k
const int N = 2e3 + 10;
int n, p = 1001;
LL f[N][N];
void get_c()
{for (int i = 0; i <= N; i++){f[i][0] = 1;for (int j = 1; j <= i; j++){f[i][j] = (f[i - 1][j - 1] + f[i - 1][j]) % p;}}
}
int main()
{get_c();cout << f[3][2] << endl;return 0;
}
方式三阶乘以及阶乘逆元表 + 公式
多次查询CnmmodpC_n^m \mod pCnmmodp 1≤n≤1061 \leq n \leq 10^61≤n≤106 q≤106q \leq 10^6q≤106 p 为质数且大于 n。
数据量过⼤,⽤杨辉三⻆打表会超时并且超空间。
• 此时考虑使⽤公式直接计算。
Cnm=n!(n−m)!⋅m!C_n^m = \frac{n!}{(n - m)! \cdot m!} Cnm=(n−m)!⋅m!n!
• 只要能够将所有的阶乘,以及阶乘的逆元全部存在两个数组中,那么仅需在两个表中拿出相应的值
即可。
我们需要打几个表
打表所有的阶乘:
f[i] 表⽰ i 的阶乘 mod p 的值,f[i] = f[i − 1] × i
。
这样,从左往右递推⼀遍,就可以把所有的阶乘全部维护出来。
打表所有的阶乘的逆元:
用 ( g[i] ) 表示 ( i ) 的阶乘模 ( p ) 的值的逆元,即:
g[i]=(i×(i−1)×⋯×1)−1modpg[i] = \left( i \times (i-1) \times \dots \times 1 \right)^{-1} \mod p g[i]=(i×(i−1)×⋯×1)−1modp
g[i−1]=(i×(i−1)×⋯×1)−1×imodpg[i-1] = \left( i \times (i-1) \times \dots \times 1 \right)^{-1} \times i \mod p g[i−1]=(i×(i−1)×⋯×1)−1×imodp
推出来
g[i−1]=g[i]×imodpg[i-1] =g[i]\times i \mod p g[i−1]=g[i]×imodp
这样,从右往左递推⼀遍,就可以把所有阶乘的逆元维护出来。
const int N = 1e6 + 10;int n, p;
LL f[N], g[N];
void init()
{f[0] = 1;for (int i = 1; i <= N; i++) f[i] = f[i - 1] * i % p;g[n] = qpow(f[n], p - 2, p);for (int i = n - 1; i >= 0; i--) g[i] = g[i + 1] * (i + 1) % p;
}
LL C(int n, int m)
{if (n < m) return 0;return f[n] * g[n - m] % p * g[m] % p;
}
int main()
{return 0;
}
方式四卢卡斯定理
问题:多次查询CnmmodpC_n^m \mod pCnmmodp ,其中1≤n≤10181 \leq n \leq 10^{18}1≤n≤1018,只保证p≤10p \leq 10p≤10且是质数,也就是ppp有可能小于nnn。
我们只需要记住这个公式即可
卢卡斯定理
对于组合数CnmmodpC_n^m \mod pCnmmodp (其中模数 ( p ) 为质数),有如下递归关系:
Cnm≡C⌊np⌋⌊mp⌋×Cnmodpmmodp(modp)C_n^m \equiv C_{\left\lfloor \frac{n}{p} \right\rfloor}^{\left\lfloor \frac{m}{p} \right\rfloor} \times C_{n \mod p}^{m \mod p} \pmod{p}Cnm≡C⌊pn⌋⌊pm⌋×Cnmodpmmodp(modp)
const int N = 1e6 + 10;int n, p;
LL f[N], g[N];
void init()
{f[0] = 1;for (int i = 1; i <= N; i++) f[i] = f[i - 1] * i % p;g[n] = qpow(f[n], p - 2, p);for (int i = n - 1; i >= 0; i--) g[i] = g[i + 1] * (i + 1) % p;
}
LL C(int n, int m, int p)
{if (n < m) return 0;return f[n] * g[n - m] % p * g[m] % p;
}
LL lucas(LL n, LL m, LL p)
{if (m == 0) return 1;return lucas(n / p, m / p, p) * C(n % p, m % p, p) % p;
}
int main()
{return 0;
}