二项式反演学习笔记
二项式反演(Binomial Inversion)是一种反演,它基于容斥原理.它可以把计数问题中求解"恰好X个的方案数"转化为求解"至少X个的方案数",让问题变得更简单。
约定:
- (C_n^m)表示从(n)个数里选(m)个数的方案数,等价于(inom{m}{n}).另外为了方便推式子,规定(n<m)时(C_n^{m}=0)
- 若(S)是一个集合,规定(overline{S})为(S)的补集
- 若(S)是一个集合,规定(|S|)为(S)的元素个数
- ([p(x)]=egin{cases} 1,p(x) \ 0, eg p(x) end{cases})
基本定理
二项式反演有3种基本形式:
接下来我们会证明它。
标准形式
定理1(二项式反演形式1):$$f_n = sum_{i=0}^n (-1)^i C_n^i g_i Leftrightarrow g_n = sum_{i=0}^n (-1)^i C_n^i f_i ag{1.1}$$
这个式子充满了对称性,接下来我们证明它
容斥解释
实际上,二项式反演是容斥原理的特例。
我们可以构造一些集合来证明这个定理,全集为(S),(S)中具有(n)种性质的集合分别为(A_1,A_2 dots A_n).特别地,我们构造这些集合,使它们满足其中任意(i)个集合的并集大小相等,记为(g_i).另外规定(g_0=|S|)
那么根据容斥原理,均没有这些性质的集合的大小为:
又因为任意(i)个集合的并集大小相等
同理可得(A_i)的补集的并集大小相等,不妨记为(f_i),同样令(f_0=|S|).那么上式可化为$f_n = sum_{i=0}^n (-1)^i C_n^i g_i $.
再使用一次容斥原理:
那么$g_n = sum_{i=0}^n (-1)^i C_n^i f_i $
这说明,两个式子是等价的。因此我们就证明了反演定理
代数解释
虽然容斥解释很妙,但是在题目中我们可能会遇到非标准的式子,因此要理解代数展开证明的思想。
已知$f_n = sum_{i=0}^n (-1)^i C_n^i g_i $
那么
第二步是交换了求和顺序,我们尝试对于每个(f_j)计算出它的系数并提到外面,容易发现对于(j in [1,i])都会乘上系数((-1)^iC_n^i),也就是说会被(>=j)的((-1)^iC_n^i)更新。
那么这样只需要后面的和式在(j
eq n)时为0,(j=n)时为1,那么总和就为(g_n)
接下来需要用到组合数的性质
代回上式
当(n
eq j)时( ext{原式}=0)
当(n = j)时出现了(0^0)无法直接算,考虑直接用原式(因为最后一步转化破坏了等价性),(sum_{i=j}^n (-1)^{i+j}C_{n}^i C_{i}^j=sum_{i=n}^n (-1)^{i+n}C_{n}^n C_{i}^n=(-1)^{2n}C_n^nC_n^n=1)
因此( ext{原式}=[n=j])
也就是说
至多和恰好的转化
定理2(二项式反演形式2):$$f_n = sum_{i=0}^n C_n^i g_i Leftrightarrow g_n = sum_{i=0}^n (-1)^{n-i} C_n^i f_i ag{1.2}$$
这个式子的意义是,(f_n)表示至多(n)个的方案数,那么(g_n)就表示恰好(n)个的方案数。
证明:
考虑形式1
令(g'_i=(-1)^{-i} g_i,那么f_n= sum_{i=0}^n (-1)^i C_n^i g'_i)
那么(g'_n = sum_{i=0}^n (-1)^i C_n^i f_i)
即((-1)^{-n}g_n=sum_{i=0}^n (-1)^i C_n^i f_i)
两边同乘((-1)^{-2n}=1),得到$g_n=sum_{i=0}^n (-1)^{i-n} C_n^i f_i=sum_{i=0}^n (-1)^{n-i} C_n^i (
) (forall x in mathbb{Z},(-1)^{-x}=(-1)^{2x}(-1)^{-x}=(-1)^x)$
至少和恰好的转化
这是最常见的二项式反演形式,一定要掌握!
定理3(二项式反演形式3):$$f_n = sum_{i=n}^m C_i^n g_i Leftrightarrow g_n = sum_{i=n}^m (-1)^{i-n} C_i^n f_i ag{1.3}$$
对于(forall m geq n),结论都成立,即使是一个无限求和也可以
这个式子的意义是,(f_n)表示 "至少" (n)个的方案数,那么(g_n)就表示恰好(n)个的方案数.这里的 "至少"打引号是因为它不是一个简单的后缀和,它表示我们"钦定"(n)个数满足条件,剩下的数不一定满足条件求(f)的时候每个(g_i)是会被重复计算的,计算了(C_n^i)次.
证明:
同((1.1))的证明,交换求和顺序
注意到后面的(sum_{i=n}^j (-1)^{i-n} C_{j}^i C_i^n)部分也很相似,因此可以用类似的方法处理。
代入组合数的性质((1.1.2))
容易发现这个证明过程只是把原来的(n-j)换成(j-n)而已。
*广义的反演
我们发现形式(1)和形式(3)的证明有相似之处,都是交换求和顺序然后提取出和式,证明它等于([n=k]). 我们不妨将这个方法推广到任意序列。
定理4
命题$$f_n = sum_{i=0}^n a_{n,i} g_i Leftrightarrow g_n = sum_{i=0}^n b_{n,i} f_i $$成立的充要条件是[forall i in mathbb{N},sum_{j=i}^n b_{n,j}a_{j,i}=[n=i] ]
证明:
还是交换求和顺序
把字母换一下就得到了上面的定理
应用
在做题中,看到求"恰好X"的方案数,就要敏感的想到二项式反演。
形式1几乎没有什么用
形式2(至多)的应用
错排问题
求错排问题的通项公式:
若一个排列中所有的元素都不在自己原来的位置上,那么这样的排列就称为原排列的一个错排。 (n)个元素的错排数记为(D_n)[D_n=n!sum_{i=0}^n frac{(-1)^i}{i!} ]
其实可以把(D_n)看成不在原来位置上的元素有(n)个的排列数。设(f_n)表示长度为n的排列,在原来位置上的元素至多有(n)个的排列数,显然所有排列都满足这个性质,即(f_n=n!)
那么
min-max容斥
设(S)为一个集合,(max(S),min(S))分别表示集合中的最大值和最小值。特别地,规定(max(emptyset)=min(emptyset)=0),则$$max(S)=sum_{Tsubset S}(-1)^{|T|-1} min(T)$$
证明:
设存在一个以集合大小为自变量的函数(f)满足
记(S)中元素从大到小分别为(a_1,a_2 dots a_{|S|}).
那么(a_i)对等式左边的贡献是([i=1]),因为(a_1=max(S)).
(a_i)对等式右边的贡献为(sum_{j=0}^{i-1}C_{i-1}^j f(j+1)). 这是因为要(i)成为大小为(j+1)的集合的集合中的最小值,就要从(a_1,a_2 dots a_{i-1})这些比(i)大的数中选出(j)个,再加上(a_i)构成一个集合。
那么为了等式成立,左右的贡献应该两两抵消,只剩下(a_1).所以必有
构造函数(A(x)=[x=0]),那么(A(x-1)=[x=1]). 注意到右边有(f(j+1)),不方便反演,不妨令(B(x)=f(x+1))
那么(A(i)=sum_{j=0}^i C_i^j B(j))
反演得到(B(i)=sum_{j=0}^i (-1)^{n-j} C_i^jA(j)=sum_{j=0}^i (-1)^{n-j} C_i^j[j=0]=(-1)^{i})
所以(f(x)=B(x-1)=(-1)^{x-1}),代回原式,得
min-max容斥有许多应用,这里不再赘述。
形式3(至少)的应用
由于形式3的(f)定义是可以有重复计数的,因此它会比"恰好"的形式好求,因为不需要考虑计数时的一些判重问题。这也给求(f)的过程(如直接数学计算/DP)提供了方便。
[BZOJ2839] 集合计数
一个有N个元素的集合有(2^N)个不同子集(包含空集),现在要在这(2^N)个集合中取出若干集合(至少一个),使得
它们的交集的元素个数为K,求取法的方案数,答案模1000000007。
设(f_i)表示交集的元素个数 "至少" 为(i)的方案数。那么我们可以从(n)个元素中选出(i)个钦定为交集。剩下的(n-i)个元素组成(2^{n-i})个包含空集的集合。从这些集合中任意选一些集合并上那(i)个数,它们的交集一定是这(i)个数。
因此
根据二项式反演,答案为
注意(2^{2^{n-i}})无法快速幂,只需要递推求即可。因为(2^{2^i}=2^{2^{i-1}} cdot2^{2^{i-1}})
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define mod 1000000007
#define maxn 1000000
using namespace std;
typedef long long ll;
inline ll fast_pow(ll x,ll k){
ll ans=1;
while(k){
if(k&1) ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
inline ll inv(ll x){
return fast_pow(x,mod-2);
}
ll fact[maxn+5],invfact[maxn+5];
void ini(int n){
fact[0]=1;
for(int i=1;i<=n;i++) fact[i]=fact[i-1]*i%mod;
invfact[n]=inv(fact[n]);
for(int i=n-1;i>=0;i--) invfact[i]=invfact[i+1]*(i+1)%mod;
}
ll C(int n,int m){
return fact[n]*invfact[n-m]%mod*invfact[m]%mod;
}
int n,k;
ll f[maxn+5],g[maxn+5];
int main(){
scanf("%d %d",&n,&k);
ini(n);
ll pw=2;//2^(2^(n-i)),无法快速幂计算,只能递推
for(int i=n;i>=0;i--){
f[i]=C(n,i)*(pw-1)%mod;
pw=pw*pw%mod;
}
ll ans=0;
for(int i=k;i<=n;i++){
if((i-k)%2==1) ans=ans-C(i,k)*f[i]%mod;
else ans=ans+C(i,k)*f[i]%mod;
ans=(ans+mod)%mod;
}
printf("%lld
",ans);
}
[LuoguP4859]已经没有什么好害怕的了
(这里是简化版的题意)给出两个长度为(n)的序列(a,b),现在我们要把每个(a)中元素与每个(b)中元素配对,问满足(a_x>b_y)的配对数比(a_x<b_y)的配对数多(k)的配对方案数
首先问题转化为(a>b)的个数有(frac{n+k}{2})
设(f(i))表示保证(钦定)有(i)对(a>b),其余不一定。(g(i))表示恰好(i)对a>b.显然(f(i)=sum_{j=i}^m C_j^i g(j)),根据二项式反演定理(g(i)=sum_{j=i}^m C_{j}^i f(j))
考虑如何求(f).把(a)从小到大排序,记(cnt(i))表示(b)中小于(a_i)的数的个数。设(dp_{i,j})为(a)序列的前(i)个,保证有(j)对(a>b)的方案数,则(f_i=dp_{n,i}(n-i)!)
容易写出转移方程:
(dp_{i,j}=dp_{i,j-1}+dp_{i-1,j-1}cdot (cnt(i)-(j-1)))
我们按(a)排序,因此(b)中小于(a_i)的数的集合一定包含(b)中小于(a_{i-1})的数的集合.之前已经选了(j-1)个数,那只能选剩下的(cnt(i)-(j-1))个数来使得(a_i>b_x),这样对数会增加1。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define mod 1000000009
#define maxn 2000
using namespace std;
typedef long long ll;
inline ll fast_pow(ll x,ll k){
ll ans=1;
while(k){
if(k&1) ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
inline ll inv(ll x){
return fast_pow(x,mod-2);
}
ll fact[maxn+5],invfact[maxn+5];
void ini(int n){
fact[0]=1;
for(int i=1;i<=n;i++) fact[i]=fact[i-1]*i%mod;
invfact[n]=inv(fact[n]);
for(int i=n-1;i>=0;i--) invfact[i]=invfact[i+1]*(i+1)%mod;
}
ll C(int n,int m){
return fact[n]*invfact[n-m]%mod*invfact[m]%mod;
}
int n,k;
int a[maxn+5],b[maxn+5];
int cnt[maxn+5];//a从小到大排序后,a[i]>b[j]的j的数量
ll dp[maxn+5][maxn+5];
ll f[maxn+5],g[maxn+5];
void calc_inv(ll *f,ll *g,int n){
for(int i=0;i<=n;i++){
for(int j=i;j<=n;j++){
if((j-i)%2==1) g[i]-=C(j,i)*f[j]%mod;
else g[i]+=C(j,i)*f[j]%mod;
g[i]=(g[i]+mod)%mod;
}
}
}
int main(){
scanf("%d %d",&n,&k);
ini(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) scanf("%d",&b[i]);
sort(a+1,a+1+n);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++) if(a[i]>b[j]) cnt[i]++;
}
for(int i=0;i<=n;i++) dp[i][0]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
dp[i][j]=(dp[i-1][j-1]*max(cnt[i]-j+1,0)%mod+dp[i-1][j])%mod;
}
}
for(int i=0;i<=n;i++) f[i]=dp[n][i]*fact[n-i]%mod;
calc_inv(f,g,n);
printf("%lld
",g[(n+k)/2]);
}
[NOIOnline2提高组T3]游戏
小 A 和小 B 正在玩一个游戏:有一棵包含 n=2m个点的有根树(点从1∼n 编号),它的根是 1 号点,初始时两人各拥有 m 个点。游戏的每个回合两人都需要选出一个自己拥有且之前未被选过的点,若对手的点在自己的点的子树内,则该回合自己获胜;若自己的点在对方的点的子树内,该回合自己失败;其他情况视为平局。游戏共进行 m 回合。
对于 k=0,1,2,⋯,m,计算出非平局回合数为 k 的情况数。两种情况不同当且仅当存在一个小 A 拥有的点 x,小 B 在 x 被小 A 选择的那个回合所选择的点不同。
二项式反演的套路,设(f(i))为非平局回合数至少为(i)的情况. (g(i))为非平局回合数恰好为(i)的情况,则(f(i)=sum_{j=i}^m C_j^i g(j)).(这里的"至少"指的是我们"钦定"有(i)局一定平局,其他不一定,所以至少为(i).因此对(f(i))求后缀和不是(g(i)),会算重)我们要求的是(g(k)),但是(g)不太好求,考虑求(f)再反演得出(g).根据二项式反演定理(g(i)=sum_{j=i}^m C_{j}^i f(j))
(f)可以用树形背包来求。设(dp_{x,i})为(x)的子树内至少(i)对选择的有祖先-后代关系的点(即非平局的回合).那么(f(i)=dp_{1,i}cdot(m-i)!)(剩下的(m-i)对关系可以任意排序)
DP的时候先用模板的树形背包合并子树,然后考虑(x)这个点对答案的影响,有
(dp_{x,i} leftarrow dp_{x,i}+dp_{x,i-1}cdots( ext{x子树内不被x的拥有者拥有的节点个数}-(i-1)))
这是因为(x)可以和子树内不被(x)的拥有者拥有的,且还没被配对(减去(i-1))的节点形成非平局回合。
#include<iostream>
#include<cstdio>
#include<cstring>
#define mod 998244353
#define maxn 5000
using namespace std;
typedef long long ll;
inline ll fast_pow(ll x,ll k){
ll ans=1;
while(k){
if(k&1) ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
inline ll inv(ll x){
return fast_pow(x,mod-2);
}
ll fact[maxn+5],invfact[maxn+5];
void ini(int n){
fact[0]=1;
for(int i=1;i<=n;i++) fact[i]=fact[i-1]*i%mod;
invfact[n]=inv(fact[n]);
for(int i=n-1;i>=0;i--) invfact[i]=invfact[i+1]*(i+1)%mod;
}
ll C(int n,int m){
return fact[n]*invfact[n-m]%mod*invfact[m]%mod;
}
int m,n;
int a[maxn+5];
struct edge {
int from;
int to;
int next;
} E[maxn*2+5];
int head[maxn+5];
int esz=1;
void add_edge(int u,int v) {
esz++;
E[esz].from=u;
E[esz].to=v;
E[esz].next=head[u];
head[u]=esz;
}
int sz[maxn+5];
int cnt[maxn+5][2];
ll dp[maxn+5][maxn+5];//x的子树内,至少有i对祖先关系的方案数(有i对我们已经钦定,剩下的随便搞
//在dp转移中,我们钦定i对关系来转移,最后再乘上(m-i)!,表示剩下的关系可以任意匹配
//因为只是钦定,转移的时候就不需要考虑剩下的关系。而如果直接定义状态为恰好i对,转移时无法保证合法
void dfs(int x,int fa) {
static ll tmp[maxn+5];
sz[x]=1;
cnt[x][a[x]]++;
dp[x][0]=1;
for(int i=head[x];i;i=E[i].next){//先合并子树
int y=E[i].to;
if(y!=fa){
dfs(y,x);
for(int j=0;j<=sz[x]+sz[y];j++) tmp[j]=0;
for(int j=0;j<=sz[x];j++){
for(int k=0;k<=sz[y];k++){
tmp[j+k]=(tmp[j+k]+dp[x][j]*dp[y][k]%mod)%mod;
}
}
for(int j=0;j<=sz[x]+sz[y];j++) dp[x][j]=tmp[j];
sz[x]+=sz[y];
cnt[x][0]+=cnt[y][0];
cnt[x][1]+=cnt[y][1];
}
}
//计算x的贡献
for(int i=cnt[x][a[x]^1];i>=1;i--){//对于dp[x][i],x可以和子树里还没被选的与a[x]颜色相反的点匹配
//于是贡献为dp[x][i-1]*(cnt[x][a[x]^1]-(i-1)) 倒序是为了防止重复更新
dp[x][i]+=dp[x][i-1]*(cnt[x][a[x]^1]-(i-1))%mod;
dp[x][i]%=mod;
}
}
ll f[maxn+5],g[maxn+5];
int main() {
int u,v;
scanf("%d",&n);
m=n/2;
ini(n);
for(int i=1;i<=n;i++) scanf("%1d",&a[i]);
for(int i=1;i<n;i++){
scanf("%d %d",&u,&v);
add_edge(u,v);
add_edge(v,u);
}
dfs(1,0);
for(int i=0;i<=m;i++) f[i]=dp[1][i]*fact[m-i]%mod;//求出真正的dp值
for(int i=0;i<=m;i++){
for(int j=i;j<=m;j++){
if((j-i)%2==1) g[i]-=C(j,i)*f[j]%mod;
else g[i]+=C(j,i)*f[j]%mod;
g[i]=(g[i]+mod)%mod;
}
printf("%lld
",g[i]);
}
}