「学习笔记」容斥原理及其应用

这篇博客介绍了容斥原理在解决计数问题中的应用,通过分析HDU和CDOJ等竞赛题目,讲解了如何利用容斥原理解决错排问题、数的整除性、互质数个数和硬币翻转等问题。同时,讨论了三重循环在处理多质因数容斥问题中的效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

容斥原理

说的是一种计数方式,使得每种情况只被记一次。

∑ i = 1 n C n i ( − 1 ) i + 1 = [ n ≥ 1 ] \sum_{i=1}^{n}C_n^i(-1)^{i+1}=[n\geq 1] i=1nCni(1)i+1=[n1]

入门例题

(1) HDU 1465 \text{(1) HDU 1465} (1) HDU 1465

错排问题模板题。

推一下递推式子:

n n n i ̸ = a i i\not = a_i i̸=ai当成 n n n个条件然后容斥,每次选一些不错位,剩下随便排.

d p [ n ] = ∑ i = 0 n ( − 1 ) i + 1 C n i ( n − i ) ! dp[n]=\sum_{i=0}^n (-1)^{i+1}C_{n}^{i}(n-i)! dp[n]=i=0n(1)i+1Cni(ni)!

d p [ n ] = ∑ i = 0 n ( − 1 ) i + 1 n ! i ! ( n − i ) ! ( n − i ) ! dp[n]=\sum_{i=0}^n (-1)^{i+1}\frac{n!}{i!(n-i)!}(n-i)! dp[n]=i=0n(1)i+1i!(ni)!n!(ni)!

d p [ n ] = ∑ i = 0 n ( − 1 ) i + 1 n ! i ! dp[n]=\sum_{i=0}^n (-1)^{i+1}\frac{n!}{i!} dp[n]=i=0n(1)i+1i!n!

d p [ n ] = ( − 1 ) n + ∑ i = 0 n − 1 ( − 1 ) i + 1 n ! i ! dp[n]=(-1)^n+\sum_{i=0}^{n-1} (-1)^{i+1}\frac{n!}{i!} dp[n]=(1)n+i=0n1(1)i+1i!n!

d p [ n ] = ( − 1 ) n + n ∑ i = 0 n − 1 ( − 1 ) i + 1 ( n − 1 ) ! i ! dp[n]=(-1)^n+n\sum_{i=0}^{n-1} (-1)^{i+1}\frac{(n-1)!}{i!} dp[n]=(1)n+ni=0n1(1)i+1i!(n1)!

d p [ n ] = ( − 1 ) n + n    d p [ n − 1 ] dp[n]=(-1)^n+n \;dp[n-1] dp[n]=(1)n+ndp[n1]

#include <cstdio>

long long dp[21];

int main() {
	dp[0] = dp[1] = 0;
	for(int i = 2; i <= 20; i ++)
		dp[i] = i * 1ll * dp[i - 1] + ((i & 1) ? -1 : 1);
	for(int n; ~ scanf("%d", &n); )
		printf("%lld\n", dp[n]);
	return 0;
}

(2) HDU 1796 \text{(2) HDU 1796} (2) HDU 1796

题意:给定 m ( m ≤ 10 ) m(m\leq 10) m(m10)个数 a i a_i ai,求小于 n n n的数中有多个数满足能被任意一个 a i a_i ai整除.

容斥应用入门题。 DFS \text{DFS} DFS容斥一下就好。即加上只被某1个整除的,减去被某2个整除的,加上…

#include <cstdio>

typedef long long LL;

int n, m, a[10];
LL ans;

LL gcd(const LL & a, const LL & b) {
	return b == 0 ? a : gcd(b, a % b);
}

LL lcm(const LL & a, const LL & b) {
	return a / gcd(a, b) * b;
}

void dfs(int k, int c, LL ret) {
	if(k == m) {
		if(c > 0) ans += ((n - 1) / ret) * ((c & 1) ? 1 : -1);
		return ;
	}
	dfs(k + 1, c, ret);
	if(a[k] > 0 && a[k] < n)
		dfs(k + 1, c + 1, lcm(ret, a[k]));
}

int main() {
	while(~ scanf("%d%d", &n, &m)) {
		for(int i = 0; i < m; i ++)
			scanf("%d", &a[i]);
		ans = 0;
		dfs(0, 0, 1);
		printf("%lld\n", ans);
	}
	return 0;
}

(3) HDU 4135 \text{(3) HDU 4135} (3) HDU 4135

题意:求 [ a , b ] [a,b] [a,b]中与 n n n互质的数, a , b ≤ 1 0 15 , n ≤ 1 0 9 a,b \leq 10^{15},n\leq 10^9 a,b1015,n109.

首先拆成前缀,转换为求 [ 1 , r ] [1,r] [1,r]中与 n n n互质的数。

这个不好求,补集转化,求为求 [ 1 , r ] [1,r] [1,r]中与 n n n不互质的数,然后用 r r r减去这个答案。

不互质意味着包含 n n n的某个质因子,把 n n n质因数分解为 ∏ i = 1 k p i c i \prod_{i=1}^{k} p_i^{c_i} i=1kpici。考虑一个质因子 p i p_i pi [ 1 , r ] [1,r] [1,r]中与 p i p_i pi不互质的数个数为 ⌊ r p i ⌋ \lfloor\frac{r}{p_i}\rfloor pir。这个会有重复,需要容斥。

举个例子, n = p 1 c 1 p 2 c 2 p 3 c 3 n=p_1^{c_1}p_2^{c_2}p_3^{c_3} n=p1c1p2c2p3c3,答案即为 r p 1 + r p 2 + r p 3 − r p 1 p 2 − r p 1 p 3 − r p 2 p 3 + r p 1 p 2 p 3 \frac{r}{p_1}+\frac{r}{p_2}+\frac{r}{p_3}-\frac{r}{p_1p_2}-\frac{r}{p_1p_3}-\frac{r}{p_2p_3}+\frac{r}{p_1p_2p_3} p1r+p2r+p3rp1p2rp1p3rp2p3r+p1p2p3r

#include <algorithm>
#include <cstdio>
using namespace std;

typedef long long LL;

const int N = 2e6 + 10; 

LL p[N], r, ans;
int tot;

void dfs(int k, int s, LL x) {
	if(k == tot + 1) {
		if(x ^ 1)
			ans += ((s & 1) ? 1 : -1) * (r / x);
		return ;
	}
	dfs(k + 1, s, x);
	dfs(k + 1, s + 1, x * p[k]);
}

LL solve(LL r, int n) {
	if((:: r = r) < 1) return 0;
	tot = 0;
	int s = sqrt(n) + 0.5;
	for(int i = 2; i <= s; i ++) {
		if(n % i == 0) {
			p[++ tot] = i;
			while(n % i == 0) n /= i;
		}
	}
	if(n > 1) p[++ tot] = n;
	ans = 0;
	dfs(1, 0, 1);
	return r - ans;
}

int main() {
	int T; scanf("%d", &T);
	for(int Case = 1, n; Case <= T; ++ Case) {
		LL a, b;
		scanf("%lld%lld%d", &a, &b, &n);
		printf("Case #%d: %lld\n", Case, solve(b, n) - solve(a - 1, n));
	}
	return 0;
}

容斥的实现可以 DFS \text{DFS} DFS也可以状压,以这题为例,演示一下状压写法。

#include <algorithm>
#include <cstdio>
using namespace std;

typedef long long LL;

const int N = 2e6 + 10; 

LL p[N], r, ans;
int tot;

void calc() {
	ans = 0;
	for(int i = 1; i < 1 << tot; i ++) {
		int cnt = 0;
		LL prod = 1;
		for(int j = 0; j < tot; j ++)
			if(i >> j & 1) ++ cnt, prod *= p[j];
		ans += ((cnt & 1) ? 1 : -1) * (r / prod);
	}
}

LL solve(LL r, int n) {
	if((:: r = r) < 1) return 0;
	tot = 0;
	int s = sqrt(n) + 0.5;
	for(int i = 2; i <= s; i ++) {
		if(n % i == 0) {
			p[tot ++] = i;
			while(n % i == 0) n /= i;
		}
	}
	if(n > 1) p[tot ++] = n;
	calc();
	return r - ans;
}

int main() {
	int T; scanf("%d", &T);
	for(int Case = 1, n; Case <= T; ++ Case) {
		LL a, b;
		scanf("%lld%lld%d", &a, &b, &n);
		printf("Case #%d: %lld\n", Case, solve(b, n) - solve(a - 1, n));
	}
	return 0;
}

(4) HDU 2204 \text{(4) HDU 2204} (4) HDU 2204

题意:求 [ 1 , n ] [1,n] [1,n]中能表示成 m k ( k &gt; 1 ) m^k(k&gt;1) mk(k>1)的数的个数, n ≤ 1 0 18 n\leq 10^{18} n1018

首先 m = 1 m=1 m=1单独算, 1 1 1个。考虑 m ≥ 2 m\geq2 m2

2 63 &gt; 1 0 18 2^{63}&gt;10^{18} 263>1018,因此 k ≤ 63 k\leq 63 k63

我们只需要考虑 k k k是质数的情况,因为 k k k不是质数的时候一定会在 k k k是质数的时候考虑到。

预处理出 63 63 63以内的质数:

{ 2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 , 31 , 37 , 41 , 43 , 47 , 53 , 59 } \{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59\} {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59}

考虑某个质数 p i p_i pi n n n以内能表示成 m p i m^{p_i} mpi的数有 ⌊ n p i ⌋ \lfloor\sqrt[p_i]{n}\rfloor pin 个(即 ⌊ n 1 p i ⌋ \lfloor n^{\frac{1}{p_i}}\rfloor npi1个)

每个质数的答案贡献的集合有可能相交,因此需要容斥

20 20 20个数做 2 20 2^{20} 220的容斥?

注意到其实 4 4 4个质数就已经没有交集了, 2 × 3 × 5 × 7 = 210 &gt; 63 2\times 3 \times 5 \times 7=210&gt;63 2×3×5×7=210>63

因此只需三重循环即可。

#include <algorithm>
#include <cstdio>
using namespace std;

typedef long long LL;

int p[20] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59};

int main() {
	for(LL n; ~ scanf("%lld", &n); ) {
		LL ans = 0, tmp;
		for(int i = 0; i < 20; i ++) {
			tmp = (LL) pow(n, 1.0 / p[i]);
			if(tmp < 1) break ;
			ans += tmp - 1;
			for(int j = i + 1; j < 20; j ++) {
				tmp = (LL) pow(n, 1.0 / (p[i] * p[j]));
				if(tmp < 1) break ;
				ans -= tmp - 1;
				for(int k = j + 1; k < 20; k ++) {
					tmp = (LL) pow(n, 1.0 / (p[i] * p[j] * p[k]));
					if(tmp < 1) continue ;
					ans += tmp - 1;
				}
			}
		}
		printf("%lld\n", ans + 1);
	}
	return 0;
}

推系数的容斥例题

(1) CDOJ 1544 \text{(1) CDOJ 1544} (1) CDOJ 1544

CDOJ 题目传送门Ifrog 备用传送门Vjudge 备用传送门UESTC 备用传送门

这是“玲珑杯”线上赛 Round \text{Round} Round # 17 17 17 河南专场 B \text{B} B 题。

题意:有 n n n个硬币,初始全部正面朝上,现在有 m m m次操作,每次把编号是 x x x的倍数的硬币翻面,
最后问多少个硬币正面朝上。 1 ≤ N ≤ 1 0 9 , 1 ≤ M ≤ 15 , 1 ≤ x ≤ 2 × 1 0 5 1\leq N \leq 10^9,1\leq M\leq 15,1\leq x\leq 2\times 10^5 1N1091M151x2×105

原问题转化为:给定 m m m个数 a 1 , . . . , a m a_1,...,a_m a1,...,am, 统计 [ 1 , n ] [1,n] [1,n]的整数中, 满足 a 1 , . . . , a m a_1,...,a_m a1,...,am 中有奇数个数整除它的数的个数。

即:

其他容斥题

(1) HDU 4336 \text{(1) HDU 4336} (1) HDU 4336

题意:有 n n n个卡牌,一包零食中有第 i i i张卡牌的概率为 p i p_i pi,求期望买多少包能收集到所有卡牌。

考虑单独一张卡牌 i i i,期望 E { i } = 1 p i E_{\{i\}}=\frac{1}{p_i} E{i}=pi1,这个比较好理解。

考虑两张卡牌 i , j i,j i,j,期望 E { i , j } = 1 p i + 1 p j − 1 p i + p j E_{\{i,j\}}=\frac{1}{p_i}+\frac{1}{p_j}-\frac{1}{p_i+p_j} E{i,j}=pi1+pj1pi+pj1。即收集 i , j i,j i,j的期望为收集 i i i的期望加收集 j j j的期望,但是存在情况就是考虑 i i i的时候虽然没收集到 i i i但收集到了 j j j,考虑 j j j的时候虽然没收集到 j j j但收集到了 i i i

三张以及以上直接容斥即可。

#include <cstdio>

int main() {
	double * p = new double [21];
	for(int n; ~ scanf("%d", &n); ) {
		for(int i = 0; i < n; i ++)
			scanf("%lf", &p[i]);
		double ans = 0;
		for(int s = 1; s < 1 << n; ++ s) {
			int cnt = -1; double a = 0;
			for(int i = 0; i < n; i ++) {
				if(s >> i & 1) {
					cnt *= -1;
					a += p[i];
				}
			}
			ans += cnt / a;
		}
		printf("%.4f\n", ans);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值