二项式反演及其应用
概念
二项式反演为一种反演形式,常用于通过 “指定某若干个” 求 “恰好若干个” 的问题。
注意:二项式反演虽然形式上和多步容斥极为相似,但它们并不等价,只是习惯上都称之为多步容斥。
引入
既然形式和多步容斥相似,我们就从多步容斥讲起。
我们都知道: ,这其实就是容斥原理。
它的一般形式为:
证明:
设某一元素被 个集合所包含,则其对左侧的贡献为 ;
对右侧的贡献为 。
故左侧等于右侧 ,证毕。
形式
形式零
沿用刚刚多步容斥的公式,记 表示 的补集,则将一般形式变形,可以得到:
同时,由于补集的补集就是原集,因此又有:
考虑一种特殊情况:多个集合的交集大小只和集合的数目有关。
记 表示 个补集的交集大小, 表示 个原集的大小,则两个公式分别可以写成:
显然这两个公式是等价关系,更是相互推导的关系,于是我们得到了二项式反演的形式零:
形式一
公式如下:
证明一
在形式零中,令 ,则形式零就变为了:
整理后就是形式一。
证明二
将右侧代入左侧,则:
考虑调换两个求和符号的顺序,即先枚举 ,再枚举 ,则又有:
考虑 的组合意义:从 个中选 个,再从 个中选 个。不妨反过来想,先从 个中选 个,再从剩下的 个中选出 个,即 。
于是可以得到:
当 时,显然 ;
当 时,出现 不能直接计算,需要使用组合形式求解,此时 。
故 ,于是:
左右恒等,证毕。
注:由于证明二并未用到 从 开始这一性质,因此更通用的公式为:
形式二
这个形式和形式一类似,是最常用的公式。公式如下:
证明
将右侧代入左侧,则:
左右恒等,证毕。
组合意义
记 表示 “钦定选 个”, 表示 “恰好选 个”,则对于任意的 , 在 中被计算了 次,故 ,其中 是数目上界。
注意:在定义中, 表示先钦定 个,再统计钦定情况如此的方案数,其中会包含重复的方案,因为一个方案可以有多种钦定情况。具体地,对于恰好选择 个,钦定情况数为 ,故 在 中被计算了 次。切勿将 理解为普通的后缀和。
例题
[bzoj2839]集合计数
题目大意
一个有 个元素的集合有 个不同子集(包含空集),现在要在这 个集合中取出至少一个集合,使得它们的交集的元素个数为 ,求取法的方案数模 。
, 。
题解
对于稍有组合数学基础的人,通过直觉很容易列出式子 。即钦定 个交集元素,则包含这 个的集合有 个;每个集合可选可不选,但不能都不选,由此可得此方案数。
接下来考虑上式与所求的关系:设 表示钦定交集元素为某 个的方案数, 表示交集元素恰好为 个的方案数,则 。
通过二项式反演求出 。
使用一些预处理手段,时间复杂度 。
代码
#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]已经没有什么好害怕的了
题目大意
给出两个长度均为 的序列 和 ,保证这 个数互不相同。现要将 序列中的数与 序列中的数两两配对,求 “ 的对数比 的对数恰好多 ” 的配对方案数模 。
题解
显然当 为奇数时必然无解;当 为偶数时, 的对数恰好为 ,记 。
由于 “恰好” 这一限制不容易处理,考虑将其转化为 “钦定” 限制,进而通过二项式反演来处理。
先将 和 从小到大排序,设 表示考虑了 的前 个数,钦定了 对 的方案数。
讨论 的配对情况:若不配对,则方案数为 ;若配对,记 中比 小的数的个数为 ,则方案数为 。
故 。
设 表示钦定 对 的方案数, 表示恰好 对 的方案数,则 。
故 。
时间复杂度 。
代码
#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]分特产
题目大意
有 个人和 种物品,第 种物品有 个,同种物品之间没有区别。现在要将这些物品分给这些人,使得每个人至少分到一个物品,求方案数模 。
题解
对于多种物品的情况,“每个人至少分到一个物品” 是一个非常棘手的条件,考虑将其转化为 “恰好 个人没有分到物品” ,并用二项式反演来解决。
设 表示钦定 个人没有分到物品的方案数, 表示恰好 个人没有分到物品的方案数,则在 中,对于第 种物品,分配时相当于 个物品分给 个人,方案数为 , 于是 。
故 。
最终的答案 。
时间复杂度 。
代码
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架