二项式反演学习笔记

前置芝士

二项式定理

(a+b)n=i=0n(ni)aibni

组合数的性质:

将选出的集合对全集取补集,数值不变:

(nm)=(nnm)(1)

根据定义可以推出:

(nk)=nk(n1k1)(2)

用杨辉三角的表达式,可以推出:

(nm)=(n1m1)+(n1m)(3)

取二项式定理中 a=b=1 的特殊情况,可以得到:

(n0)+(n1)+...+(nn)=i=0n(ni)=2n(4)

同理,取二项式定理中 a=1,b=1 的特殊情况,可以得到:

i=0n(1)i(ni)=[n==0](5)

拆式子,感性理解一下可以得到:

i=0m(ni)(mmi)=(n+mm)(6)

(6)n=m 的特殊情况,可以得到:

i=0n(ni)2=(2nn)(7)

通过对 (4) 所对应的多项式函数求导,可以得到:

i=0ni(ni)=n2n1(8)

1n+1 中选出 k+1 个元素,可以考虑枚举最大的元素的值,那么就得到等式:

i=0n(ik)=(n+1k+1)(9)

n 个元素中选取 r 个元素,再在 r 中选取 k 个元素,等价于先选 k 个元素,再在剩下的 nk 个元素中选取 rk 个元素:

(nr)(r[)k]=(nk)(nkrk)(10)

而从杨辉三角上也不难发现:

i=0n(nii)=Fn+1

其中 F 为斐波那契数列。

二项式反演

fn 表示恰好使用 n 个不同元素形成特定结构的方案数,gn 表示从 n 个不同元素中选出 i0 个元素形成特定结构的总方案数。

形式一:

g(n)=i=0n(ni)f(i)f(n)=i=0n(1)ni(ni)g(i)

形式二:

g(n)=i=nm(in)f(i)f(n)=i=nm(1)in(in)g(i)

上述已知 gnfn 的过程,即被称为二项式反演

证明:

此处证明形式一。

将反演的公式中的 gi 展开,得到:

fn=i=0n(ni)(1)ni[j=0i(ij)fj]

=i=0nj=0i(ni)(ij)(1)nifj

交换枚举顺序,得到:

fn=j=0ni=jn(ni)(ij)(1)nifj

=j=0nfji=jn(ni)(ij)(1)ni

利用前面得到的公式 (10),可以得到:

fn=j=0nfji=jn(nj)(njij)(1)ni

=j=0nfj(nj)i=jn(njij)(1)ni

k=ij,则 $i=k+j,上式转换为:

fn=j=0nfj(nj)k=0nj(njk)(1)njk

对后半部分式子利用公式 (5),可以得到:

fn=j=0nfj(nj)[n==j]=fn

证毕。

已经没有什么好害怕的了

给出两个长度均为 n 的序列 AB ,保证这 2n 个数互不相同。现要将 A 序列中的数与 B 序列中的数两两配对,求 “ A>B 的对数比 A<B 的对数恰好多 k ” 的配对方案数模 109+9

1n20000kn

思路

将题意转换一下,也就是求 A>B 的对数恰好为 n+k2

AB 从小到大排序,设 dpi,j 表示配对到 A 数组中的第 i 个数时,已经钦定了jA>B 的方案数。

cnti 表示 B 中有 cnti 个数小于 Ai,那么分是否要钦定当前的数进行转移:

dpi,j=dpi1,j+dpi1,j1×(cnti(j1))

最终 dpn,i×Anini 就为至少有 iA>B 的方案数。记为 g(i)

f(i)恰好iA>B 的方案数。那么就有:

gm=i=mn(im)f(i)

通过二项式反演,可以得到:

f(m)=i=mn(1)im(im)g(i)=i=mn(1)im(im)×dpn,i×Anini

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2010,mod=1e9+9;
int n,k,f[N][N],c[N][N],ans,a[N],b[N],cnt[N],fac[N];
void add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
void sub(int &a,int b){a-=b;if(a<0) a+=mod;}
int main()
{
	scanf("%d%d",&n,&k);if(n+k&1) return puts("0"),0;k=(n+k)/2;fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
	for(int i=0;i<=n;i++) for(int j=0;j<=i;j++) if(!j) c[i][j]=1;else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);sort(a+1,a+n+1);
    for(int i=1;i<=n;i++) scanf("%d",&b[i]);sort(b+1,b+n+1);
    for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) cnt[i]+=(a[i]>b[j]);f[0][0]=1;
    for(int i=1;i<=n;i++) for(int j=0;j<=i;j++) add(f[i][j],f[i-1][j]),add(f[i][j],1ll*f[i-1][j-1]*(cnt[i]-j+1)%mod);
    for(int i=k;i<=n;i++) if(i-k&1) sub(ans,1ll*c[i][k]*f[n][i]%mod*fac[n-i]%mod);else add(ans,1ll*c[i][k]*f[n][i]%mod*fac[n-i]%mod);
    printf("%d\n",ans);
	return 0;
}

游戏

小 A 和小 B 正在玩一个游戏:有一棵包含 n=2m 个点的有根树(点从 1n 编号),它的根是 1 号点,初始时两人各拥有 m 个点。游戏的每个回合两人都需要选出一个自己拥有且之前未被选过的点,若对手的点在自己的点的子树内,则该回合自己获胜;若自己的点在对方的点的子树内,该回合自己失败;其他情况视为平局。游戏共进行 m 回合。

为对于 k=0,1,2,,m,计算出非平局回合数为 k 的情况数。两种情况不同当且仅当存在一个小 A 拥有的点 x,小 B 在 x 被小 A 选择的那个回合所选择的点不同。

答案对 998244353 取模。

1n5000

思路:

考虑设 gi存在i 回合非平局的方案数,fi恰好i 回合非平局的方案数。不难得到 g 的表达式:

g(m)=i=mn(im)f(i)

根据二项式反演,得到 f 的表达式:

f(m)=i=mn(1)im(im)g(i)

dpi,j 表示在以 i 为根的子树中存在 j 个非平局的回合数。简单转移一下即可。

最终 gi=j=im(ji)dp1,jAmjmj

code:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=5010,mod=998244353;
int tmp[N],siz[N],siz1[N],h[N],idx,n,f[N][N],fac[N],c[N][N];char s[N];
struct edge{int v,nex;}e[N<<1];
void add(int u,int v){e[++idx]=edge{v,h[u]};h[u]=idx;}
void Add(int &a,int b){a+=b;if(a>=mod) a-=mod;}
void Sub(int &a,int b){a-=b;if(a<0) a+=mod;}
void dfs(int u,int fa)
{
	siz[u]=1;siz1[u]=s[u]-'0';f[u][0]=1;
	for(int i=h[u];i;i=e[i].nex)
	{
		int v=e[i].v;if(v==fa) continue;dfs(v,u);
		for(int j=0;j<=siz[u]+siz[v];j++) tmp[j]=0;
		for(int j=0;j<=siz[u];j++) for(int k=0;k<=siz[v];k++) if(j+k<=n) Add(tmp[j+k],1ll*f[u][j]*f[v][k]%mod);
		for(int j=0;j<=siz[u]+siz[v];j++) f[u][j]=tmp[j];
		siz[u]+=siz[v];siz1[u]+=siz1[v];
	}
	for(int i=min(siz1[u],siz[u]-siz1[u]);i;i--)
	    if(s[u]=='1') Add(f[u][i],1ll*f[u][i-1]*(siz[u]-siz1[u]-(i-1))%mod);
	    else Add(f[u][i],1ll*f[u][i-1]*(siz1[u]-(i-1))%mod);
}
int main()
{
	scanf("%d",&n);scanf("%s",s+1);for(int u,v,i=1;i<n;i++) scanf("%d%d",&u,&v),add(u,v),add(v,u);fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=1ll*fac[i-1]*i%mod;
	for(int i=0;i<=n;i++) for(int j=0;j<=i;j++) if(!j) c[i][j]=1;else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;dfs(1,0);
	for(int i=0;i<=n/2;i++)
	{
	    int ans=0;
		for(int j=i;j<=n/2;j++)
		    if(j-i&1) Sub(ans,1ll*c[j][i]*f[1][j]%mod*fac[n/2-j]%mod);
		    else Add(ans,1ll*c[j][i]*f[1][j]%mod*fac[n/2-j]%mod);
		
		printf("%d\n",ans);
	}
	return 0;
}
posted @   曙诚  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示