二项式反演及其应用

概念

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

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

引入

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

我们都知道:(|Acup B|=|A|+|B|-|Acap B|) ,这其实就是容斥原理。

它的一般形式为:

[|A_1cup A_2cup...cup A_n|=sumlimits_{1le ile n}|A_i|-sumlimits_{1le i<jle n}|A_icap A_j|+...+(-1)^{n-1} imes |A_1cap A_2cap ...cap A_n| ]

证明:

设某一元素被 (m) 个集合所包含,则其对左侧的贡献为 (1)
对右侧的贡献为 (sumlimits_{i=1}^m(-1)^{i-1}{mchoose i}=-sumlimits_{i=1}^m(-1)^i{mchoose i}=1-sumlimits_{i=0}^m(-1)^i{mchoose i}=1-(1-1)^m=1)

故左侧等于右侧 ,证毕。

形式

形式零

沿用刚刚多步容斥的公式,记 (A_i^c) 表示 (A_i) 的补集,则将一般形式变形,可以得到:

[|A_1^ccap A_2^ccap ...cap A_n^c|=|S|-sumlimits_{1le ile n}|A_i|+sumlimits_{1le i<jle n}|A_icap A_j|-...+(-1)^n imes |A_1cap A_2cap ...cap A_n| ]

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

[|A_1cap A_2cap ...cap A_n|=|S|-sumlimits_{1le ile n}|A_i^c|+sumlimits_{1le i<jle n}|A_i^ccap A_j^c|-...+(-1)^n imes |A_1^ccap A_2^ccap ...cap A_n^c| ]

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

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

[f(n)=sumlimits_{i=0}^n(-1)^i{nchoose i}g(i)\ g(n)=sumlimits_{i=0}^n(-1)^i{nchoose i}f(i) ]

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

[f(n)=sumlimits_{i=0}^n(-1)^i{nchoose i}g(i)Leftrightarrow g(n)=sumlimits_{i=0}^n(-1)^i{nchoose i}f(i) ]

形式一

公式如下:

[f(n)=sumlimits_{i=0}^n{nchoose i}g(i)Leftrightarrow g(n)=sumlimits_{i=0}^n(-1)^{n-i}{nchoose i}f(i) ]

证明一

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

[f(n)=sumlimits_{i=0}^n{nchoose i}h(i)Leftrightarrow frac{h(n)}{(-1)^n}=sumlimits_{i=0}^n(-1)^i{nchoose i}f(i) ]

整理后就是形式一。

证明二

将右侧代入左侧,则:

[egin{split} f(n)&=sumlimits_{i=0}^n{nchoose i}sumlimits_{j=0}^i(-1)^{i-j}{ichoose j}f(j)\&=sumlimits_{i=0}^nsumlimits_{j=0}^i(-1)^{i-j}{nchoose i}{ichoose j}f(j) end{split} ]

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

[f(n)=sumlimits_{j=0}^nf(j)sumlimits_{i=j}^n(-1)^{i-j}{nchoose i}{ichoose j} ]

考虑 ({nchoose i}{ichoose j}) 的组合意义:从 (n) 个中选 (i) 个,再从 (i) 个中选 (j) 个。不妨反过来想,先从 (n) 个中选 (j) 个,再从剩下的 (n-j) 个中选出 (i-j) 个,即 ({nchoose j}{n-jchoose i-j})

于是可以得到:

[egin{split} f(n)&=sumlimits_{j=0}^n{nchoose j}f(j)sumlimits_{i=j}^n{n-jchoose i-j}(-1)^{i-j}\ &=sumlimits_{j=0}^n{nchoose j}f(j)sumlimits_{t=0}^{n-j}{n-jchoose t}(-1)^t1^{n-j-t}\ &=sumlimits_{j=0}^n{nchoose j}f(j)(1-1)^{n-j} end{split} ]

(n-j eq 0) 时,显然 ((1-1)^{n-j}=0)
(n-j=0) 时,出现 (0^0) 不能直接计算,需要使用组合形式求解,此时 ({n-jchoose t}(-1)^{t}=1)

(sumlimits_{t=0}^{n-j}{n-jchoose t}(-1)^t=[j=n]) ,于是:

[f(n)={nchoose n}f(n)=f(n) ]

左右恒等,证毕。

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

[f(n)=sumlimits_{i=m}^n{nchoose i}g(i)Leftrightarrow g(n)=sumlimits_{i=m}^n(-1)^{n-i}{nchoose i}f(i) ]

形式二

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

[f(n)=sumlimits_{i=n}^m{ichoose n}g(i)Leftrightarrow g(n)=sumlimits_{i=n}^m(-1)^{i-n}{ichoose n}f(i) ]

证明

将右侧代入左侧,则:

[egin{split} f(n)&=sumlimits_{i=n}^m{ichoose n}sumlimits_{j=i}^m(-1)^{j-i}{jchoose i}f(j)\ &=sumlimits_{i=n}^msumlimits_{j=i}^m(-1)^{j-i}{ichoose n}{jchoose i}f(j)\ &=sumlimits_{j=n}^mf(j)sumlimits_{i=n}^j(-1)^{j-i}{ichoose n}{jchoose i}\ &=sumlimits_{j=n}^m{jchoose n}f(j)sumlimits_{i=n}^j{j-nchoose j-i}(-1)^{j-i}\ &=sumlimits_{j=n}^m{jchoose n}f(j)sumlimits_{t=0}^{j-n}{j-nchoose t}(-1)^{t}1^{j-n-t}\ &=sumlimits_{j=n}^m{jchoose n}f(j)(1-1)^{j-n}\ &=sumlimits_{j=n}^m{jchoose n}f(j)[j=n]\ &={nchoose n}f(n)\ &=f(n) end{split} ]

左右恒等,证毕。

组合意义

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

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

例题

[bzoj2839]集合计数

题目大意

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

(1le nle 10^6)(0le kle n)

题解

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

接下来考虑上式与所求的关系:设 (f(i)) 表示钦定交集元素为某 (i) 个的方案数, (g(i)) 表示交集元素恰好为 (i) 个的方案数,则 ({nchoose k}(2^{2^{n-k}-1})=f(k)=sumlimits_{i=k}^n{nchoose i}g(i))

通过二项式反演求出 (g(k)=sumlimits_{i=k}^n(-1)^{i-k}{ichoose k}f(i)=sumlimits_{i=k}^n(-1)^{i-k}{ichoose k}{nchoose i}(2^{2^{n-i}}-1))

使用一些预处理手段,时间复杂度 (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
" , (ans + mod) % mod);
    return 0;
}

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

题目大意

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

题解

显然当 (n-k) 为奇数时必然无解;当 (n-k) 为偶数时,(A>B) 的对数恰好为 (frac{n+k}2) ,记 (m=frac{n+k}2)

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

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

讨论 (A_i) 的配对情况:若不配对,则方案数为 (dp(i-1,j)) ;若配对,记 (B) 中比 (A_i) 小的数的个数为 (cnt(i)) ,则方案数为 (dp(i-1,j-1) imes (cnt(i)-(j-1)))

(dp(i,j)=dp(i-1,j)+dp(i-1,j-1) imes(cnt(i)-(j-1)))

(f(i)) 表示钦定 (i)(A>B) 的方案数,(g(i)) 表示恰好 (i)(A>B) 的方案数,则 ((n-m)! imes dp(n,m)=f(m)=sumlimits_{i=m}^n{ichoose m}g(i))

(g(m)=sumlimits_{i=m}^n(-1)^{i-m}{ichoose m}f(i)=sumlimits_{i=m}^n(-1)^{i-m}{ichoose m}(n-i)! imes dp(n,i))

时间复杂度 (O(n^2))

代码

#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
" , ans);
    return 0;
}

[bzoj4710]分特产

题目大意

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

题解

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

(f(i)) 表示钦定 (i) 个人没有分到物品的方案数,(g(i)) 表示恰好 (i) 个人没有分到物品的方案数,则在 (f(t)) 中,对于第 (i) 种物品,分配时相当于 (a_i) 个物品分给 (n-t) 个人,方案数为 ({n-t+a_i-1choose a_i-1}) , 于是 ({nchoose t}prodlimits_{i=1}^m{n-t+a_i-1choose a_i-1}=f(t)=sumlimits_{i=t}^n{ichoose t}g(i))

(g(t)=sumlimits_{i=t}^n(-1)^{i-t}{ichoose t}f(i)=sumlimits_{i=t}^n(-1)^{i-t}{ichoose t}{nchoose i}prodlimits_{j=1}^m{n-i+a_j-1choose a_j-1})

最终的答案 (g(0)=sumlimits_{i=0}^n(-1)^i{nchoose i}prodlimits_{j=1}^m{n-i+a_j-1choose a_j-1})

时间复杂度 (O(n^2+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
" , ans);
    return 0;
}
原文地址:https://www.cnblogs.com/GXZlegend/p/11407185.html