[算法] 容斥

对于某些毒瘤计数题,经常会出现统计重复或遗漏的问题,这时候就可能需要容斥一下

容斥原理

先从一个经典的例子入手:有三个学科,设为 S1,S2,S3,有一堆人选不同的学科,现已知选每门学科各自有多少人选,求一共有多少人选学科;

根据题意,我们要求的就是:S1S2S3

考虑咋求,这是一个小学数学问题,直接用 |S1|+|S2|+|S3||S1S2||S2S3||S1S3|+|S1S2S3| 算一下即可;

其实这就是一个容斥;

扩展一下,将 S 抽象为若干个集合,对于 S1,S2,...,Sn,我们要求 |i=1nSi|,那么我们可以得到:

|i=1nSi | = i|Si|i<j|SiSj|+i<j<k|SiSjSk|...

这就是容斥原理

简记为:奇加偶减

对于其补集同理,有:

|i=1nSi | = i|Si|i<j|SiSj|+i<j<k|SiSjSk|...

那么,对于集合的交,我们不难得到:

|i=1nSi|=|U||i=1nSi|

其中 U 代表全集;

对于右者使用容斥原理即可;

这就是比较常用的三个公式(其实都差不多);

应用

不定方程非负整数解计数

这是本篇文章主要研究的问题;

问题: 给出不定方程 i=1nai=m 以及 n 个形如 aibi 的限制,求合法非负整数解的个数;

没有限制

我们看作有 n 个盒子,各个盒子放的球数就是对应一位 a 的值,有 m 个相同小球,盒子可以为空,求将小球放入盒子中的方案数;

如果每个盒子至少有一个球的话,那么我们可以用插板法来做,对于可以为空的情况,我们可以再加 n 个球依次放入每个盒子中,那么问题就转化成有 n+m 个小球,放入 n 个盒子中,用插板法做就是 Cn+m1n1

扩展一下,对于形如 i=1nai=m 以及 n 个形如 aibi 的限制的问题,我们可以先把对应位置放上其对应的 b,那么依据上述思路答案为:

Cn+mi=1nbi1n1

其实上述问题就是 i[1,n],bi=0 的特殊情况;

有限制

发现我们按照没有限制做会算多 aibi+1 的情况,那么我们需要容斥掉它;

考虑刚刚容斥原理中的第三个公式,答案可表示为:

|i=1nSi|=|U||i=1nSi|

其中对于 |i=1nSi|,(就是 aibi+1)我们应用第二个公式展开一下,得到:

|i=1nSi | = i|Si|i<j|SiSj|+i<j<k|SiSjSk|...

随便提出一项 |SiSjSk|,我们考虑它的实际意义,为 i=1nai=m 以及 3 个形如 aibi+1, ajbj+1 akbk+1 的限制,求其方案数,依据上面的没有限制的思路,可得答案为:

Cn+m(bi+1)(bj+1)(bk+1)1n1×Cn3

其他项同理;

例题

image

可以看成 01 分成了 nm+1 个段(当然有的段可以为 0);

所以这题就是要对于每个 k[1,m] 求出 i=1nm+1ai=mmaxi=1nm+1ai=k 的非负整数解个数,其实跟上面的问题很相像;

我们把原问题拆解成两个子问题:小于等于 k 的减去小于等于 k1

对于小于等于 k 的子问题,应用上面的思路很容易求解;

最后做个差分即可;

时间复杂度:依据调和级数是 O(mlogm) 的;

点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
const long long mod = 1e9 + 7;
int n, m;
long long ksm(long long a, long long b) {
	long long ans = 1;
	while(b) {
		if (b & 1) ans = ans * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return ans;
}
long long fac[500005], fav[500005];
long long C(long long a, long long b) {
	if (b < 0 || a < 0) return 0;
	if (a == b) return 1;
	if (a < b) return 0;
	if (b == 0) return 1;
	return fac[a] * fav[b] % mod * fav[a - b] % mod;
}
int a[500005];
int main() {
	freopen("a.in", "r", stdin);
	freopen("a.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	fac[0] = 1;
	fav[0] = 1;
	for (int i = 1; i <= 500000; i++) {
		fac[i] = fac[i - 1] * i % mod;
		fav[i] = ksm(fac[i], mod - 2);
	}
	for (int k = 1; k <= m; k++) {
		long long ans = 0;
		long long su = 0;
		for (int i = 0; i <= m / (k + 1); i++) {
			ans = (ans + ((i & 1) ? -1ll : 1ll) * C(n - m + m - i * (k + 1), n - m) % mod * C(n - m + 1, i) % mod) % mod;
		}
		a[k] = ans;
	}
	for (int i = m; i >= 1; i--) {
		a[i] = (a[i] - a[i - 1] % mod + mod) % mod;
	}
	for (int i = 1; i <= m; i++) {
		cout << (a[i] + mod) % mod << ' ';
	}
	return 0;
}
posted @   Peppa_Even_Pig  阅读(101)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示