====== 二项式反演 ====== ===== 算法简介 ===== 一种特殊反演,主要应用于组合计数。 ===== 算法思想 ===== $$f(n)=\sum_{i=m}^n{n\choose i}g(i)\iff g(n)=\sum_{i=m}^n(-1)^{n-i}{n\choose i}f(i)$$ $$f(n)=\sum_{i=n}^m{i\choose n}g(i)\iff g(n)=\sum_{i=n}^m(-1)^{i-n}{i\choose n}f(i)$$ ===== 算法例题 ===== ==== 例题一 ==== === 题意 === 求错排数 $D_n$。 === 题解 === 考虑任意排列,有 $n!$ 种,其中恰有 $i$ 封信错排的方案数为 ${n\choose i}D_i$,于是 $n!=\sum_{i=0}^n{n\choose i}D_i$。 根据二项式反演,有 $D_n=\sum_{i=0}^n(-1)^{n-i}{n\choose i}i!$ ==== 例题二 ==== === 题意 === 一个有 $n$ 个元素的集合,现在要从他的所有子集中取出至少一个集合,使得所选子集的交集的元素个数为 $k$,求方案数。 === 题解 === 定义 $f(k)$ 表示所有钦定 $k$ 个元素属于交集(其余元素任意)的方案数之和,于是有 $f(k)={n\choose k}2^{2^{n-k}}$。 定义 $g(k)$ 表示恰好有 $k$ 个元素属于交集的方案数,对于 $f(k)$ 的每个方案,$g(i)(i\ge k)$ 被计算 ${i\choose k}$ 次,于是 $f(k)=\sum_{i=k}^n{i\choose k}g(i)$。 根据二项式反演,有 $g(k)=\sum_{i=k}^n(-1)^{i-k}{i\choose k}f(i)$,题目所求即为 $g(k)$,时间复杂度 $O(n)$。 ===== 算法练习 ===== ==== 习题一 ==== [[https://www.luogu.com.cn/problem/P5505|洛谷p5505]] === 题意 === 有 $n$ 个人和 $m$ 种物品,第 $i$ 种物品有 $a_i$ 个,同种物品之间没有区别。现在要将这些物品分给这些人,使得每个人至少分到一个物品,求方案数。 === 题解 === 定义 $f(k)$ 表示所有钦定 $k$ 个人没有分到物品(其余人任意)的方案数之和,于是有 $f(k)={n\choose k}\prod_{i=1}^m{n-k+a_i-1\choose n-k-1}$。 定义 $g(k)$ 表示恰好有 $k$ 个人没有分到物品的方案数,对于 $f(k)$ 的每个方案,$g(i)(i\ge k)$ 被计算 ${i\choose k}$ 次,于是 $f(k)=\sum_{i=k}^n{i\choose k}g(i)$。 根据二项式反演,有 $g(k)=\sum_{i=k}^n(-1)^{i-k}{i\choose k}f(i)$,题目所求即为 $g(0)$,时间复杂度 $O(nm)$。 const int MAXN=2005,Mod=1e9+7; int a[MAXN],C[MAXN][MAXN]; int main() { int n=read_int(),m=read_int(); _for(i,0,m)a[i]=read_int(); C[0][0]=1; _for(i,1,MAXN){ C[i][0]=1; _rep(j,1,i) C[i][j]=(C[i-1][j-1]+C[i-1][j])%Mod; } int ans=0; _rep(i,0,n){ int t=C[n][i]; _for(j,0,m)t=1LL*t*C[n-i+a[j]-1][n-i-1]%Mod; if(i&1)ans=(ans-t)%Mod; else ans=(ans+t)%Mod; } enter((ans+Mod)%Mod); return 0; } ==== 习题二 ==== [[https://www.luogu.com.cn/problem/P4859|洛谷p4859]] === 题意 === 给出两个长度均为 $n$ 的序列 $\text{A}$ 和 $\text{B}$,保证所有数互异。 现要将 $\text{A}$ 序列中的数与 $\text{B}$ 序列中的数两两配对,求 $a_i\gt b_i$ 的对数比 $a_i\lt b_i$ 的对数多 $k$ 的配对方案数。 === 题解 === 首先如果 $n+k$ 为奇数,则方案数为 $0$,否则 $a_i\gt b_i$ 的对数恰好为 $\frac {n+k}2$。 将序列 $\text{A}$ 和 $\text{B}$ 都按从小到大排序,设 $\text{dp}(i,j)$ 表示序列 $\text{A}$ 的前 $i$ 个数中钦定 $j$ 对数满足 $a_i\gt b_i$ (其余数暂时无视)的方案数之和。 设有 $c$ 个数小于 $a_i$,不难得出状态转移方程 $\text{dp}(i,j)=\text{dp}(i-1,j)+(c-j+1)\text{dp}(i-1,j-1)$,边界条件 $\text{dp}(0,0)=0$。 定义 $f(k)$ 表示所有钦定 $k$ 对数满足 $a_i\gt b_i$ (其余数对任意)的方案数之和,于是有 $f(k)=(n-k)!\text{dp}(n,k)$。 定义 $g(k)$ 表示恰好有 $k$ 对数满足 $a_i\gt b_i$ 的方案数,对于 $f(k)$ 的每个方案,$g(i)(i\ge k)$ 被计算 ${i\choose k}$ 次,于是 $f(k)=\sum_{i=k}^n{i\choose k}g(i)$。 根据二项式反演,有 $g(k)=\sum_{i=k}^n(-1)^{i-k}{i\choose k}f(i)$,题目所求即为 $g(\frac {n+k}2)$,时间复杂度 $O(n^2)$。 const int MAXN=2005,Mod=1e9+9; int quick_pow(int a,int b){ int ans=1; while(b){ if(b&1)ans=1LL*ans*a%Mod; a=1LL*a*a%Mod; b>>=1; } return ans; } int a[MAXN],b[MAXN],dp[MAXN][MAXN],frac[MAXN],invfrac[MAXN]; int C(int a,int b){return 1LL*frac[a]*invfrac[b]%Mod*invfrac[a-b]%Mod;} int main() { int n=read_int(),k=read_int(),m; if((n-k)&1){ puts("0"); return 0; } m=n+k>>1; _rep(i,1,n)a[i]=read_int(); _for(i,0,n)b[i]=read_int(); sort(a+1,a+n+1);sort(b,b+n); int pos=0; dp[0][0]=1; _rep(i,1,n){ while(b[pos]pos)break; dp[i][j]=(dp[i-1][j]+1LL*dp[i-1][j-1]*(pos+1-j))%Mod; } } frac[0]=1; _rep(i,1,n)frac[i]=1LL*frac[i-1]*i%Mod; invfrac[n]=quick_pow(frac[n],Mod-2); for(int i=n;i;i--)invfrac[i-1]=1LL*invfrac[i]*i%Mod; int ans=0,t; _rep(i,m,n){ t=1LL*C(i,m)*frac[n-i]%Mod*dp[n][i]%Mod; if((i-m)&1)ans=(ans-t)%Mod; else ans=(ans+t)%Mod; } enter((ans+Mod)%Mod); return 0; }