组合数学 基础

本文全面介绍了组合数学的基础概念与核心技巧,包括计数基础、排列组合、错排问题及容斥原理等内容,并通过实例深入浅出地解析了各种经典算法的应用场景。

组合数学知识点总结
组合数学全家桶
组合数学入门

计数基础

计数技巧,基础!!!

组合

可重组合

n n n 个元素,每种元素可以选择多次,一共选出 k k k 个,有多少种方法?
答案: C ( n − 1 + k , k ) = C ( n − 1 + k , n − 1 ) C(n-1+k,k)=C(n-1+k,n-1) C(n1+k,k)=C(n1+k,n1)
在这里插入图片描述
如果认为答案为 n k n^k nk 那么就是忽略了顺序,这样选择是有顺序的,但是题目要求的选出,是组合问题,显然 n k n^k nk 不对。
这个问题也等价于,把 k k k 个小球放到 n n n 个盒子,允许空盒
分析:设第 i i i 个盒子选 x i x_i xi 次,那么 x 1 + x 2 + . . . x n = k x_1+x_2+...x_n=k x1+x2+...xn=k
y i = x i + 1 y_i=x_i+1 yi=xi+1,那么方程转化成 y 1 + y 2 + . . . y n = k + n y_1+y_2+...y_n=k+n y1+y2+...yn=k+n
显然问题是等价的
如果不允许空盒存在,那么问题就可以用隔板法解决
k k k 个小球分成 n n n 份,相当于在 k − 1 k-1 k1 个空中选出 n − 1 n-1 n1 个即可,即 C k − 1 n − 1 C_{k-1}^{n-1} Ck1n1

不相邻组合

在这里插入图片描述

常用组合数公式

C n m = C n − 1 m + C n − 1 m − 1 C n m + 1 = n − m m + 1 ∗ C n m C n m = C n n − m C_n^m=C_{n-1}^m+C_{n-1}^{m-1} \\ C_n^{m+1}=\frac{n-m}{m+1}*C_n^m \\ C_n^m=C_n^{n-m} Cnm=Cn1m+Cn1m1Cnm+1=m+1nmCnmCnm=Cnnm

排列

可重排列

n^k

不尽相异元素全排列

在这里插入图片描述

一个结论

设有 p p p 个的元素是 x ( ∈ [ 0 , 9 ] ) x(\in [0,9]) x([0,9])
那么以 x x x 开头的排列,占总排列的 p n \frac{p}{n} np (排列中有 n n n 个数
一般用来删掉 0 0 0 开头的排列

错排问题

内容:考虑一个有 n n n 个元素的排列,若一个排列中所有的元素都不在自己原来的位置上,那么这样的排列就称为原排列的一个错排
递推公式:
D n = ( n − 1 ) ∗ ( D n − 1 + D n − 2 ) ( n > = 3 ) D 1 = 0 , D 2 = 1 D_n=(n-1)*(D_{n-1}+D_{n-2})(n>=3) \\ D_1=0,D_2=1 Dn=(n1)(Dn1+Dn2)(n>=3)D1=0,D2=1
n n n 最大到 20 20 20 左右不会爆 l l ll ll

圆排列

圆排列:围城一个圈的排列,如果两个排列可以通过旋转得到,那么两个排列就等价的
结论:
A n n n = ( n − 1 ) ! \frac{A_n^n}{n}=(n-1)! nAnn=(n1)!
为啥要除 n n n 呢,考虑加入最后一个数, n − 1 n-1 n1 个数有 n n n 个空位,无论最后一个数加到哪个位置,得到的都是一个排列

容斥原理

容斥原理内容很简单:就是按照奇加偶减的原则统计即可
容斥讲解
博主讲的很详细,还有例题讲解

一 般 问 题 描 述 中 的 不 是 倍 数 其 实 就 是 互 质 一般问题描述中的不是倍数其实就是互质

大水题
题意:给定 n n n,求 [ 1 , n ] [1,n] [1,n] 中不是 2 , 5 , 11 , 13 2,5,11,13 2,5,11,13 的倍数的数个数
思路:
首先要知道两个知识点
[ 1 , n ] [1,n] [1,n] x x x 的倍数的个数为: ⌊ n x ⌋ \lfloor \frac{n}{x} \rfloor xn
[ 1 , n ] [1,n] [1,n] 同时是 ( x 1 , x 2 , x 3 . . . , x n ) (x_1,x_2,x_3...,x_n) (x1,x2,x3...,xn) n n n 个数倍数的个数是: ⌊ n L c m ⌋ ( L c m = l c m ( x 1 , x 2 , x 3 , . . . , x n ) ) \lfloor \frac{n}{Lcm} \rfloor(Lcm=lcm(x_1,x_2,x_3,...,x_n)) Lcmn(Lcm=lcm(x1,x2,x3,...,xn))
容斥就好了
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e6 + 9;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
ll n, m;
int a[5] = {2, 5, 11, 13};

void work()
{
	while(cin >> n)
	{
		ll ans = n;
		for(int i = 1; i < (1 << 4); ++i){
			ll Lcm = 1, cnt = 0;
			for(int j = 0; j < 4; ++j) if(i & (1 << j))
			{
				++cnt;
				Lcm *= a[j];
			}
			if(cnt & 1) ans -= n / Lcm;
			else ans += n / Lcm;
		}
		cout << ans << endl;
	}
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

Co-prime
题意:
求区间 [ a , b ] [a,b] [a,b] 中与 n n n 互质的数的个数
思路:
直接求 [ a , b ] [a,b] [a,b] 区间内与 n n n 互质的不熟悉,但是统计区间 [ 1 , m ] [1,m] [1,m] 内与 n n n 互质,可以用容斥简单实现
先把 n n n 进行质因子分解,设 n = p 1 q 1 ∗ p 2 q 2 . . . p k q k n=p_1^{q_1}*p_2^{q_2}...p_k^{q_k} n=p1q1p2q2...pkqk
考虑用容斥统计 [ 1 , m ] [1,m] [1,m] 中与 n n n 不互质的
那么每次统计 [ 1 , m ] [1,m] [1,m] 中,含有质因子 p 1 p_1 p1 p 2 p_2 p2 . . . p k ...p_k ...pk 的,然后 m m m 减去,剩下的即为与 n n n 互质的
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll p[maxn], cnt, fac[maxn], tot;
bool vis[maxn];
ll _;
void xxs()
{
	for(int i = 2; i <= maxn - 9; ++i)
	{
		if(!vis[i]) p[++cnt] = i;
		for(int j = 1; j <= cnt && p[j] * i <= maxn - 9; ++j)
		{
			vis[p[j] * i] = 1;
			if(i % p[j] == 0) break;
		}
	}
}
void getfac(int n)
{
	tot = 0;
	for(int i = 1; p[i] * p[i] <= n && i <= cnt; ++i) if(n % p[i] == 0)
	{
		fac[++tot] = p[i];
		while(n % p[i] == 0) n /= p[i];
	}
	if(n != 1) fac[++tot] = n;
}
ll getans(ll m, ll num)
{
	ll ans = 0;
	for(ll i = 1; i < (1 << num); ++i)
	{
		ll sum = 0, tmp = 1;// 因为是枚举质因子,所以直接求乘积和求 lcm 是等价的
		for(ll j = 0; j < num; ++j) if(i & (1 << j))
		{
			++sum;
			tmp *= fac[j+1];// 注意 + 1 !!!!!
		}
		if(sum & 1) ans += m / tmp;
		else ans -= m / tmp;
	}
	return m - ans;
}
void work()
{
	ll a, b, n;
	cin >> a >> b >> n;
	getfac(n);
	ll ans = getans(b, tot) - getans(a - 1, tot);
	cout << "Case #" << ++_ << ": " << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
	xxs();
	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

Helping Cicada—lightoj–1117
题意:给定 m m m 个数,求 [ 1 , n ] [1,n] [1,n] 中不能被这 m m m 个数整除的数的个数
思路:
考虑用容斥统计区间内可以被 m m m 个数整除的数的个数(可以被 m m m 个数中任意一个整除即满足
二进制枚举选了 m m m 个数中的多少个
假设选了 x x x 个,可以整除这 x x x 个数的数的个数,为 n l c m ( a 1 , a 2 , . . . a x ) \frac{n}{lcm(a_1,a_2,...a_x)} lcm(a1,a2,...ax)n
ps:如果 a i a_i ai 都是质数,可以用除法直接正向容斥求答案,而不用 l c m lcm lcm
然后根据选择的个数 x x x,进行奇加偶减的原则计算即可
注意二进制枚举从 1 1 1 开始
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 2e6 + 9;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
ll n, m;
int a[maxn];
ll _;

ll lcm(ll a, ll b){// lcm求法
	return a / __gcd(a, b) * b;
}
void work()
{
	cin >> n >> m;
	for(int i = 1; i <= m; ++i) cin >> a[i];
	ll ans = 0;
	for(int i = 1; i < (1 << m); ++i)
	{
		ll Lcm = 1, cnt = 0;
		for(int j = 0; j < m; ++j) if(i & (1 << j))
		{
			++cnt;
			Lcm = lcm(Lcm, a[j + 1]);
		}
		if(cnt & 1) ans += n / Lcm;
		else ans -= n / Lcm;
	}
	cout << "Case " << ++_ << ": " << n - ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

2018—hdu-6286
题意:给定 a , b , c , d a,b,c,d a,b,c,d,求在 a < = x < = b , c < = y < = d a<=x<=b,c<=y<=d a<=x<=b,c<=y<=d 条件下,多少数对 ( x , y ) (x,y) (x,y) 满足 x ∗ y x*y xy 2018 2018 2018 的倍数
思路:
考虑容斥
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll a, b, c, d;
ll f1(ll x){// 返回区间 [a,b] 含有因子 x 的数的个数  
	return b / x - (a - 1) / x;
}
ll f2(ll x){
	return d / x - (c - 1) / x;
}
void work()
{
	ll ans = 0;
/*	ans += f1(2018) * (d - c + 1) + (b - a + 1) * f2(2018);//2018的倍数 * 任意数 && 任意数 * 2018的倍数
	ans += (f1(1009) - f1(2018)) * (f2(2) -  f2(2018));//是1009的倍数且不是2018的倍数 * 是2的倍数且不是2018的倍数
	ans += (f1(2) - f1(2018)) * (f2(1009) - f2(2018));//是2的倍数且不是2018的倍数 * 是1009的倍数且不是2018的倍数
	ans -= f1(2018) * f2(2018);//减掉上面少减的2018的倍数 *2018的倍数
*/
	ans += f1(1) * f2(2018);
	ans += f1(2) * (f2(1009) - f2(2018));
	ans += f1(1009) * (f2(2) - f2(2018));
	ans += f1(2018) * (f2(1) - f2(2) - f2(1009) + f2(2018));// 容斥 
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
	while(cin >> a >> b >> c >> d)
	work();
	return 0;
}

P1450 [HAOI2008]硬币购物
题意:初始给定 4 4 4 种硬币的面值,然后查询 n n n 次,每次带了 d i d_i di 个第 i i i 种硬币,想买 m m m 价值的东西,求付款方案数
思路:
首先看题目就是多重背包,能选择的物品数量有限制,但是这个问题直接用背包来解决复杂度太大了
比较妙的一个思路就是考虑容斥原理,用容斥统计超额的方案数
那么 有 限 制 的 方 案 数 = 无 限 制 的 方 案 数 − 超 过 限 制 的 方 案 数 有限制的方案数=无限制的方案数-超过限制的方案数 =
如果没有数量限制,那么显然就是完全背包,我们可以用完全背包预处理出价值为 i i i 时,没有限制的可选方案数 f [ i ] f[i] f[i]
首先考虑一种硬币超额使用的方案数怎么计算。
若第 j j j 种 硬币超额使用,我们可以先强制选了 d j + 1 d_j+1 dj+1 个第 j j j 种硬币,这样在剩下的价值里, 4 4 4 种硬币随便选,这样就能保证第 j j j 种硬币一定超额使用,第 j j j 种硬币超额的不合法方案就等于 f [ m − ( d j + 1 ) ∗ c j ] f[m-(d_j+1)*c_j] f[m(dj+1)cj]
把集合根据超额硬币种类,然后我们只需要容斥超额硬币种类数即可,总的方案数,减去一种硬币不合法方案数,加上两种硬币不合法方案数,…
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 1e5 + 9;
const int mod = 1e9 + 7;
ll n, m;
ll f[maxn], c[30], d[30];
void init()
{
	f[0] = 1;
	for(int i = 1; i <= 4; ++i)
		for(int j = c[i]; j <= maxn - 9; ++j)
			f[j] += f[j - c[i]];
}
void work()
{
	for(int i = 1; i <= 4; ++i) cin >> c[i];
	cin >> n;
	init();
	while(n--)
	{
		for(int i = 1; i <= 4; ++i)	cin >> d[i];
		cin >> m;
		ll ans = f[m];
		for(int i = 1; i < (1 << 4); ++i)
		{
			ll k = 0, now = 0;
			for(int j = 0; j < 4; ++j) if(i & (1 << j))
			{
				++k;
				now += (d[j + 1] + 1) * c[j + 1];// 注意 + 1 !!!!!!
			}
			if(m >= now)
				k & 1 ? ans -= f[m-now] : ans += f[m-now];// 超额的硬币占了now元,剩下的就随便选 
		}
		cout << ans << endl;
	}
}

int main()
{
	ios::sync_with_stdio(0);
	work();
	return 0;
}

Character Encoding
题意:
给定三个数 n , m , k n,m,k n,m,k,在 [ 0 , n − 1 ] [0,n-1] [0,n1] n n n 个数中选出 m m m 个,加和为 k k k m m m 个数可以重复选择,求有多少种排列。
思路:
可重复组合
考虑没有限制条件,在 [ 0 , n − 1 ] [0,n-1] [0,n1] 中选,那么可以转化为
x 1 + x 2 + x 3 + . . . + x m = k x_1+x_2+x_3+...+x_m=k x1+x2+x3+...+xm=k
隔板法显然答案为 C k + m − 1 m − 1 C_{k+m-1}^{m-1} Ck+m1m1
然后考虑到限制条件,我们可以容斥减去超过条件的部分
我们枚举有 i i i 个数超过了限制条件,即这 m m m 个数中,有 i i i 个大小超过了 n − 1 n-1 n1,那么这时的方案数为: C m i ∗ C k + m − 1 − i ∗ n m − 1 C_m^i * C_{k+m-1-i*n}^{m-1} CmiCk+m1inm1
对应的方程为:
x 1 ′ + x 2 ′ + . . . + x m ′ = k − i ∗ n x_1^{'}+x_2^{'}+...+x_m^{'}=k-i*n x1+x2+...+xm=kin
根据 i i i 个数,进行奇加偶减求出不合法的方案数,总数减去即可
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 2e5 + 9;
const int mod = 998244353;
ll n, m;
ll fac[maxn], inv[maxn];
void init()
{
	fac[0] = inv[0] = 1;
	fac[1] = inv[1] = 1;
	for(int i = 2; i <= maxn - 9; ++i)
		fac[i] = fac[i-1] * i % mod,
		inv[i] = (mod - mod / i) * inv[mod % i] % mod;
	for(int i = 1; i <= maxn - 9; ++i)
		inv[i] = inv[i] * inv[i-1] % mod;
}
ll C(ll n, ll m){
	if(m > n) return 0ll;
	return fac[n] * inv[m] % mod * inv[n-m] % mod;
}
void work()
{
	ll k;
	cin >> n >> m >> k;
	ll ans = C(k + m - 1, m - 1);
	ll sum = 0;
	for(int i = 1; i <= m; ++i)
	{
		if(i & 1)
			sum = (sum + C(m, i) * C(k + m - 1 - i * n, m - 1) % mod) % mod;
		else
			sum = (sum - C(m, i) * C(k + m - 1 - i * n, m - 1) % mod + mod) % mod;
	}
	ans = ((ans - sum) % mod + mod) % mod;
	cout << ans << endl;
}

int main()
{
	ios::sync_with_stdio(0);
	init();
	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

无关(relationship)
题意:求 [ l , r ] [l,r] [l,r] 中,对任意一个 x i x_i xi,都不是 x i x_i xi 的倍数的数的个数
思路:
水题,因为 x i x_i xi 都是质数,所以可以直接容斥求合法的
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 2e5 + 9;
const int mod = 998244353;
ll n, m;
ll a[30];
ll l, r, k;

ll f(ll x){
	ll sum = 0;
	for(int i = 0; i < (1ll << k); ++i)
	{
		ll cnt = 0, res = x;
		for(int j = 0; j < k; ++j) if(i & (1 << j))
			res /= a[j + 1], ++cnt;
		if(cnt & 1)// 含有一个的是不合法的,因为是直接求合法的,所以是减
			sum -= res;
		else sum += res; 		
	}
	return sum;
}

void work()
{
	cin >> l >> r >> k;
	for(int i = 1; i <= k; ++i) cin >> a[i];
	cout << f(r) - f(l - 1);
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

容斥求不合法的,然后总的减去
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
using namespace std;
const int maxn = 2e5 + 9;
const int mod = 998244353;
ll n, m;
ll a[30];
ll l, r, k;
ll f(ll x){
	ll sum = 0;
	for(int i = 1; i < (1ll << k); ++i)
	{
		ll Lcm = 1, cnt = 0;
		for(int j = 0; j < k; ++j) if(i & (1 << j))
		{
			Lcm *= a[j + 1];
			++cnt;
			if(Lcm > r) break;// Lcm 已经大于 r了,后边 x/Lcm 一定是0了,而且继续乘会爆 ll 
		}
		if(cnt & 1) 
			sum += x / Lcm;
		else sum -= x / Lcm; 		
	}
	return x - sum;
}

void work()
{
	cin >> l >> r >> k;
	for(int i = 1; i <= k; ++i) cin >> a[i];
	cout << f(r) - f(l - 1);
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

CCA的小球
题意:
给定 n   ( < 1 0 6 ) n \ (<10^6) n (<106) 个小球,每个小球有颜色,要将它们摆成一行 。
两个方案不同,当且仅当存在某个位置,两种方案摆在这个位置的小球颜色不同。
一个方案合法, 当且仅当不存在任意两个位置相邻的小球颜色相同,求合法方案数对 1 0 9 + 7 10^9+7 109+7 取模后的值 。
每种颜色的小球至多有两个
思路:
每种颜色的小球至多有两个,也就是说一种颜色的小球只有简单的相邻不相邻两种很好区分的情况
颜色只有一个的小球无影响
考虑容斥, 0 0 0 个相同颜色的小球相邻 减去 1 1 1 个相同颜色的小球相邻 加上 …
把相邻的小球看作一个,那么一共还有 n − i n-i ni 个小球
∑ i = 0 c n t C c n t i ∗ ( n − i ) ! 2 t o t − i \sum_{i=0}^{cnt}C_{cnt}^i * \frac{(n-i)!}{2^{tot-i}} i=0cntCcnti2toti(ni)!
因为颜色相同的小球交换位置不会使方案数增多
因此不相邻的 t o t − i tot-i toti 个小球会使得方案数增多,除以 t o t − i tot-i toti A 2 2 A_2^2 A22 即可
code:

#include<bits/stdc++.h>
#define endl '\n'
#define ll long long
#define ull unsigned long long
#define ld long double
#define all(x) x.begin(), x.end()
#define eps 1e-6
using namespace std;
const int maxn = 1e6 + 9;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
ll n, m;
int a[maxn];
ll fac[maxn], inv[maxn];

void init(){
	fac[0] = inv[0] = 1;
	fac[1] = inv[1] = 1;
	for(int i = 2; i <= maxn - 9; ++i)
		fac[i] = fac[i-1] * i % mod,
		inv[i] = (mod - mod / i) * inv[mod % i] % mod;
	for(int i = 2; i <= maxn - 9; ++i)
		inv[i] = inv[i-1] * inv[i] % mod;
}
ll C(int n, int m){
	return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
ll q_pow(ll a, ll b, ll ans = 1){
	while(b){
		if(b & 1) ans = ans * a % mod;
		b >>= 1;
		a = a * a % mod;
	}return ans;
}
void work()
{
	init();
	cin >> n;
	for(int i = 1; i <= n; ++i){
		cin >> a[i];
	}
	sort(a + 1, a + 1 + n);
	int cnt = 0;
	for(int i = 2; i <= n; ++i) if(a[i] == a[i-1]) ++cnt;
	ll ans = 0;
	for(int i = 0; i <= cnt; ++i){
		ll sum = C(cnt, i) * fac[n-i] % mod * q_pow(q_pow(2, cnt - i), mod - 2) % mod;
		if(i & 1) (ans -= sum);
		else ans += sum;
		(ans += mod) %= mod;
	}
	cout << ans;
}

int main()
{
	ios::sync_with_stdio(0);
//	int TT;cin>>TT;while(TT--)
	work();
	return 0;
}

D. Training Session

题解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值