二项式反演及其应用

概念

二项式反演为一种反演形式,常用于通过 “指定某若干个” 求 “恰好若干个” 的问题。

注意:二项式反演虽然形式上和多步容斥极为相似,但它们并不等价,只是习惯上都称之为多步容斥。

引入

既然形式和多步容斥相似,我们就从多步容斥讲起。

我们都知道:|AB|=|A|+|B||AB| ,这其实就是容斥原理。

它的一般形式为:

|A1A2...An|=1in|Ai|1i<jn|AiAj|+...+(1)n1×|A1A2...An|

证明:

设某一元素被 m 个集合所包含,则其对左侧的贡献为 1
对右侧的贡献为 i=1m(1)i1(mi)=i=1m(1)i(mi)=1i=0m(1)i(mi)=1(11)m=1

故左侧等于右侧 ,证毕。

形式

形式零

沿用刚刚多步容斥的公式,记 Aic 表示 Ai 的补集,则将一般形式变形,可以得到:

|A1cA2c...Anc|=|S|1in|Ai|+1i<jn|AiAj|...+(1)n×|A1A2...An|

同时,由于补集的补集就是原集,因此又有:

|A1A2...An|=|S|1in|Aic|+1i<jn|AicAjc|...+(1)n×|A1cA2c...Anc|

考虑一种特殊情况:多个集合的交集大小只和集合的数目有关。

f(n) 表示 n 个补集的交集大小,g(n) 表示 n 个原集的大小,则两个公式分别可以写成:

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

显然这两个公式是等价关系,更是相互推导的关系,于是我们得到了二项式反演的形式零:

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

形式一

公式如下:

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

证明一

在形式零中,令 h(n)=(1)ng(n) ,则形式零就变为了:

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

整理后就是形式一。

证明二

将右侧代入左侧,则:

f(n)=i=0n(ni)j=0i(1)ij(ij)f(j)=i=0nj=0i(1)ij(ni)(ij)f(j)

考虑调换两个求和符号的顺序,即先枚举 i ,再枚举 j ,则又有:

f(n)=j=0nf(j)i=jn(1)ij(ni)(ij)

考虑 (ni)(ij) 的组合意义:从 n 个中选 i 个,再从 i 个中选 j 个。不妨反过来想,先从 n 个中选 j 个,再从剩下的 nj 个中选出 ij 个,即 (nj)(njij)

于是可以得到:

f(n)=j=0n(nj)f(j)i=jn(njij)(1)ij=j=0n(nj)f(j)t=0nj(njt)(1)t1njt=j=0n(nj)f(j)(11)nj

nj0 时,显然 (11)nj=0
nj=0 时,出现 00 不能直接计算,需要使用组合形式求解,此时 (njt)(1)t=1

t=0nj(njt)(1)t=[j=n] ,于是:

f(n)=(nn)f(n)=f(n)

左右恒等,证毕。

注:由于证明二并未用到 i0 开始这一性质,因此更通用的公式为:

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

形式二

这个形式和形式一类似,是最常用的公式。公式如下:

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

证明

将右侧代入左侧,则:

f(n)=i=nm(in)j=im(1)ji(ji)f(j)=i=nmj=im(1)ji(in)(ji)f(j)=j=nmf(j)i=nj(1)ji(in)(ji)=j=nm(jn)f(j)i=nj(jnji)(1)ji=j=nm(jn)f(j)t=0jn(jnt)(1)t1jnt=j=nm(jn)f(j)(11)jn=j=nm(jn)f(j)[j=n]=(nn)f(n)=f(n)

左右恒等,证毕。

组合意义

f(n) 表示 “钦定选 n 个”,g(n) 表示 “恰好选 n 个”,则对于任意的 ing(i)f(n) 中被计算了 (in) 次,故 f(n)=i=nm(in)g(i) ,其中 m 是数目上界。

注意:在定义中,f(n) 表示先钦定 n 个,再统计钦定情况如此的方案数,其中会包含重复的方案,因为一个方案可以有多种钦定情况。具体地,对于恰好选择 i 个,钦定情况数为 (in) ,故 g(i)f(n) 中被计算了 (in) 次。切勿将 f(n) 理解为普通的后缀和。

例题

[bzoj2839]集合计数

题目大意

一个有 n 个元素的集合有 2n 个不同子集(包含空集),现在要在这 2n 个集合中取出至少一个集合,使得它们的交集的元素个数为 k ,求取法的方案数模 109+7

1n1060kn

题解

对于稍有组合数学基础的人,通过直觉很容易列出式子 (ni)(22ni1) 。即钦定 i 个交集元素,则包含这 i 个的集合有 2ni 个;每个集合可选可不选,但不能都不选,由此可得此方案数。

接下来考虑上式与所求的关系:设 f(i) 表示钦定交集元素为某 i 个的方案数, g(i) 表示交集元素恰好为 i 个的方案数,则 (nk)(22nk1)=f(k)=i=kn(ik)g(i)

通过二项式反演求出 g(k)=i=kn(1)ik(ik)f(i)=i=kn(1)ik(ik)(ni)(22ni1)

使用一些预处理手段,时间复杂度 O(n)

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll mod = 1000000007;
ll fac[1000010] , ine[1000010] , fi[1000010] , pow[1000010];
ll choose(ll n , ll m)
{
    return fac[n] * fi[m] % mod * fi[n - m] % mod;
}
int main()
{
    int n , k , i;
    ll ans = 0;
    scanf("%d%d" , &n , &k);
    fac[0] = fac[1] = ine[1] = fi[0] = fi[1] = 1;
    for(i = 2 ; i <= n ; i ++ )
    {
        fac[i] = fac[i - 1] * i % mod;
        ine[i] = (mod - mod / i) * ine[mod % i] % mod;
        fi[i] = fi[i - 1] * ine[i] % mod;
    }
    pow[0] = 2;
    for(i = 1 ; i <= n ; i ++ ) pow[i] = pow[i - 1] * pow[i - 1] % mod;
    for(i = k ; i <= n ; i ++ )
    {
        if((i & 1) == (k & 1)) ans = (ans + choose(n , i) * choose(i , k) % mod * (pow[n - i] - 1 + mod)) % mod;
        else ans = (ans - choose(n , i) * choose(i , k) % mod * (pow[n - i] - 1 + mod) % mod + mod) % mod; 
    }
    printf("%lld\n" , (ans + mod) % mod);
    return 0;
}

[bzoj3622]已经没有什么好害怕的了

题目大意

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

题解

显然当 nk 为奇数时必然无解;当 nk 为偶数时,A>B 的对数恰好为 n+k2 ,记 m=n+k2

由于 “恰好” 这一限制不容易处理,考虑将其转化为 “钦定” 限制,进而通过二项式反演来处理。

先将 AB 从小到大排序,设 dp(i,j) 表示考虑了 A 的前 i 个数,钦定了 jA>B 的方案数。

讨论 Ai 的配对情况:若不配对,则方案数为 dp(i1,j) ;若配对,记 B 中比 Ai 小的数的个数为 cnt(i) ,则方案数为 dp(i1,j1)×(cnt(i)(j1))

dp(i,j)=dp(i1,j)+dp(i1,j1)×(cnt(i)(j1))

f(i) 表示钦定 iA>B 的方案数,g(i) 表示恰好 iA>B 的方案数,则 (nm)!×dp(n,m)=f(m)=i=mn(im)g(i)

g(m)=i=mn(1)im(im)f(i)=i=mn(1)im(im)(ni)!×dp(n,i)

时间复杂度 O(n2)

代码

#include <cstdio>
#include <algorithm>
#define N 2010
#define mod 1000000009
using namespace std;
typedef long long ll;
int a[N] , b[N];
ll c[N][N] , fac[N] , f[N][N];
int main()
{
    int n , k , i , j , p = 0;
    ll ans = 0;
    scanf("%d%d" , &n , &k);
    if((n ^ k) & 1)
    {
        puts("0");
        return 0;
    }
    k = (n + k) >> 1;
    for(i = 1 ; i <= n ; i ++ ) scanf("%d" , &a[i]);
    for(i = 1 ; i <= n ; i ++ ) scanf("%d" , &b[i]);
    sort(a + 1 , a + n + 1) , sort(b + 1 , b + n + 1);
    for(i = 0 ; i <= n ; i ++ ) f[i][0] = 1;
    for(i = 1 ; i <= n ; i ++ )
    {
        while(p < n && b[p + 1] < a[i]) p ++ ;
        for(j = 1 ; j <= n ; j ++ )
            f[i][j] = (f[i - 1][j] + f[i - 1][j - 1] * (p - j + 1)) % mod;
    }
    c[0][0] = fac[0] = 1;
    for(i = 1 ; i <= n ; i ++ )
    {
        c[i][0] = 1 , fac[i] = fac[i - 1] * i % mod;;
        for(j = 1 ; j <= i ; j ++ )
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
    }
    for(i = k ; i <= n ; i ++ )
    {
        if((i ^ k) & 1) ans = (ans - c[i][k] * f[n][i] % mod * fac[n - i] % mod + mod) % mod;
        else ans = (ans + c[i][k] * f[n][i] % mod * fac[n - i]) % mod;
    }
    printf("%lld\n" , ans);
    return 0;
}

[bzoj4710]分特产

题目大意

n 个人和 m 种物品,第 i 种物品有 ai 个,同种物品之间没有区别。现在要将这些物品分给这些人,使得每个人至少分到一个物品,求方案数模 109+7

题解

对于多种物品的情况,“每个人至少分到一个物品” 是一个非常棘手的条件,考虑将其转化为 “恰好 0 个人没有分到物品” ,并用二项式反演来解决。

f(i) 表示钦定 i 个人没有分到物品的方案数,g(i) 表示恰好 i 个人没有分到物品的方案数,则在 f(t) 中,对于第 i 种物品,分配时相当于 ai 个物品分给 nt 个人,方案数为 (nt+ai1ai1) , 于是 (nt)i=1m(nt+ai1ai1)=f(t)=i=tn(it)g(i)

g(t)=i=tn(1)it(it)f(i)=i=tn(1)it(it)(ni)j=1m(ni+aj1aj1)

最终的答案 g(0)=i=0n(1)i(ni)j=1m(ni+aj1aj1)

时间复杂度 O(n2+nm)

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 2010
using namespace std;
typedef long long ll;
const ll mod = 1000000007;
ll c[N][N];
int w[N];
int main()
{
    int n , m , i , j;
    ll ans = 0 , tmp;
    for(i = 0 ; i <= 2000 ; i ++ )
    {
        c[i][0] = 1;
        for(j = 1 ; j <= i ; j ++ )
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
    }
    scanf("%d%d" , &n , &m);
    for(i = 1 ; i <= m ; i ++ ) scanf("%d" , &w[i]);
    for(i = 0 ; i < n ; i ++ )
    {
        tmp = c[n][i];
        for(j = 1 ; j <= m ; j ++ )
            tmp = tmp * c[w[j] + n - i - 1][w[j]] % mod;
        if(i & 1) ans = (ans - tmp + mod) % mod;
        else ans = (ans + tmp) % mod;
    }
    printf("%lld\n" , ans);
    return 0;
}
posted @   GXZlegend  阅读(17993)  评论(13编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示