当前位置: 首页 > news >正文

Acwing 组合计数

一个递推式:
从 a 个元素中选择 b 个,有多少种取法 C a b = a × ( a − 1 ) × ⋯ × ( a − b + 1 ) 1 × 2 × 3 × ⋯ × b = a ! b ! × ( a − b ) ! = C a − 1 b + C a − 1 b − 1 从a个元素中选择b个,有多少种取法C_{a}^{b} = \frac{a\times(a-1)\times\dots\times(a-b+1)}{1\times2\times3\times\dots\times b} \\=\frac{a!}{b!\times(a-b)!}\\= C_{a-1}^{b} + C_{a-1}^{b-1} a个元素中选择b个,有多少种取法Cab=1×2×3××ba×(a1)××(ab+1)=b!×(ab)!a!=Ca1b+Ca1b1
Acwing 855.求组合数 I
在这里插入图片描述
实现思路:询问10000次,数字范围2000,直接使用递推式

  • 递推式:
    C a b = C a − 1 b + C a − 1 b − 1 C_{a}^{b} = C_{a-1}^{b} + C_{a-1}^{b-1} Cab=Ca1b+Ca1b1
    其实就是动态规划DP, O ( n 2 ) . O(n^2). O(n2).
  • 因为数字范围就2000,直接先预处理出2000范围内的所有组合数,使用二维数组存储c[][],后续只需调用就行
  • 注意结果要取模,防止结果超出int范围

具体实现代码(详解版):

#include <iostream>using namespace std;const int N = 2010,mod = 1e9+7;
int c[N][N];//预处理出这2000范围内的所有组合数
void init(){for(int i = 0 ; i < N ; i ++){for(int j = 0 ; j <= i ; j ++){if(!j) c[i][j] =1;else c[i][j] = (c[i-1][j] + c[i-1][j-1]) % mod;//递推式}}
}int main(){int n;cin >> n;init();while(n --){int a,b;cin >> a >> b;cout << c[a][b] << endl;}return 0;
}

如果数据范围变大,递推的效率就会变低,超时!看下面的题目:
Acwing 886.求组合数II
在这里插入图片描述
实现思路:这是使用公式+乘法逆元的方法预处理得到阶乘
公式 C a b = a ! b ! × ( a − b ) ! 公式C_{a}^{b} =\frac{a!}{b!\times(a-b)!} 公式Cab=b!×(ab)!a!

  • 考虑到计算过程存在取模且公式存在分式,则需要将除法转化为乘法,因为除法取模会得到错误答案,所以转化为乘法再做取模运算,这时就想到乘法逆元(因为本题模数1e9+7是质数,所以可以使用快速幂求逆元,费马定理b^(m-2)。注意若没有对质数取模,则只能用扩展欧几里得算),对分母两个数分别取逆元再乘以分子,即可得到答案
  • 设置一个数组fact[],表示分子阶乘;数组infact[],表示分母阶乘的逆元,最后通过fact[a]*infact[b]*infact[a-b]得到组合数结果
    具体实现代码(详解版)
#include <iostream>
#include <algorithm>using namespace std;typedef long long LL; 
const int N = 100010; 
const int mod = 1e9 + 7; // 模数 (10^9 + 7)// 阶乘数组和阶乘逆元数组
LL fact[N], infact[N];// 快速幂函数,计算 (a^k) % p
LL qmi(LL a, LL k, LL p) {LL res = 1; // 初始化结果为 1LL t = a % p; // 基数 a 取模// 进行快速幂计算while (k) {if (k & 1) // 如果当前位为 1res = (res * t) % p; // 更新结果t = (t * t) % p; // 基数平方并取模k >>= 1; // 右移 k,处理下一位}return res; // 返回结果
}int main() {// 初始化阶乘和逆元fact[0] = infact[0] = 1; // 0! = 1for (int i = 1; i < N; i++) {fact[i] = (fact[i - 1] * i) % mod; // 计算阶乘infact[i] = (infact[i - 1] * qmi(i, mod - 2, mod)) % mod; // 计算阶乘的逆元}int n; cin >> n; while (n--) {int a, b; cin >> a >> b; // 计算并输出组合数 C(a, b)cout << (fact[a] * infact[b] % mod * infact[a - b] % mod) << endl;}return 0; 
}

如果数据范围继续增大,可以考虑使用Lucas定理
Acwing 887.求组合数III
在这里插入图片描述

  • 卢卡斯(lucas)定理:
  • C a b ≡ C a m o d p b m o d p × C a / p b / p ( m o d p ) C_{a}^{b} \equiv C_{a\ mod \ p}^{b\ mod \ p} \times C_{a/p}^{b/p} \pmod{p} CabCa mod pb mod p×Ca/pb/p(modp)
    lucas函数:函数的参数为a,b,若a<p且b<p则说明a,b足够小可以通过组合数函数C(a,b)直接计算,否则返回C(a%p,b%p)*lucas(a/p,b/p)(因为a,b除p可能依然很大,当足够小时就像上述情况直接返回组合数函数)
  • 注意这里求组合数C,与上题不同,无需枚举处理所有组合数,直接用基本公式:
    - C a b = a ∗ ( a − 1 ) . . . ∗ ( a − b + 1 ) b ! C_{a}^{b}=\frac{a*(a-1)...*(a-b+1)}{b!} Cab=b!a(a1)...(ab+1)
  • 采用类似双指针算法,指针i由后向前遍历到a-b+1,j由1向前遍历到b
  • 然后再利用乘法逆元(快速幂求逆元),将除法转化为乘法,取模

具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;typedef long long LL; // 定义长整型别名
const int N = 100010; // 最大支持的 n 值
const int mod = 1e9 + 7; // 模数LL fact[N], infact[N]; // 分子阶乘数组,分母阶乘的逆元数组// 快速幂算法,用于计算 (a^k) % p
LL qmi(LL a, LL k, LL p) {LL res = 1; // 结果初始值为 1LL t = a % p; // 基数 a 取模while (k) {if (k & 1) { // 如果 k 的当前位为 1res = (res * t) % p; // 更新结果}t = (t * t) % p; // 基数平方并取模k >>= 1; // 右移 k,处理下一位}return res; // 返回结果
}// 基本公式求组合数,利用乘法逆元
int C(int a, int b, int p) {if (b < 0 || a < 0 || a < b) return 0; // 检查 b 是否负数,或者 a 是否小于 bLL x = 1, y = 1; // x 是分子,y 是分母for (int i = a, j = 1; j <= b; i--, j++) {x = (x * i) % p; // 计算分子y = (y * j) % p; // 计算分母}return x * (LL)qmi(y, p - 2, p) % p; // 返回组合数
}// Lucas 定理计算组合数
int lucas(LL a, LL b, int p) {if (a < p && b < p) return C(a, b, p); // 基本情况// 递归计算return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}int main() {int n; // 测试案例的数量cin >> n; // 输入测试案例数量while (n--) {LL a, b, p; // a 和 b 为组合数参数,p 为模数cin >> a >> b >> p; // 输入 a、b 和 pcout << lucas(a, b, p) << endl; // 输出结果}return 0;
}

当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用,同时需要用到高精度乘法。
Acwing 888.组合计数IV
在这里插入图片描述

实现思路:

  • 使用组合数公式
    C a b = a ! b ! × ( a − b ) ! C_{a}^{b} =\frac{a!}{b!\times(a-b)!} Cab=b!×(ab)!a!

  • 线筛法求出对于公式中最大的数a之前的所有的质数,之后在得到在a!的中所包含的每个质因子p对应的幂次
    a ! 中质因子 p 的幂次 = a p + a p 2 + . . . . . a p n ,向下取整 a!中质因子p的幂次=\frac{a}{p}+\frac{a}{p^2}+.....\frac{a}{p^n},向下取整 a!中质因子p的幂次=pa+p2a+.....pna,向下取整

  • 同理得到b!(a-b)!中对应p的质因子的幂次,然后a!中p的幂次-b!中p幂次-(a-b)!中p的幂次=结果中p的幂次,以此类推得到结果中每个质因子pi对应的幂次 最终结果 C a b = p 1 α 1 ∗ p 2 α 2 ∗ p 3 α 3 ∗ . . . . ∗ p k α k 最终结果C_{a}^{b}=p_{1}^{α1}*p_{2}^{α2}*p_{3}^{α3}*....*p_{k}^{αk} 最终结果Cab=p1α1p2α2p3α3....pkαk

  • 再对结果进行一个高精度的乘法得到最终结果

具体实现代码(详解版):

#include <iostream>
#include <algorithm>
#include <vector>using namespace std;const int N = 5010; 
int primes[N], cnt;
int sum[N]; // 存储每个质数的次数
bool st[N]; // 存储每个数是否被筛掉// 线性筛法求素数
void get_primes(int n) {for (int i = 2; i <= n; i++) {if (!st[i]) primes[cnt++] = i; // 如果当前数为质数,则存储// 标记合成数for (int j = 0; primes[j] <= n / i; j++) {st[primes[j] * i] = true; // 标记 primes[j] * i 为合成数if (i % primes[j] == 0) break; // 防止重复标记}}
}// 计算质因数的次数
int get(int n, int p) {int res = 0;while (n) {res += n / p; // 计算 n! 中 p 的次数n /= p; // 继续向下除以 p}return res;
}// 高精度乘法
vector<int> mul(vector<int> a, int b) {vector<int> c; // 存储乘法结果int t = 0; // 进位for (int i = 0; i < a.size(); i++) {t += a[i] * b; // 逐位乘以 bc.push_back(t % 10); // 当前位t /= 10; // 更新进位}// 处理最后的进位while (t) {c.push_back(t % 10);t /= 10;}return c; 
}int main() {int a, b;cin >> a >> b; get_primes(a); // 预处理范围内的所有质数// 求每个质因数的次数for (int i = 0; i < cnt; i++) {int p = primes[i];sum[i] = get(a, p) - get(b, p) - get(a - b, p);}vector<int> res; // 存储结果res.push_back(1); // 初始化高精度数为 1// 用高精度乘法将所有质因子相乘for (int i = 0; i < cnt; i++) {//遍历所有质因子for (int j = 0; j < sum[i]; j++) {res = mul(res, primes[i]); // 对应幂次循环}}for (int i = res.size() - 1; i >= 0; i--) cout << res[i];return 0; 
}

用到了之前的高精度乘法、线性筛法求素数等,所以难题都是可以分解的,每部分都是模板题。
卡特兰数
Acwing 889.满足条件的01序列
在这里插入图片描述
思路分析:

  • 将01序列置于坐标系中,起点定于原点。若0表示向右走,1表示向上走,那么任何前缀中0的个数不少于1的个数就转化为:路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。
  • 下图中,表示从(0,0)到(n,n)的路径,在绿线即以下表示合法,若触碰红线则不合法。
    在这里插入图片描述

由图可知,任何一条不合法的路径(如黑色路径),都对应一条从(0,0)走到(n-1,n+1)的一条路径(如灰色路径).而任何一条(0,0)走到(n-1,n+1)的路径,也对应了一条从(0,0)走到(n,n)的不合法路径。

假设有6个0,6个1,设0表示向右走一步,1表示向上走一步,则路径是从原点(0,0)到点(6,6),需要12步。根据题目要求前缀中0的个数大于1的个数,转化为所走路径要满足的条件就是:路径上的每一点的坐标都必须满足横坐标x>=纵坐标y,即路径必须在图中红色斜线(y=x+1)的下方,不能与红色斜线有交叉。目标点(6,6)关于红色斜线对称的点为(5,7),则只要是从原点(0,0)走到(5,7)的路径必然会与红线有交叉,即不符合路径的要求。
(0,0)–> (6,6)的路径方案数:C_{12}^{6} (总方案)

(0,0)–> (5,7)的路径方案数:C_{12}^{5} (不符合条件的方案)
故符合条件的方案数C_{12}^{6} - C_{12}^{5}
由以上推广到 n 个 0 和 n 个 1 ,得到公式 : C 2 n n − C 2 n n − 1 = C 2 n n n + 1 = ( 2 n ) ! ( n + 1 ) ! n ! ,即卡特兰数 由以上推广到n个0和n个1,得到公式:\\C_{2n}^{n}-C_{2n}^{n-1}=\frac{C_{2n}^{n}}{n+1}=\frac{(2n)!}{(n+1)!n!},即卡特兰数 由以上推广到n0n1,得到公式:C2nnC2nn1=n+1C2nn=(n+1)!n!(2n)!,即卡特兰数
注:这里结果依旧要对一个质数取模,所以可以用费马定理,快速幂求逆元

具体实现代码(详解版):

#include <iostream>
#include <algorithm>using namespace std;const int mod = 1e9 + 7;
typedef long long LL; // 快速幂
LL qmi(int a, int k, int p) {int res = 1; // 初始化结果为1int t = a; // 初始化当前基数while (k) { // 当 k 不为0if (k & 1) res = (LL)res * t % p; // 当 k 的当前位为1,乘入当前基数t = (LL)t * t % p; // 当前基数平方k >>= 1; // 右移 k,准备下一轮}return res; 
}// 用组合数基本公式求卡特兰数
LL Katelan(int n) {int a = 2 * n, b = n; // 2n 和 nLL res = 1; // 初始化结果for (int i = a, j = 1; j <= b; j++, i--) {res = (LL)res * i % mod; // 计算 (2n)! / n!res = (LL)res * qmi(j, mod - 2, mod) % mod; // 乘以 (n!) 的逆元}return (LL)res * qmi(n + 1, mod - 2, mod) % mod; // 乘以 (n+1) 的逆元
}int main() {int n;cin >> n; LL res = Katelan(n); cout << res << endl; return 0; // 程序结束
}

以上就是关于组合数的几种计算方法,我们要根据数据的大小范围确定使用哪种方法,对于最后的卡特兰数,就是组合数的一个应用。它是从一个01序列的问题推出来的,比较有技巧性。


http://www.mrgr.cn/news/40555.html

相关文章:

  • 第十九章(自定义类型:结构体)
  • 今日指数项目项目集成RabbitMQ与CaffienCatch
  • 【漏洞复现】泛微OA E-Office do_excel.php 任意文件写入漏洞
  • 编码能力提升计划 - 华为OD统一考试(E卷)
  • C++入门基础 (超详解)
  • Trilium Notes笔记本地化部署与简单使用指南打造个人知识库
  • Spring Boot与足球青训后台系统的协同
  • IPv4与TCP数据包结构解析
  • 计算机视觉(CV)技术的优势和挑战
  • 2024北京市赛 A.不要玩弄字符串
  • 【DCGAN 生成漫画头像】
  • 拥抱可持续创新,数据驱动未来——The Open Group 2024生态系统架构·可持续发展年度大会盛情邀约
  • ubuntu 24.04如何分配内存
  • 对个人来说,炒股有赚钱的吗,个人炒股真能赚到钱吗
  • TIM(Timer)定时器的原理
  • C语言进阶【8】--联合体和枚举(联合体和枚举这么好用,你不想了解一下吗?)
  • 为啥数据需转换成tensor才能参与后续建模训练
  • 基于JAVA+SpringBoot+Vue的社区养老服务平台
  • 力扣10.1
  • 点可云ERP进销存V8版本—其他入库单的操作使用及各单据状态说明