「學習筆記」二項式反演
Description
在兩個集合中選數字,求選出來的方案中 \(A\) 恰好比 \(B\) 多 \(k\) 個的方案數
\(|A|=|B|\le 2000\)
Solution
按照這種題目描述,我們發現是個二項式反演的題目
二項式反演:
\[f(n)=\sum _ {i=0}^n (-1)^i \binom n i g(i) \Leftrightarrow g(n)=\sum _ {i=0} ^n (-1)^i \binom n i f(i) \]
這個式子可以通過集合相關知識得到(容斥)
然後令:\(h(n)=(-1)^ng(n)\)
那麼有
\[f(n)=\sum_{i=0}^n \binom n i h(i) \Leftrightarrow \frac{h(n)}{(-1)^n}=\sum_{i=0}^n \binom n i f(i) \]
然後把式子整理一下,得到第二種表示二項式定理的形式
\[f(n)=\sum_{i=0}^n \binom n i g(i) \Leftrightarrow g(n)=\sum_{i=0}^n \binom n i f(i) \]
直接帶入推導並且交換列舉順序可以得到:
\[f(n)=\sum_{j=0} ^n f(j)\sum _ {i=j} ^n (-1)^{i-j} \binom n i \binom i j \]
後面的組合數:\(\binom n i \binom i j\) 考慮其組合意義,從 \(n\) 個裡面選 \(i\) 個再從 \(i\) 個裡面選 \(j\) 個用組合階乘式子能給推成\(\binom n j\binom {n-j}{i-j}\)
然後再改變列舉順序,平移一下迴圈變數
\[f(n)=\sum_{j=0} ^n \binom n j f(j) \sum _ {t=0} ^{n-j} (-1)^t \binom {n-j} t \]
在後面乘上 \(1\) 的對應次方,用二項式定理轉一步
\[f(n)=\sum_{i=0} ^n \binom n i f(i) (1-1)^{n-j} \]
顯然得證(只有\(i=n\)時那個 \((1-1)^{n-j}\) 不為 \(0\))
還有一種相對常見的式子,因為題目中大多會表述為恰好選幾個,然後我們用多步容斥(或二項式反演)就會方便求解
\[f(n)=\sum_{i=n}^m \binom i n g(i)\Leftrightarrow g(n)=\sum_{i=n}^m (-1) ^{i-n} \binom i n f(i) \]
相關證明和上面的類似
所以在本題目中,我們設 \(g\) 為正確答案,\(f\) 為至少 \(k\) 個答案
設 \(dp_{i,j}\) 為前 \(i\) 個選 \(j\) 個 \(A\) 中比 \(B\) 中大的
先看是不是要配對,不要那就是 \(f_{i,j}+=f_{i-1,j}\)
再看如果配對的話,那就是 \(f_{i,j}=f_{i-1,j-1}+(larger(i)-j+1)\)
\(larger(i)\) 表示 \(A_{i}\) 大於幾個 \(B_i\),可以用暴力或者單調棧做掉……
\(f_x=dp_{n,x}\times(n-x)!\)
這裡上個反演就做完了
\[g_i=\sum_{i=k}^n (-1)^{i-k} \binom i k \times dp_{n,i}\times (n-i)! \]
Code
#include<bits/stdc++.h>
using namespace std;
#define int long long
namespace yspm{
inline int read()
{
int res=0,f=1; char k;
while(!isdigit(k=getchar())) if(k=='-') f=-1;
while(isdigit(k)) res=res*10+k-'0',k=getchar();
return res*f;
}
const int N=2010,mod=1e9+9;
inline int add(int x,int y){return x+y>=mod?x+y-mod:x+y;}
int fac[N],inv[N],n,k,c[N],m[N],dp[N][N],l[N];
inline void prework()
{
fac[0]=fac[1]=inv[0]=inv[1]=1;
for(int i=1;i<N;++i) fac[i]=fac[i-1]*i%mod;
for(int i=2;i<N;++i) inv[i]=mod-(mod/i)*inv[mod%i]%mod ;
for(int i=1;i<N;++i) inv[i]=inv[i-1]*inv[i]%mod;
return ;
}
inline int C(int n,int m){return fac[n]*inv[m]%mod*inv[n-m]%mod;}
signed main()
{
n=read(); k=read(); dp[0][0]=1; prework();
if((n-k)&1) return puts("0"),0;
for(int i=1;i<=n;++i) c[i]=read();
for(int i=1;i<=n;++i) m[i]=read();
sort(c+1,c+n+1); sort(m+1,m+n+1);
//用單調棧解決這部分的問題
for(int i=1;i<=n;++i)
{
l[i]=-1;
for(int j=1;j<=n;++j)
{
if(m[j]>c[i]){l[i]=j-1; break;}
}if(l[i]==-1) l[i]=n;
}
for(int i=1;i<=n;++i)
{
dp[i][0]=1;
for(int j=1;j<=i;++j)
{
dp[i][j]=add(dp[i-1][j],dp[i-1][j-1]*max(0ll,l[i]-j+1)%mod);
}
}
int ans=0; k=(n+k)>>1;
for(int i=k;i<=n;++i)
{
int res=C(i,k)%mod*dp[n][i]%mod*fac[n-i]%mod;
if((i-k)&1) ans-=res,ans=add(ans,mod);
else ans=add(ans,res);
}printf("%lld\n",ans);
return 0;
}
}
signed main(){return yspm::main();}
二項式反演主要是應對類似於“恰好”,這樣的計數問題,然後主要是用到最後一種形式吧……