反演基础
前言
在 OI 中,所谓容斥思想,很多时候就是面对题目中的强性质不好直接表示时,先表示出一个易求的弱性质,再去考虑用强性质表示出弱性质,通过反演来得到用弱性质表示的强性质。
对于不同式子的反演方法各不相同,类似的变化也比较多样,下文将结合例题来探讨各种不同反演的应用。
二项式反演
基本思想
记 \(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\) 写成列向量,则二项式反演可以写为
所以上文的证明等价于证明 \(\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]++;
}//双指针求出d
f[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;//反演求ans
cout << ans;
return 0;
}
Luogu P6295 有标号 DAG 计数
求 \(\le n\) 个点的有标号弱连通有向无环图(有向边替换为无向边后图连通)个数 \(\bmod998244353\)。
此题需要多项式与生成函数基础。
设有标号弱连通 DAG 方案数的 EGF 为 \(F(x)\),有标号 DAG 方案数的 EGF 为 \(G(x)\)。根据对 EGF 的 exp 组合意义是有标号组合对象组成的集合个数,由于弱连通 DAG 性质强于一般 DAG ,我们如果把弱连通 DAG 看做基本组合对象,其经过组合可以得到所有的一般 DAG。也就是说我们有
所以如果我们可以求出 \(G(x)\) 的 EGF,再做一次多项式 \(\ln\) 就可以得到答案的 EGF。
接下来我们考虑表示出 \(G(x)\)。设 \(G(x)\) 系数为 \(g_i\),我们要尝试容斥出一个式子能够刻画 \(g_i\)。
一个 DAG 计数的套路是我们可以钦定 DAG 中入度为 \(0\) 的点的个数,因为 DAG 中至少存在一个入度为 \(0\) 的点。不妨设 \(h_{i,j}\) 表示 \(i\) 个点 DAG 钦定有 \(j\) 个点入度为 \(0\),其余点不作限制的方案数,\(c_{i,j}\) 表示 \(i\) 个点 DAG 钦定恰好有 \(j\) 个入度为 \(0\) 的方案数。根据二项式反演我们可以表示出 \(h_{i,j}\) 和 \(c_{i,j}\) 的关系有
考虑 \(h_{i,j}\) 中除开钦定的 \(j\) 个入度为 \(0\) 的点,剩下不做限制的 \(i-j\) 个点实际上构成了一个一般的 DAG,即 \(g_{i-j}\)。这两部分其中的 \(j\) 个入度为 \(0\) 的点可以向除开 \(j\) 个点本身的所有点随意连边,这是钦定入度为 \(0\) 带来的性质,所以在总共 \(j(n-j)\) 条边中可以任意选/不选,方案数就是 \(2^{j(n-j)}\)。所以有 \(h_{i,j}\) 与 \(g_{i-j}\) 的关系式
\(\sum c_{i,j}\) 实际上包括了 \(i\) 个点可能构成的所有 DAG 的方案,也就是说 \(g_i\) 可以直接用 \(c_{i,j}\) 表示,有
现在我们可以做一个代换,对上式用 \(h\) 表示 \(c\),再用 \(g\) 表示 \(h\),得到 \(g\) 和 \(g\) 的关系即
先想办法化简,考虑交换求和次序,凑二项式定理
接下来我们的核心问题集中在处理 \(2^{j(n-j)}\) 这个离奇的式子上,实际上我们也可以先把 \({n\choose j}\) 拆开找找灵感
如果您的注意力比较不错,就会发现拆开后两边 \(g\) 的下标都出现在阶乘里。如果我们调整一下位置
出现了 \(G(x)\) 系数的 EGF 形式!而且两边还是是同构的!
现在我们要把 \(2^{j(n-j)}\) 拆成易处理的,且同样含有 \(n,(n-j)\) 项的东西。
实际上有一个不太常见的凑式子 trick:\(j(n-j)={n\choose 2}-{j\choose 2}-{n-j\choose 2}\)。数值上易证。
所以就又可以继纟卖化简口拉:
设 $$P(x)=\sum_{i\ge0}{g_i\over2^{i\choose2} i!}x^i$$
把上式两边同时看做 OGF 的系数,有
可以发现右式和 \(P(x)Q(x)\) 很像。根据卷积的定义,如果 \(A(x),B(x)\) 都是下标从 \(0\) 开始的生成函数,我们有
然而 \(Q(x)\) 下标是从 \(1\) 开始的,这意味着我们要调整第二个求和使其没有 \(q_0\) 这一项,即
发现将上界调整为 \(n-1\) 就可以解决这个问题。
接着我们再用 \(j=i+1\) 来作为指标使上式看起来更接近原式,有
这时候我们突然发现—— \(n=0\) 的这一项消失了!但是原式是有 \(g_0\) 这一项的。我们要拿 \(P(x)Q(x)\) 替换原式就要补上 \(g_0\)。
那么 \(g_0\) 应该是多少呢?我们回忆一下之前的一个式子
容易得到 \(g_1=1\),因为一个点的 DAG 当然只有一种情况。
于是上式带入 \(n=1\),就得到了 \(g_0=g_1=1\)!
于是前面的 \(P(x)\) 就被表示出来了!有
其中
\(Q(x)\) 可以直接 \(O(n\log n)\) 计算得到。
\(1-Q(x)\) 做一遍多项式求逆就得到了 \(P(x)\)。
\(P(x)\) 处理一下就得到了 \(G(x)\) 的 EGF。
\(G(x)\) 的 EGF 做一遍多项式 \(\ln\) 就得到了 \(F(x)\) 的 EGF。
\(F(x)\) 的 EGF 转成 OGF 就得到答案啦啦啦!
代码实现
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int maxn=2e5 + 10, mo=998244353, inv2 = 499122177;
int n, fac[maxn], ifac[maxn];
//以下是多项式板子
inline int add(int x,int y){
return (x+y>=mo)?x+y-mo:x+y;
}
inline int sub(int x,int y){
return (x-y<0)?x-y+mo:x-y;
}
inline int mul(int x,int y){
return 1ll*x*y%mo;
}
int qpow(int x,ll y){
int rs=1;
while(y){
if(y&1) rs=1ll*rs*x%mo;
x=1ll*x*x%mo;
y>>=1;
}
return rs;
}
int rev[1<<20];
void init_rev(int len){
for(int i=0;i<len;++i){
rev[i]=rev[i>>1]>>1;
if(i&1) rev[i]|=len>>1;
}
}
namespace Polynomial{
struct poly{
int a[1<<20];
poly(){};
inline int& operator[](int x){
return a[x];
}
inline int operator[](int x)const{
return a[x];
}
inline void operator=(const poly &t){
memcpy(a,t.a,sizeof(a));
return;
}
inline void slice(int len,int k){
for(int i=len;i<k;++i) a[i]=0;
}
void NTT(int len,int rv){
for(int i=0;i<len;++i) if(i<rev[i]) swap(a[i],a[rev[i]]);
for(int h=2;h<=len;h<<=1){
int wn=qpow(3,(mo-1)/h);
for(int i=0;i<len;i+=h){
int w=1;
for(int k=i;k<i+h/2;++k){
int u=a[k],v=mul(w,a[k+h/2]);
a[k]=add(u,v);
a[k+h/2]=sub(u,v);
w=mul(w,wn);
}
}
}
if(rv==-1){
reverse(a+1,a+len);
int inv=qpow(len,mo-2);
for(int i=0;i<len;++i) a[i]=mul(a[i],inv);
}
}
poly inv(int len)const{
poly g,g0,d;
g[0]=qpow(a[0],mo-2);
for(int lim=2;(lim>>1)<len;lim<<=1){
g0=g,d=*this;
d.slice(lim,len);
int k=lim<<1;
init_rev(k);
g0.NTT(k,1),d.NTT(k,1);
for(int i=0;i<k;++i) g0[i]=mul(g0[i],sub(2,mul(g0[i],d[i])));
g0.NTT(k,-1);
g0.slice(lim,k);
g=g0;
}
return g;
}
poly der(int len)const{
poly g=*this;
for(int i=0;i<len;++i) g[i]=mul(g[i+1],i+1);
return g;
}
poly integ(int len)const{
poly g=*this;
for(int i=len-1;i;--i) g[i]=mul(g[i-1],qpow(i,mo-2));
g[0]=0;
return g;
}
poly ln(int len)const{
poly iv=this->inv(len),rs;
poly d=this->der(len);
int k=len<<1;
init_rev(k);
d.NTT(k,1),iv.NTT(k,1);
for(int i=0;i<k;++i) rs[i]=mul(d[i],iv[i]);
rs.NTT(k,-1);
rs=rs.integ(k);
rs.slice(len,k);
return rs;
}
};
}
using namespace Polynomial;
poly P,Q,F,G;
//以上是多项式板子
int len = 1;
void pre_calc() {
fac[0] = ifac[0] = 1; for(int i = 1; i <= n; i++) fac[i] = 1ll * fac[i - 1] * i % mo;
ifac[n] = qpow(fac[n], mo - 2); for(int i = n - 1; i >= 1; i--) ifac[i] = 1ll * ifac[i + 1] * (i + 1) % mo;
while(len<=n) len<<=1;
return;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n; n++; pre_calc();
Q[0] = 1; for(int i = 1; i < n; i++) Q[i] = 1ll * qpow(inv2, 1ll * i * (i - 1) / 2) * ifac[i] % mo, Q[i] = (i & 1) ? mo - Q[i] : Q[i];
P = Q.inv(len);
for(int i = 0; i < n; i++) G[i] = 1ll * P[i] * qpow(2, 1ll * i * (i - 1) / 2) % mo;
F = G.ln(len);
for(int i = 1; i < n; i++) cout << 1ll * F[i] * fac[i] % mo << endl;
return 0;
}
后记
拼尽全力终于在花了整整一天时间解决了此题
生成函数卷积那个地方看了接近两个小时才大概懂
蒟蒻宝宝的第三道黑题,第一道黑题题解
应该还是挺有质量的罢

浙公网安备 33010602011771号