数学 Trick 之:二项式反演+匹配滞后

能够解决的问题

形如:

  • 你需要给两个集合建立单射,并恰好\(k\)(给定)对满足性质 \(P\)(给定),求方案数。

优缺点

优点:代码简单,这类题有区分度。

缺点:泛用性不太广。

思路

  1. 考虑反演:放宽性质,将恰好变为至少
  2. dp 转移。

在转移中,有一个非常重要的技巧:匹配滞后。具体来说,有这两种转移方式(\(dp_{i, j}\) 表示前 \(i\) 和,匹配了 \(j\) 组了):

  • 什么都不做,直接沿用上一个状态,\(dp_{i, j} \gets dp_{i - 1, j}\)(你一定会感到疑惑,请先往下看)。
  • 匹配一组满足题目要求的性质的(具体见前文“能够解决的问题”),\(dp_{i, j} \gets dp_{i - 1, j - 1} + k\)\(k\) 为能够和 \(i\) 匹配的元素的个数)。

聪明的你,一定看出了端倪:

  • 在你做第一类转移时,这个 \(i\) 什么也没有匹配呀!
  • 你这样怎么保证答案的正确性呢?

我们在转移中,只匹配了满足性质的,又因为我们将将恰好变为了至少,所以其他的随便匹配就行(Trick 核心,请深刻理解)。

所以我们只需要做后一步操作:

\[dp_{i, j} \gets dp_{i, j} \times (i - j)! \]

我们就得到了答案,然后反演回去即可。

例题与代码

P4859 已经没有什么好害怕的了

这道题是这个 Trick 的典型应用,将题目中的 \(k\) 变成 \(\frac{n + k}{2}\) 即为这题的模板,照着思路做即可。

#include <bits/stdc++.h>
using namespace std;

#define int __int128
constexpr int maxn = 2010, modd = 1000000009;

int n, a[maxn], b[maxn], r[maxn], k, dp[maxn][maxn], fac[maxn], ans, c[maxn][maxn];

int p(int k) { // 反演系数
	return ((k & 1) ? -1 : 1);
}

int read() {
	int x = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') ch = getchar();
	while (ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + ch - 48, ch = getchar();
	return x;
}
void write(int x) {
	if (x < 0) putchar('-'), x = -x;
	if (x < 10) {
		putchar(x + 48);
	} else {
		write(x / 10);
		putchar(x % 10 + 48);
	}
	return ;
}

signed main() {
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	
	n = read();
	k = read();
	k = (n + k) >> 1;
	c[0][0] = 1;
	for (int i = 1; i <= n; i++) { // 组合数初始化
		c[i][0] = c[i][i] = 1;
		for (int j = 1; j < i; j++) {
			c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % modd;
		}
	}
	fac[0] = 1;
	for (int i = 1; i <= n; i++) { // 阶乘初始化
		fac[i] = i * fac[i - 1] % modd;
		a[i] = read();;
	}
	for (int i = 1; i <= n; i++) {
		b[i] = read();
	}
	
	sort(a + 1, a + 1 + n);
	sort(b + 1, b + 1 + n);
	for (int i = 1, j; i <= n; i++) { // r[i] 表示有多少个 b[i] 能与 a[i] 产生有贡献的匹配
		j = 0;
		while (a[i] > b[j + 1] && j < n) j++;
		r[i] = j;
	}
	
	dp[0][0] = 1;
	for (int i = 1; i <= n; i++) { // dp
		dp[i][0] = 1;
		for (int j = 1; j <= i; j++) {
			dp[i][j] = dp[i - 1][j] + (r[i] - j + 1) * dp[i - 1][j - 1] % modd;
			dp[i][j] %= modd;
		}
	}
	for (int i = 0; i <= n; i++) { // 将剩下的匹配
		dp[n][i] = dp[n][i] * fac[n - i] % modd;
	}
	
	for (int i = k; i <= n; i++) { // 反演
		ans = (ans + p(i - k) * c[i][k] * dp[n][i] % modd) % modd;
	}
	
	write((ans % modd + modd) % modd);
	
	return 0;
}
posted @ 2025-04-19 12:27  porse114514  阅读(18)  评论(0)    收藏  举报