数论——容斥原理、莫比乌斯函数

数论——容斥原理、莫比乌斯函数

1、容斥原理:

  • 时间复杂度为 O ( 2 N ) O(2^N) O(2N),下面会有证明。
  • 举一个简单的例子:用韦恩图来思考,求 S 1 S1 S1 S 2 S2 S2 S 3 S3 S3三个集合的原有元素的并集,那么结果为: S 1 + S 2 + S 3 − S 1 ∩ S 2 − S 1 ∩ S 3 − S 2 ∩ S 3 + S 1 ∩ S 2 ∩ S 3 S1+S2+S3-S1 \cap S2-S1 \cap S3-S2 \cap S3+S1 \cap S2 \cap S3 S1+S2+S3S1S2S1S3S2S3+S1S2S3
  • 以此类推到 N N N个圆的交集:用容斥原理的方法答案为所有单个集合的元素个数-所有两个集合互相交集的元素个数+所有三个集合互相交集的元素个数…
  • 我们知道容斥原理公式一共涉及到的元素个数为: C N 1 + C N 2 + C N 3 + . . . + C N N C_N^1+C_N^2+C_N^3+...+C_N^N CN1+CN2+CN3+...+CNN。因为 C N 0 + C N 1 + C N 2 + C N 3 + . . . + C N N = 2 n C_N^0+C_N^1+C_N^2+C_N^3+...+C_N^N=2^n CN0+CN1+CN2+CN3+...+CNN=2n,因此 C N 1 + C N 2 + C N 3 + . . . + C N N = 2 n − 1 C_N^1+C_N^2+C_N^3+...+C_N^N=2^n-1 CN1+CN2+CN3+...+CNN=2n1,因此容斥原理公式一共涉及到的元素个数为 2 n − 1 2^n-1 2n1。关于此公式( C N 0 + C N 1 + C N 2 + C N 3 + . . . + C N N = 2 n C_N^0+C_N^1+C_N^2+C_N^3+...+C_N^N=2^n CN0+CN1+CN2+CN3+...+CNN=2n)的证明,我们可以假设等号左边为对于 N N N个物品所有选法的总个数,等号右边考虑每个物品选与不选两种情况,因此等式成立。
  • 因此容斥原理的时间复杂度为 O ( 2 N ) O(2^N) O(2N)
  • 容斥原理的证明:对于容斥原理 ∣ S 1 ∪ S 2 ∪ . . . ∪ S N ∣ = ∑ i = 1 N S i − ∑ i , j N S i ∩ S j + ∑ i , j , k N S i ∩ S j ∩ S k + . . . |S1 \cup S2 \cup ... \cup SN|=\sum_{i=1}^N{Si}-\sum_{i, j}^N{Si \cap Sj}+\sum_{i, j, k}^N{Si \cap Sj \cap Sk}+... S1S2...SN=i=1NSii,jNSiSj+i,j,kNSiSjSk+...
    对于一个元素 x x x,它在 k k k个集合中, 1 ≤ k ≤ N 1 \leq k \leq N 1kN,它本身被选择的次数为 C k 1 − C k 2 + C k 3 − . . . + ( − 1 ) k − 1 C k k C_k^1-C_k^2+C_k^3-...+(-1)^{k-1}C_k^k Ck1Ck2+Ck3...+(1)k1Ckk。我们知道一个结论: C k 1 − C k 2 + C k 3 − . . . + ( − 1 ) k − 1 C k k = 1 C_k^1-C_k^2+C_k^3-...+(-1)^{k-1}C_k^k=1 Ck1Ck2+Ck3...+(1)k1Ckk=1,因此对于每一个元素 x x x,它只被计算了 1 1 1次,证毕。

例题:AcWing 890. 能被整除的数

给定一个整数 n n n m m m个不同的质数 p 1 , p 2 , … , p m p1,p2,…,pm p1,p2,,pm。请你求出 1 1 1 n n n中能被 p 1 , p 2 , … , p m p1,p2,…,pm p1,p2,,pm中的至少一个数整除的整数有多少个。

首先我们知道,在 N N N个数中能被 x x x整除的数的个数为 ⌊ N / x ⌋ \lfloor{N/x}\rfloor N/x

因此我们只需要根据容斥原理,求出可以被单个元素整除的个数之和-可以被两个元素整除的个数之和+可以被三个元素整除的个数之和…我们用位运算来求得答案,时间复杂度为 O ( 2 N ) O(2^N) O(2N)

#include <bits/stdc++.h>
#define int long long
using namespace std;
int p[20];

void work() {
	int n, m;
	cin >> n >> m;
	for (int i = 0; i < m; i++) cin >> p[i];
	
	int res = 0;
	for (int i = 1; i < 1 << m; i++) {
		int t = 1, s = 0;
		for (int j = 0; j < m; j++)
			if (i >> j & 1) {
				if (t * p[j] > n) {
					t = -1;
					break;
				} 
				t *= p[j];
				s++;
			}
		if (t != -1) {
			if (s % 2) res += n / t;
			else res -= n / t;
		}
	}
	cout << res << endl;
}

int32_t main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int cas;
	cas = 1;
	while (cas--) work();
}

例题:AcWing 214. Devu和鲜花

N N N个盒子,第 i i i个盒子中有 A i Ai Ai枝花。同一个盒子内的花颜色相同,不同盒子内的花颜色不同。要从这些盒子中选出 M M M枝花组成一束,求共有多少种方案。若两束花每种颜色的花的数量都相同,则认为这两束花是相同的方案。结果需对 1 0 9 + 7 10^9+7 109+7取模之后方可输出。

我们先考虑:从 N N N个盒子选 M M M枝花,每个盒子的花的个数为无限个,问一共能选多少枝花?

此问题等价于从 N N N个盒子选 M + N 枝 M+N枝 M+N花,那么每个盒子至少选 1 1 1枝。那么此问题由等价于把 N + M N+M N+M个点分成 N N N份,我们可以用隔板法来做,一共有 N + M − 1 N+M-1 N+M1个空隙,有 N − 1 N-1 N1个板子,因此答案为 C N + M − 1 N − 1 C_{N+M-1}^{N-1} CN+M1N1

拓展到此问题,第 i i i个盒子中有 A i Ai Ai枝花。那么我们可以反过来考虑,用总共的答案 C N + M − 1 N − 1 C_{N+M-1}^{N-1} CN+M1N1减去其中第 i i i个盒子被拿走了大于 A i Ai Ai枝花的方案。第 i i i个盒子被拿走了大于 A i Ai Ai枝花的方案数为:假设此盒子已经被拿走了 A i + 1 Ai+1 Ai+1枝花,那么等价于前面的问题,从 N N N个盒子中共拿走 M − A i − 1 M-Ai-1 MAi1枝花的方案数,等价于从N个盒子拿走 M − A i − 1 + N M-Ai-1+N MAi1+N的方案数,每个盒子至少被拿 1 1 1枝,因此答案为 C M − A i − 1 + N − 1 N − 1 C_{M-Ai-1+N-1}^{N-1} CMAi1+N1N1

根据容斥原理来做,可知答案为总共的 C N + M − 1 N − 1 C_{N+M-1}^{N-1} CN+M1N1减去所有 1 1 1个盒子不满足的加上所有 2 2 2个盒子不满足的减去所有 3 3 3个盒子不满足的…

#include <bits/stdc++.h>
#define int long long
using namespace std;
int A[20];
constexpr int mod = 1e9 + 7;
int down = 1;

int qmi(int a, int b, int p) {
	int res = 1;
	while (b) {
		if (b & 1) res = res * a % p;
		a = a * a % p;
		b >>= 1;
	}
	return res;
}

int C(int a, int b) {
	if (a < b) return 0;
	int up = 1;
	for (int i = a; i > a - b; i--) up = i % mod * up % mod;
	return up * down % mod;
}

void work() {
	int n, m;
	cin >> n >> m;
	for (int i = 0; i < n; i++) cin >> A[i];
	for (int j = 1; j <= n - 1; j++) down = j * down % mod;
	down = qmi(down, mod - 2, mod);
	
	int res = 0;
	for (int i = 0; i < 1 << n; i++) {
		int a = n + m - 1, b = n - 1;
		int sign = 1;
		for (int j = 0; j < n; j++)
			if (i >> j & 1) {
				sign *= -1;
				a -= A[j] + 1;
			}
		res = (res + C(a, b) * sign) % mod;
 	}
 	cout << (res % mod + mod) % mod << endl;
}

int32_t main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int cas;
	cas = 1;
	while (cas--) work();
}

2、莫比乌斯函数:


我们举例一道经典的应用题,求 1 1 1 N N N中与 a a a互质的数的个数:那么根据容斥原理,设 S i S_i Si 1 1 1 N N N中和 a a a有公因子i的数的个数,答案为 N − S 2 − S 3 − S 5 − S 7 . . + S 2 , 3 + S 3 , 5 + S 2 , 5 + . . . N-S_2-S_3-S_5-S_7..+S_{2,3}+S_{3,5}+S_{2,5}+... NS2S3S5S7..+S2,3+S3,5+S2,5+...,我们可以惊奇的发现,其答案为 ∑ i = 0 N u ( i ) ∗ S i \sum_{i=0}^{N}{u(i)*S_i} i=0Nu(i)Si

我们可以根据线性筛质数在 O ( N ) O(N) O(N)的时间内算出前 N N N个数的莫比乌斯数。

int primes[N], cnt;
bool st[N];
int mobius[N];

void init(int n) {
	mobius[1] = 1;
	for (int i = 2; i <= n; i++) {
		if (!st[i]) {
			primes[cnt++] = i;
			mobius[i] = -1;
		}
		for (int j = 0; primes[j] * i <= n; j++) {
			int t = primes[j] * i;
			st[t] = true;
			if (i % primes[j] == 0) {
				mobius[t] = 0;
				break;
			}
			mobius[t] = mobius[i] * -1;
		}
	}
}

AcWing 215. 破译密码

对于给定的整数 a a a b b b d d d,有多少正整数对 x x x y y y,满足 x ≤ a x \leq a xa y ≤ b y\leq b yb,并且 g c d ( x , y ) = d gcd(x, y)=d gcd(x,y)=d 5 e 4 5e4 5e4组询问, a 、 b a、b ab的数据范围为 5 e 4 5e4 5e4

根据数据的范围,我们可以推断出时间复杂度为 O ( n n ) O(n\sqrt n) O(nn )

每次询问的问题等价于:有多少正整数对 x x x y y y,满足 x ≤ a / d x \leq a/d xa/d y ≤ b / d y\leq b/d yb/d,并且 g c d ( x , y ) = 1 gcd(x, y)=1 gcd(x,y)=1。那么根据容斥原理:值为 m i n ( a / d , b / d ) − S 2 − S 3 − S 5 + S 2 , 3 + S 2 , 5 + S 3 , 5 − S 2 , 3 , 5 . . . min(a/d, b/d)-S_2-S_3-S_5+S_{2,3}+S_{2,5}+S_{3,5}-S{2,3,5}... min(a/d,b/d)S2S3S5+S2,3+S2,5+S3,5S2,3,5...,其答案为 ∑ i = 0 m i n ( a / d , b / d ) u ( i ) ∗ S i \sum_{i=0}^{min(a/d, b/d)}{u(i)*S_i} i=0min(a/d,b/d)u(i)Si,也就是 ∑ i m i n ( a , b ) = a / i ∗ b / i ∗ u [ i ] \sum^{min(a,b)}_i=a/i∗b/i∗u[i] imin(a,b)=a/ib/iu[i]。我们可以推断出时间复杂度为 O ( n ) O(n) O(n)

考虑把上述过程优化,发现,这个式子中虽然i要枚举 N N N次,但是实际上因为整除的原因 a i ai ai的值很少,只有 2 a 2\sqrt a 2a 个。

因为 a / 1 、 a / 2 、 a / 3 、 … a/1 、a/2、a/3、… a/1a/2a/3是单调递减的,并且有的值相同,所以整个序列一共有有 2 a 2\sqrt a 2a 个值。(证明:在分母为 1 1 1 a \sqrt a a 之间,值的个数为 a / a a / \sqrt a a/a 个值,在 a + 1 \sqrt a +1 a +1 n n n之间,值的个数为 a / a a / \sqrt a a/a 个值)。

g ( x ) g(x) g(x)表示 a / x a/x a/x的取值不变的最大的 x x x值,那么 a / x = a / g ( x ) a/x=a/g(x) a/x=a/g(x),并且 a / x > a / ( g ( x ) + 1 ) a/x>a/(g(x)+1) a/x>a/(g(x)+1),其中 g ( x ) = a / ( a / x ) g(x)=a/(a/x) g(x)=a/(a/x)

证明 a / x = a / g ( x ) a/x=a/g(x) a/x=a/g(x)

证明: g ( x ) = a / ( a / x ) g(x)=a/(a/x) g(x)=a/(a/x)

综上:将原来的序列分成 2 a 2\sqrt a 2a 段,而且每次都会跳一段,所以总共会跳 2 a 2\sqrt a 2a 次,时间复杂度就是 O ( a ) O(\sqrt a) O(a )

加上询问后总的时间复杂度为 O ( n n ) O(n\sqrt n) O(nn )

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 50010;

int primes[N], cnt;
bool st[N];
int mobius[N], sum[N];

void init(int n) {
	mobius[1] = 1;
	for (int i = 2; i <= n; i++) {
		if (!st[i]) {
			primes[cnt++] = i;
			mobius[i] = -1;
		}
		for (int j = 0; primes[j] * i <= n; j++) {
			int t = primes[j] * i;
			st[t] = true;
			if (i % primes[j] == 0) {
				mobius[t] = 0;
				break;
			}
			mobius[t] = mobius[i] * -1;
		}
	}
	for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + mobius[i];
}

void work() {
	int a, b, d;
	cin >> a >> b >> d;
	a /= d, b /= d;
	int n = min(a, b);
	ll res = 0;
	for (int l = 1, r; l <= n; l = r + 1) {
		r = min(n, min(a / (a / l), b / (b / l)));
		res += (sum[r] - sum[l - 1]) * (ll)(a / l) * (b / l);
	}
	cout << res << endl;
}

int32_t main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	init(N - 1);
	int cas;
	cin >> cas;
	while (cas--) work();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值