二项式反演学习笔记

二项式反演学习笔记

二项式反演(Binomial Inversion)是一种反演,它基于容斥原理.它可以把计数问题中求解"恰好X个的方案数"转化为求解"至少X个的方案数",让问题变得更简单。

约定:

  1. \(C_n^m\)表示从\(n\)个数里选\(m\)个数的方案数,等价于\(\binom{m}{n}\).另外为了方便推式子,规定\(n<m\)\(C_n^{m}=0\)
  2. \(S\)是一个集合,规定\(\overline{S}\)\(S\)的补集
  3. \(S\)是一个集合,规定\(|S|\)\(S\)的元素个数
  4. \([p(x)]=\begin{cases} 1,p(x) \\ 0, \neg p(x) \end{cases}\)

基本定理

二项式反演有3种基本形式:

\[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 \]

\[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 \]

\[\forall m \geq n,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 \]

接下来我们会证明它。

标准形式

定理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 \ \ \tag{1.1}$$

这个式子充满了对称性,接下来我们证明它

容斥解释

实际上,二项式反演是容斥原理的特例。
我们可以构造一些集合来证明这个定理,全集为\(S\),\(S\)中具有\(n\)种性质的集合分别为\(A_1,A_2 \dots A_n\).特别地,我们构造这些集合,使它们满足其中任意\(i\)个集合的并集大小相等,记为\(g_i\).另外规定\(g_0=|S|\)

那么根据容斥原理,均没有这些性质的集合的大小为:

\[\begin{aligned}|\overline{A_1} \cap \overline{A_2} \cap \cdots \cap \overline{A_n}|= |S| - \sum |A_i| + \sum |A_i \cap A_j| + \cdots + (-1)^n \sum |A_1 \cap A_2 \cap \cdots \cap A_n| \end{aligned} \]

又因为任意\(i\)个集合的并集大小相等

\[\begin{aligned}&|S| - \sum |A_i| + \sum |A_i \cap A_j| + \cdots + (-1)^n \sum |A_1 \cap A_2 \cap \cdots \cap A_n| \\ &=g_0-C_n^1 g_1+C_n^2g_2+ \dots + (-1)^n C_n^n g_n \end{aligned} \]

同理可得\(A_i\)的补集的并集大小相等,不妨记为\(f_i\),同样令\(f_0=|S|\).那么上式可化为$f_n = \sum_{i=0}^n (-1)^i C_n^i g_i $.
再使用一次容斥原理:

\[\begin{aligned} & & &|A_1 \cap A_2 \cap \cdots \cap A_n| \\ &=& &|S| - \sum |\overline{A_i}| + \sum |\overline{A_i} \cap \overline{A_j}| \\ & &+& \cdots + (-1)^n \sum |\overline{A_1} \cap \overline{A_2} \cap \cdots \cap \overline{A_n}|\\ &=& &f_0 - C_n^1 f_1 + C_n^2 f_2 + \cdots + (-1)^n C_n^n f_n \end{aligned}\]

那么$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 $
那么

\[\begin{aligned}\sum_{i=0}^n (-1)^i C_n^i f_i &=\sum_{i=0}^n (-1)^iC_n^i \sum_{j=0}^i(-1)^j C_i^j g_j \\ &=\sum_{j=0}^n g_j \sum_{i=j}^n (-1)^{i+j}C_{n}^i C_{i}^j\end{aligned} \]

第二步是交换了求和顺序,我们尝试对于每个\(f_j\)计算出它的系数并提到外面,容易发现对于\(j \in [1,i]\)都会乘上系数\((-1)^iC_n^i\),也就是说会被\(>=j\)\((-1)^iC_n^i\)更新。
那么这样只需要后面的和式在\(j \neq n\)时为0,\(j=n\)时为1,那么总和就为\(g_n\)

接下来需要用到组合数的性质

\[\begin{aligned} C_n^i C_j^i &=\frac{n!}{(n-i)!i!}\frac{i!}{(i-j)!j!} \\ &=\frac{n!}{(n-i)!(i-j)!j!} \\ &=\frac{n!(n-j)!}{j!(n-j)!(n-i)!(i-j)!} \ \ (\text{ 为了构造组合数,上下同乘}(n-j)!) \\ &=\frac{n!}{j!(n-j)!} \frac{(n-j)!}{(n-i)![(n-i)-(n-j)]!} \\ &= C_n^j C_{n-j}^{n-i} (1.1.2)\end{aligned} \]

代回上式

\[\begin{aligned} \sum_{i=j}^n (-1)^{i+j}C_{n}^i C_{i}^j &=(-1)^jC_n^j \sum_{i=j}^n (-1)^i C_{n-j}^{n-i} \\ &=(-1)^jC_n^j \sum_{i=0}^{n-j} (-1)^{n-i} C_{n-j}^{i} \ \ (\text{把}i\text{换成}n-i) \\ &= (-1)^jC_n^j (1-1)^{n-j} \ \ (\text{二项式定理})\end{aligned} \]

\(n \neq j\)\(\text{原式}=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\)
因此\(\text{原式}=[n=j]\)
也就是说

\[\begin{aligned}\sum_{i=0}^n (-1)^i C_n^i f_i =\sum_{j=0}^n g_j [n=j]=g_n\end{aligned} \]

至多和恰好的转化

定理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 \ \ \tag{1.2}$$

这个式子的意义是,\(f_n\)表示至多\(n\)个的方案数,那么\(g_n\)就表示恰好\(n\)个的方案数。

证明:
考虑形式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 \]

\(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)(-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 \ \ \tag{1.3}$$
对于\(\forall m \geq n\),结论都成立,即使是一个无限求和也可以

这个式子的意义是,\(f_n\)表示 "至少" \(n\)个的方案数,那么\(g_n\)就表示恰好\(n\)个的方案数.这里的 "至少"打引号是因为它不是一个简单的后缀和,它表示我们"钦定"\(n\)个数满足条件,剩下的数不一定满足条件\(f\)的时候每个\(g_i\)是会被重复计算的,计算了\(C_n^i\)次.

证明:

\[\begin{aligned} \sum_{i=n}^m (-1)^{i-n} C_i^n f_i &= \sum_{i=n}^m (-1)^{i-n} C_i^n \sum_{j=i}^m C_j^i f_j\end{aligned} \]

\((1.1)\)的证明,交换求和顺序

\[\begin{aligned} \text{原式} &= \sum_{j=n}^m f_j \sum_{i=n}^j (-1)^{i-n} C_{j}^i C_i^n\end{aligned} \]

注意到后面的\(\sum_{i=n}^j (-1)^{i-n} C_{j}^i C_i^n\)部分也很相似,因此可以用类似的方法处理。
代入组合数的性质\((1.1.2)\)

\[\begin{aligned} \sum_{i=n}^j (-1)^{i-n} C_{j}^i C_i^n &= \sum_{i=n}^k C_j^n C_{j-n}^{j-i} \\ &= (-1)^{-n}C_n^j \sum_{i=n}^j (-1)^i C_{j-n}^{j-i} \\ &= (-1)^{-n}C_n^j \sum_{i=0}^{j-n} (-1)^{j-i} C_{j-n}^{i} \\ &= (-1)^{-n}C_n^j (1-1)^{j-n} \\&=[n=j] \end{aligned} \]

容易发现这个证明过程只是把原来的\(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] \]

证明:
还是交换求和顺序

\[\sum_{i=0}^n b_{n,i} f_i=\sum_{i=0}^n b_{n,i} \sum_{j=0}^i a_{i,j}g_j=\sum_{j=1}^n g_j \sum_{i=j}^n b_{n,j} a_{i,j} \]

把字母换一下就得到了上面的定理

应用

在做题中,看到求"恰好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!\)

那么

\[\begin{aligned} \sum_{i=0}^n (-1)^{n-i} C_n^i f_i &= \sum_{i=0}^n (-1)^{n-i} \frac{n!}{i!(n-i)!}i! \\ &=\sum{i=0}^n n! \frac{(-1)^{n-i}}{(n-i)!} \\ &= n!\sum_{i=0}^n \frac{(-1)^i}{i!}\end{aligned} \]

min-max容斥

\(S\)为一个集合,\(\max(S),\min(S)\)分别表示集合中的最大值和最小值。特别地,规定\(\max(\emptyset)=\min(\emptyset)=0\),则$$\max(S)=\sum_{T\subset S}(-1)^{|T|-1} \min(T)$$

证明:
设存在一个以集合大小为自变量的函数\(f\)满足

\[\max(S)=\sum_{T\subset S}f(|T|) \min(T) \]

\(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\).所以必有

\[[i=1]=\sum_{j=0}^{i-1}C_{i-1}^j f(j+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}\),代回原式,得

\[\max(S)=\sum_{T\subset S}(-1)^{|T|-1} \min(T) \]

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\)个数。
因此

\[f_i=C_n^i (2^{2^{n-i}}-1) \]

根据二项式反演,答案为

\[\sum_{i=k}^n (-1)^{i-k}C_{i}^kf_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\n",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\n",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(\text{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\n",g[i]);
	} 
}

image.png

posted @ 2020-04-30 21:40  birchtree  阅读(1214)  评论(0编辑  收藏  举报