组合数学(四种求组合数的方法:递推,逆元,lucas,卡特兰数)

本文介绍了四种计算组合数的方法:递推法、逆元求法、Lucas定理和卡特兰数,重点讨论了如何通过预处理降低时间复杂度,以适应大规模数据处理。

求组合数,对于不同的数据量可以用不同的方法。实际上只用记住最高效的那个方法即可。本文将介绍四种求组合数的办法

递推求组合数

我们需要知道一个递推式。C_{a}^{b}\textrm{} = C_{a-1}^{b-1}\textrm{}+C_{a-1}^{b}\textrm{}

怎么记忆呢?

假如我们要求从a个苹果里选b个苹果,我们可以分成两种情况

1.包含a个苹果里的苹果i(ai),那么就是C_{a-1}^{b-1}\textrm{},因为已经选了ai,再选b-1个苹果即可

2.不包含ai,就是C_{a-1}^{b}\textrm{},需要在剩下的a-1个苹果里选b个苹果

用递推式预处理,时间复杂度就大大降低了

时间复杂度O(n^{2})

int ans[2010][2010];
int mod = 1e9 + 7;
void init() {
	for(int i = 1;i<=2005;i++){
		for (int j = 0; j <= i; j++) {
			if (i == j || j == 0) {
				ans[i][j] = 1;
			}
			else {
				ans[i][j] = (ans[i - 1][j] + ans[i - 1][j - 1]) % mod;
			}

		}
	}


}
signed main() {
	int n; cin >> n;
	init();
	while (n--) {
		int a, b; cin >> a >> b;
		cout << ans[a][b] << endl;
	}
	

	return 0;
}

逆元求组合数

可以看到数据变大了,再用第一种的O(n^{2})就过不了了

分析:时间复杂度O(n\log n)。预处理。

const int N = 1e6 + 10;
long long MAX(long long a, long long b) { return a < b ? b : a; }
long long MIN(long long a, long long b) { return a < b ? a : b; }
int qmi(int a, int k, int p) {
	int res = 1;
	while (k) {
		//后面的a其实是底数与其指数的运算结果了,是不断迭代的
		//第一个a其实就是a的2的0次方
		if (k & 1) res = (res * a) % p;

		a = (a * a) % p;
		//注意,a是一个不断变化的过程
		//下一个a就等于上一个a的平方,
		
		k >>= 1;
	}
	return res;
}

int mod = 1e9 + 7;
int fact[N], infact[N];

void init() {
	//注意!0的阶乘和逆元是1!
	fact[0] = 1; infact[0] = 1;

	for (int i = 1; i <= N; i++) {
		fact[i] = fact[i - 1] * i % mod;
		infact[i] = infact[i - 1] * qmi(i, mod - 2, mod) % mod;

	}

}

signed main() {
	init();
	int t; cin >> t;

	while (t--) {
		int a, b; cin >> a >> b;
		cout << fact[a] % mod * infact[b] % mod * infact[a - b] % mod << endl;
	}
	

	return 0;
}

有个疑惑,不能直接用定义求吗?毕竟数最大才1e5,算上快速幂的时间复杂度,

也只是O(n\log n)。

错!观察一下,最多有10000个测试样例,因此会超时,肯定需要预处理的

int C(int a, int b) {//这个就是根据定义求组合数
	int ans = 1;
	int j = a;
	for (int i = 1; i <= b; i++,j--) {
		ans = ans * j % mod;
		ans = ans * qmi(i, mod - 2, mod) % mod;
	}
	return ans;
}

Lucas定理求组合数

定理内容

#include<assert.h>
#include<cstdio>
#include<set>
#include<list>
#include<queue>
#include<math.h>
#include<stdlib.h>
#include<string>
#include<string.h>
#include <stdio.h>
#include<algorithm>
#include<iomanip>
#include<cmath>
#include<sstream>
#include<stack>
#include <utility>
#include<map>
#include <vector>

#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define inf 0x3f3f3f3f
//2147483647

#define int long long
//#include <bits/stdc++.h>
typedef long long ll;
#include<iostream>
using namespace std;

const int N = 1e5 + 10;
long long MAX(long long a, long long b) { return a < b ? b : a; }
long long MIN(long long a, long long b) { return a < b ? a : b; }
int qmi(int a, int k, int p) {
	int res = 1;
	while (k) {
		//后面的a其实是底数与其指数的运算结果了,是不断迭代的
		//第一个a其实就是a的2的0次方
		if (k & 1) res = (res * a) % p;

		a = (a * a) % p;
		//注意,a是一个不断变化的过程
		//下一个a就等于上一个a的平方,
		
		k >>= 1;
	}
	return res;
}

int mod = 1e5;

int C(int a, int b) {//这个就是根据定义求组合数
	int ans = 1;
	int j = a;
	for (int i = 1; i <= b; i++,j--) {
		ans = ans * j % mod;
		ans = ans * qmi(i, mod - 2, mod) % mod;
	}
	return ans;
}

int lucas(int a, int b) {
	if (a < mod && b < mod) {
		return C(a, b);
	}
                           //用递归是因为第一次a/mod可能会是1e15,还不够小
	return C(a % mod, b % mod) * lucas(a / mod, b / mod) % mod;

}

signed main() {
	int t; cin >> t;
	while (t--) {
		int a, b; cin >> a >> b >> mod;
		cout << lucas(a, b) << endl;
	}
	

	return 0;
}

卡特兰数求组合数

结合一道例题来理解知识点

分析:有n个0和n个1,排列成一个序列,使其任意前缀序列中0的个数不少于1的个数

我们可以画一张表格,0代表向右走一格,1代表向上走一格,从(0,0)出发,最后会走到(n,n)。从(0,0)到(n,n)的走法有C_{2n}^{n}\textrm{}。(从2n步里挑n步向上走)

我们想满足“前缀序列中0的个数不少于1的个数”,即cnt_0 >= cnt_1,代表“向右走的次数>=向上走的次数”,所以走的路径只能在红线以下,如果走到红线上以及其上方就是不合法的走法。

因此 合法的走法数 = 总走法数C_{2n}^{n}\textrm{} - 不合法的走法数。

不合法的走法数怎么求?这里有一个小规律:如果一个走法不合法,那么其路径一定会走到红线上方。这时候把红线上方的路径关于红线做轴对称,会发现终点在始终在(n-1,n+1)。

因此 从(0,0)到(n-1,n+1)的走法 = 不合法的走法 = C_{2n}^{n-1}\textrm{} = C_{2n}^{n+1}\textrm{} (挑n-1步向右走或挑n+1步向上走)

所以 ans = C_{2n}^{n}\textrm{} - C_{2n}^{n-1}\textrm{} =  \frac{C_{2n}^{n}\textrm{}}{n+1} (经过一系列推导得来)

int qmi(int a, int k, int p) {
	int res = 1;
	while (k) {
		//后面的a其实是底数与其指数的运算结果了,是不断迭代的
		//第一个a其实就是a的2的0次方
		if (k & 1) res = (res * a) % p;

		a = (a * a) % p;
		//注意,a是一个不断变化的过程
		//下一个a就等于上一个a的平方,
		
		k >>= 1;
	}
	return res;
}
int C(int a, int b,int p) {
	int ans = 1;
	int j = a;
	for (int i = 1; i <= b; i++,j--) {
		ans = ans * j % p;
		ans = ans * qmi(i, p - 2, p) % p;
	}
	return ans;
}
int lucas(int a, int b,int p) {
	if (a < p && b < p) {
		return C(a, b, p);
	}
	return C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;

}

const int mod = 1e9 + 7;
signed main()
{
	int n; cin >> n;
	cout << lucas(2 * n, n, mod) % mod * qmi(n + 1, mod - 2, mod) % mod;

	return 0;
}

第一章 引论 1.1 组合数学研究的对象 1.2 组合问题典型实例 1.2.1 分派问题 1. 2.2 染色问题 1.2.3 幻方问题 1.2.4 36军官问题 1.2.5 中国邮路问题 习 题 第二章 排列与组合 2.1 两个基本计原理 2.2 无重集的排列与组合 2.3 重集的排列与组合 2.4 排列生成算法 2.4.1 序法 2.4.2 字典序法 2.4.3 轮转法 2.5 组合生成算法 .2.6 应用举例 习 题 第三章 容斥原理 3.1 引 言 3.2 容斥原理 3.3 几个重要公式 3.4 错位排列 3.5 有限制的排列 3.6 棋阵多项式 3.7 禁位排列 习 题 第四章 鸽巢原理 4.1 鸽巢原理 4. 2 鸽巢原理的推广形式 4. 3 ramsey 4.4 ramsey的性质 4.5 ramsey定理 习 题 第五章 母函 5.1 母函概念 5.2 幂级型母函 5.3 整的拆分 5.4 ferrers图 5.5 指型母函 习 题 第六章 递归关系 6.1 引言 6.2 几个典型的递归关系.. 6.3 用母函方法解递归关系 6.4 常系线性齐次递归关系解 6.5 常系线性非齐次递归关系解 6.6 非常系非线性递归关系解 6.7 差分表法 6.8 stirling 习 题 第七章 polya定理 7.1 有限集的映射 7.2 群的基本概念 7.3 置换群 7.4 置换的奇偶性 7.5 置换群下的共轭类 7.6 burnside引理 7.7 polya定理 7.8 polya定理的母函型式 7.9 不标号图的计 习 题 第八章 图论基础 8.1 图的基本概念 8.2 同构图、完全图与二分图 8.3 通路、回路与图的连通性 8.4 euler图与hamilton图 8.5 割集与树 8.6 图的矩阵表示法 8.7 平面图、对偶图与色 8.8 匹配理论 8.9 网络流 习 题 第九章 拉丁方与区组设计 9.1 引言 9.2 拉丁方 9.3 有限域 9.4 正交拉丁方的构造 9.5 完全区组设计 9.6 平衡不完全区组设计(bibd) 9.7 区组设计的构造 9.8 steiner三连系 9.9 hadamard矩阵 习 题 第十章 线性规划 10.1 lp问题引例 10.2 lp问题的一般形式 10.3 lp问题的标准型 10.4 可行域和最优可行解 10.5 单纯形法 10.6 单纯形表格法 10.7 两阶段法 10.8 对偶原理 10.9 对偶单纯形法 10.10 应用举例 习 题 第十一章 组合优化算法与计算的时间复杂度理论 11.1 dijkstra算法 11.2 floyd算法 11.3 kruskal算法 11.4 最优树的破圈法和统观法 11.5 二分图中最大匹配与最佳匹配的算法 11.6 fleury算法 11.7 中国邮路问题及其算法 11.8 深度优先搜索法--dfs算法 11.9 项目网络与关键路径法 11.10 网络最大流算法 11.11 状态转移法 11.12 好算法、坏算法和np类问题 11.13 npc类问题 11.14 货郎问题的近似解 习 题... 参考文献
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值