前言
在 OI 中,所谓容斥思想,很多时候就是面对题目中的强性质不好直接表示时,先表示出一个易求的弱性质,再去考虑用强性质表示出弱性质,通过反演来得到用弱性质表示的强性质。
对于不同式子的反演方法各不相同,类似的变化也比较多样,下文将结合例题来探讨各种不同反演的应用。
- 前言
- 二项式反演
- 基本思想
- 二项式反演
- 证明
- 推广
- 应用
- BZOJ 2839 集合计数
- Luogu P4859 已经没有什么好害怕的了
- 二项式反演
二项式反演
基本思想
记 \(f_n\) 表示恰好使用 \(n\) 个不同元素形成的特定结构的方案数,\(g_n\) 表示从 \(n\) 个不同元素中选出若干个元素形成特定结构的总方案数。显然这里的 \(g_n\) 性质是弱于 \(f_n\) 的,因为 \(g_n\) 没有限制选出元素形成特定结构的元素个数。
若已知 \(f_n\) 求 \(g_n\),考虑枚举 \(g_n\) 中元素的个数 \(i\),如果从 \(n\) 个元素中选 \(i\) 个,每种情况的方案数就是 \(f_i\),不难得到:
若已知 \(g_n\) 求 \(f_n\),我们对上式使用容斥,得到:
上面容斥式子的本质就是二项式反演。
二项式反演
二项式反演的基本形式即
这个式子还有一个等价的对称形式
证明
我们考虑基本形式的证明,其它形式的数值证明是类似的。
对于 \(f_n\) 的式子,我们先把其中的 \(g_i\) 展开,有
交换求和次序,先枚举 \(j\),再考虑枚举每个 \(i\ge j\),有
运用三项式版恒等式,再把与 \(i\) 无关的项提到前面,有
令 \(k=i-j\),枚举 \(k\) 代替枚举 \(i\),剩下的式子就是 \((1-1)^{n-j}\) 的二项式定理展开,故有
推广
从线性代数的角度来看,如果把 \(f_0,f_1,\cdots,f_n\) 和 \(g_0,g_1,\cdots,g_n\) 写成列向量,则二项式反演可以写为
\(
\begin{gather*}\underbrace{\begin{pmatrix}\binom{0}{0}&0&\cdots&0\\\binom{1}{0}&\binom{1}{1}&\cdots&0\\\vdots&\vdots&\ddots&\vdots\\\binom{n}{0}&\binom{n}{1}&\cdots&\binom{n}{n}\end{pmatrix}}_{\boldsymbol A}\begin{pmatrix}f_0\\f_1\\\vdots\\f_n\end{pmatrix}=\begin{pmatrix}g_0\\g_1\\\vdots\\g_n\end{pmatrix}\iff\underbrace{\begin{pmatrix}\binom{0}{0}&0&\cdots&0\\-\binom{1}{0}&\binom{1}{1}&\cdots&0\\\vdots&\vdots&\ddots&\vdots\\(-1)^{n}\binom{n}{0}&(-1)^{n-1}\binom{n}{1}&\cdots&\binom{n}{n}\end{pmatrix}}_{\boldsymbol B}\begin{pmatrix}g_0\\g_1\\\vdots\\g_n\end{pmatrix}=\begin{pmatrix}f_0\\f_1\\\vdots\\f_n\end{pmatrix}\end{gather*}
\)
所以上文的证明等价于证明 \(\boldsymbol A\cdot\boldsymbol B=\boldsymbol I\)。
将 \(\boldsymbol A\) 和 \(\boldsymbol B\) 转置之后得到的矩阵仍然是互逆的,所以我们还可以得到二项式反演的矩阵转置形式:
从组合意义来讲,这里的 \(g_m\) 是“至少”包含 \(m\) 个元素的方案数,与上文“至多”包含 \(m\) 个元素的方案的 \(g_m\) 不相类似。
在实际运用里,要灵活选用反演式子来解决问题。
应用
BZOJ 2839 集合计数
\(N\) 个元素的集合选出若干个子集使交集元素个数为 \(K\),求方案数 \(\bmod10^9+7\)。
我们很难直接钦定怎么选能直接选出题目要求的若干子集。考虑更弱的性质,不妨设 \(f_k\) 表示选出若干子集,交集至少有 \(k\) 个元素的方案数。我们从 \(n\) 个元素中选定 \(k\) 个元素,方案数 \({n\choose k}\);因为我们不关心余下的元素是否在交集中,剩下的 \(n-k\) 个元素可以任意选/不选,与前面 \(k\) 个元素构成的子集方案数 \(2^{n-k}\);这些子集中至少选 \(1\) 个组成交集至少为 \(k\) 的若干子集的方案数就是 \(2^{2^{n-k}}-1\)。所以 \(f_k\) 就有
再设 \(g_k\) 表示选出若干个子集,交集恰好有 \(k\) 个元素的方案数,\(g_k\) 就是题目要求的答案。接下来我们可以使用 \(g_i\) 来表示出 \(f_k\):枚举 \(i\) 从 \(k\) 到 \(n\),实际要选定 \(k\) 个元素的方案数就是 \({i\choose k}\)。所以有 \(f_k\) 与 \(g_i\) 的关系式
现在考虑二项式反演得出 \(g_k\) 关于 \(f_i\) 的式子。发现上式就是前文二项式反演的推广中那个式子,得到
直接计算即可,复杂度 \(O(n\log n)\)。
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;const int maxn = 1e6 + 10, mo = 1e9 + 7;
int n, k; ll ans;
ll fac[maxn], ifac[maxn], pow2[maxn];ll qpow(ll x, ll y) {ll res = 1;while(y) {if(y & 1) (res *= x) %= mo;(x *= x) %= mo, y >>= 1;} return res;
}
void pre_calc() {fac[0] = ifac[0] = pow2[0] = 1;for(int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mo, pow2[i] = pow2[i - 1] * 2 % (mo - 1);ifac[n] = qpow(fac[n], mo - 2); for(int i = n - 1; i >= 1; i--) ifac[i] = ifac[i + 1] * (i + 1) % mo;return;
}
ll C(int x, int y) {return fac[x] * ifac[y] % mo * ifac[x - y] % mo;}int main() {ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n >> k; pre_calc();for(int i = k; i <= n; i++) ((ans += (((i - k) & 1) ? -1 : 1) * C(i, k) * C(n, i) % mo * (qpow(2, pow2[n - i]) % mo - 1) % mo) += mo) %= mo; cout << ans;return 0;
}
Luogu P4859 已经没有什么好害怕的了
\(n\) 个 \(A\) 物品和 \(n\) 个 \(B\) 物品,每个物品有一个价值。将 \(A,B\) 物品两两配对,求 \(A\) 价值大于 \(B\) 价值的组数比其余组数多恰好 \(k\) 组的方案数 \(\bmod10^9+9\)。
可以先计算出 \(A\) 价值大于 \(B\) 价值的组数 \(K={n+k\over2}\),如果不是整数则不可能有符合条件的方案。
类似的,\(A\) 比 \(B\) 大的组数恰好为 \(K\) 的限制太强,先考虑易求的弱性质。
对 \(A,B\) 的价值分别分别排序,可以双指针简单求出每个 \(A_i\) 有多少个 \(B\) 更小的匹配数 \(d_i\) 。考虑一个 dp:设 \(f_{i,j}\) 表示已经配对前 \(i\) 个,钦定了 \(j\) 组满足 \(A>B\) 的方案数。由于 \(A\) 有序,前面匹配上大于 \(B\) 的一定会使后面的匹配数减小。可以得到转移方程:
边界是 \(f_{0,0}=1\)。
接着考虑一个 \(g_i\) 表示全部匹配上且至少有 \(i\) 组 \(A>B\) 的方案数。如果我们可以用 \(f\) 表示出 \(g\) 就可以二项式反演求出恰好 \(K\) 组的方案数。即是在 \(f_{n,i}\) 已经钦定匹配上 \(i\) 组的基础上,剩下的 \(n-i\) 组任意匹配。就有
答案 \(ans_i\),即恰好匹配上 \(i\) 组的方案数可以表示出 \(g_k\)
二项式反演,得到 \(ans_K\)
直接计算即可。复杂度 \(O(n^2)\),瓶颈在 dp。
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;const int maxn = 2e3 + 10, mo = 1e9 + 9;
int n, k, a[maxn], b[maxn], d[maxn];
ll fac[maxn], ifac[maxn], f[maxn][maxn], g[maxn], ans;ll qpow(ll x, ll y) {ll res = 1;while(y) {if(y & 1) (res *= x) %= mo;(x *= x) %= mo, y >>= 1;} return res;
}
void pre_calc() {ifac[0] = fac[0] = 1; for(int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mo;ifac[n] = qpow(fac[n], mo - 2); for(int i = n - 1; i >= 1; i--) ifac[i] = ifac[i + 1] * (i + 1) % mo;return;
}
ll C(int x, int y) {return fac[x] * ifac[y] % mo * ifac[x - y] % mo;}int main() {ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);cin >> n >> k; pre_calc(); if((n + k) & 1) {cout << 0; return 0;} k = (n + k) / 2; for(int i = 1; i <= n; i++) cin >> a[i]; sort(a + 1, a + n + 1);for(int i = 1; i <= n; i++) cin >> b[i]; sort(b + 1, b + n + 1);for(int i = 1, j = 1; i <= n; i++) {d[i] = d[i - 1];while(a[i] > b[j] && j <= n) j++, d[i]++;}//双指针求出df[0][0] = 1;for(int i = 1; i <= n; i++) {f[i][0] = f[i - 1][0];for(int j = 1; j <= i; j++) f[i][j] = (f[i - 1][j] + (d[i] - (j - 1)) * f[i - 1][j - 1]) % mo;}//dp求f for(int i = 1; i <= n; i++) g[i] = fac[n - i] * f[n][i] % mo;//推式子求g for(int i = k; i <= n; i++) ((ans += (((i - k) & 1) ? -1 : 1) * C(i, k) * g[i] % mo) += mo) %= mo;//反演求anscout << ans; return 0;
}