1. 程式人生 > 實用技巧 >「學習筆記」二項式反演

「學習筆記」二項式反演

Description

link

在兩個集合中選數字,求選出來的方案中 \(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();}

二項式反演主要是應對類似於“恰好”,這樣的計數問題,然後主要是用到最後一種形式吧……