浅谈二项式反演
stripe_python · · 算法·理论
摘自 OI 中的数学基础第 11 章。
11. 二项式反演
11.1 原理
设
已知
反着做却是困难的,这个过程就叫二项式反演。有公式:
这是二项式反演的形式之一。二项式反演的作用就是把“恰好”转化为“钦定”。
*11.2 证明
我们先给出一个引理
组合意义和代数推导都易证。
把上述式子展开:
根据引理可得
作换元,令
由组合数性质 6 可得当且仅当
上述变换都是等价变换,证毕。
11.3 常用形式
下面我们给出二项式反演的全部形式:
形式一:
这是二项式反演的基本公式。
形式二:
这是钦定意义下的“至少”转“恰好”。
形式三:
这是钦定意义下的“至多”转“恰好”。
形式四:
这是选择意义下的“至多”转“恰好”。
形式五:
这是选择意义下的“至少”转“恰好”。
二项式反演的本质是系数特殊的容斥原理。
11.4 例题
二项式反演的题目中,最常用的是形式二。同时有些题转成“钦定”以后,还要配合计数 DP 求解。
在二项式反演时,首先要找到“恰好”,然后分清“钦定”和“选择”的区别,使用恰当的反演形式。
AT_abc423_f [ABC423F] Loud Cicada
设
解释一下,对于集合
由二项式反演形式二得
复杂度 __int128。
#define popcount __builtin_popcount
const int N = 25;
int n, m;
long long y, a[N], g[N], c[N][N];
void _main() {
cin >> n >> m >> y;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 0; i <= n; i++) c[i][0] = 1, c[i][i] = 1;
for (int i = 0; i <= n; i++) {
for (int j = 1; j < i; j++) c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
}
for (int s = 0; s < (1 << n); s++) {
__int128 x = 1;
for (int i = 1; i <= n; i++) {
if (s >> (i - 1) & 1) x = x / __gcd<__int128>(x, a[i]) * a[i];
if (x > y) break; // 注意这句
} g[popcount(s)] += y / x;
}
long long res = 0;
for (int i = m; i <= n; i++) {
if ((m - i) & 1) res -= c[i][m] * g[i];
else res += c[i][m] * g[i];
} cout << res;
}
P10596 BZOJ2839 集合计数
看到不好求的“恰好”,考虑二项式反演。设
那么对于
由二项式反演形式二得
复杂度
const int N = 1e6 + 5;
mint fac[N], ifac[N], g[N];
mint C(int n, int m) {
return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m];
}
int n, k;
void _main() {
cin >> n >> k;
fac[0] = ifac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
for (int i = 0; i <= n; i++) g[i] = C(n, i) * (mint(2).pow(mod32<1000000006>(2).pow(n - i).val) - 1);
mint res = 0;
for (int i = k; i <= n; i++) {
if ((i - k) & 1) res -= C(i, k) * g[i];
else res += C(i, k) * g[i];
} cout << res;
}
P6521 [CEOI 2010] pin (day2)
首先做一个等价变形,把不同变成相同,令
将
套路地,钦定至少
const int N = 5e4 + 5;
int n, m;
long long g[5];
char a[N][5];
const int fac[] = {1, 1, 2, 6, 24};
int C(int n, int m) {return n < m ? 0 : fac[n] / fac[m] / fac[n - m];}
void _main() {
cin >> n >> m, m = 4 - m;
for (int i = 1; i <= n; i++) cin >> (a[i] + 1);
for (int s = 0; s < (1 << 4); s++) {
umap<uint64_t, int> mp;
for (int i = 1; i <= n; i++) {
uint64_t h = 0;
for (int j = 1; j <= 4; j++) {
h *= 13331;
if (s >> (j - 1) & 1) h += a[i][j];
}
g[popcount(s)] += mp[h], mp[h]++;
}
}
long long res = 0;
for (int k = m; k <= 4; k++) {
if ((k - m) & 1) res -= C(k, m) * g[k];
else res += C(k, m) * g[k];
} cout << res;
}
P6076 [JSOI2015] 染色问题
将“每一”变成“恰好”,用二项式反演的思维来思考。
先处理颜色的限制,不妨计算选择至多出现
类似地处理掉行列限制后:问题变成:一个
重复应用三次二项式反演形式四,得到答案为
复杂度
const int N = 405;
mint fac[N], ifac[N];
mint C(int n, int m) {return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m];}
int n, m, c;
void _main() {
fac[0] = ifac[0] = 1;
for (int i = 1; i < N; i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
cin >> n >> m >> c;
mint res = 0;
for (int k = 0; k <= c; k++) {
mint a1 = 0;
for (int j = 0; j <= m; j++) {
mint a2 = 0;
for (int i = 0; i <= n; i++) {
if ((n - i) & 1) a2 -= C(n, i) * mint(k + 1).pow(i * j);
else a2 += C(n, i) * mint(k + 1).pow(i * j);
}
if ((m - j) & 1) a1 -= C(m, j) * a2;
else a1 += C(m, j) * a2;
}
if ((c - k) & 1) res -= C(c, k) * a1;
else res += C(c, k) * a1;
} cout << res;
}
P5505 [JSOI2011] 分特产
正难则反,记
需要求出
逆元法预处理组合数即可,复杂度
const int N = 2005;
mint fac[N], ifac[N];
mint C(int n, int m) {return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m]; }
int n, m, a[N];
mint f(int x) {
mint res = 1;
for (int k = 1; k <= m; k++) res *= C(a[k] + n - x - 1, n - x - 1);
return res;
}
void _main() {
fac[0] = ifac[0] = 1;
for (int i = 1; i < N; i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
cin >> n >> m;
for (int i = 1; i <= m; i++) cin >> a[i];
mint res = 0;
for (int i = 0; i < n; i++) {
if (i & 1) res -= C(n, i) * f(i);
else res += C(n, i) * f(i);
} cout << res;
}
CF1228E Another Filling the Grid
我们给出二项式反演形式二的二维形式:
特判
直接做即可。复杂度
const int N = 255;
int n, k;
mint fac[N], ifac[N];
mint C(int n, int m) {return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m];}
mint f(int x, int y) {
return mint(k - 1).pow(n * n - (n - x) * (n - y)) * mint(k).pow((n - x) * (n - y));
}
void _main() {
cin >> n >> k;
if (k == 1) return cout << 1, void();
fac[0] = ifac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
mint res = 0;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
if ((i + j) & 1) res -= C(n, i) * C(n, j) * f(i, j);
else res += C(n, i) * C(n, j) * f(i, j);
}
} cout << res;
}
AT_abc172_e [ABC172E] NEQ
其实可以用第 12 章错排的方法解决,放在这里有点大炮打蚊子了。
设
答案即为
此时二项式反演退化为普通容斥原理。容易得到
复杂度
const int N = 5e5 + 5;
int n, m;
mint fac[N], ifac[N];
mint A(int n, int m) {return n < m ? 0 : fac[n] * ifac[n - m];}
mint C(int n, int m) {return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m];}
mint f(int x) {
return C(n, x) * A(m, x) * A(m - x, n - x) * A(m - x, n - x);
}
void _main() {
cin >> n >> m;
fac[0] = ifac[0] = 1;
for (int i = 1; i <= max(n, m); i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
mint res = 0;
for (int i = 0; i <= n; i++) {
if (i & 1) res -= f(i);
else res += f(i);
} cout << res;
}
*P4859 已经没有什么好害怕的了
形式化题意:给出长为
设
现在的问题就是求
可以看看下文的排列计数 DP,从插入角度考虑
于是我们
就是在有序的 DP 基础上考虑剩下
二项式反演中组合数可以逆元法来求,总复杂度
const int N = 2005;
int n, m, k, a[N], b[N], last[N];
mint fac[N], ifac[N], dp[N][N], g[N];
mint C(int n, int m) {
if (n < m) return 0;
return fac[n] * ifac[m] * ifac[n - m];
}
void _main() {
cin >> n >> k;
if ((n + k) & 1) return cout << 0, void();
fac[0] = ifac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
m = (n + k) / 2;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
sort(a + 1, a + n + 1), sort(b + 1, b + n + 1);
for (int i = 1; i <= n; i++) last[i] = lower_bound(b + 1, b + n + 1, a[i]) - b - 1;
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
dp[i][0] = dp[i - 1][0];
for (int j = 1; j <= i; j++) {
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] * max(0, last[i] - j + 1);
}
}
for (int i = 0; i <= n; i++) g[i] = dp[n][i] * fac[n - i];
mint res = 0;
for (int i = m; i <= n; i++) {
if ((i - m) & 1) res -= C(i, m) * g[i];
else res += C(i, m) * g[i];
} cout << res;
}
P10597 BZOJ4665 小 w 的喜糖
和 P4859 很像。设
所求为
我们再一次看到了二项式反演退化为普通容斥原理。考虑求
记第
设
则
const int N = 2005;
mint fac[N], ifac[N], dp[N][N];
mint C(int n, int m) {return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m];}
int n, a[N], c[N];
void _main() {
fac[0] = ifac[0] = 1;
for (int i = 1; i < N; i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], c[a[i]]++;
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= n; j++) {
for (int k = 0; k <= min(c[i], j); k++) dp[i][j] += dp[i - 1][j - k] * C(c[i], k) * ifac[c[i] - k];
}
}
mint res = 0;
for (int i = 0; i <= n; i++) {
if (i & 1) res -= dp[n][i] * fac[n - i];
else res += dp[n][i] * fac[n - i];
} cout << res;
}
*P6478 [NOI Online #2 提高组] 游戏
“恰好
考虑求
考虑对于儿子
还可以选择一个点与
其中
这是一个树形背包,且应该跑 01 背包。根据树形背包的复杂度证明,复杂度是严格
这样有
const int N = 5005;
int n, m, u, v, belong[N];
char c;
int tot = 0, head[N];
struct Edge {
int next, to;
} edge[N << 1];
inline void add_edge(int u, int v) {
edge[++tot].next = head[u], edge[tot].to = v, head[u] = tot;
}
mint f[N], g[N], fac[N], ifac[N];
mint C(int n, int m) {return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m];}
int sz[N], cnt[2][N];
mint dp[N][N];
void dfs(int u, int fa) {
sz[u] = 1, cnt[belong[u]][u] = 1, dp[u][0] = 1;
for (int j = head[u]; j != 0; j = edge[j].next) {
int v = edge[j].to;
if (v == fa) continue;
dfs(v, u);
vector<mint> f(min(sz[u] + sz[v], m), 0);
for (int j = 0; j <= min(sz[u], m); j++) {
for (int k = 0; k <= min(sz[v], m - j); k++) {
f[j + k] += dp[u][j] * dp[v][k];
}
}
sz[u] += sz[v], cnt[0][u] += cnt[0][v], cnt[1][u] += cnt[1][v];
copy(f.begin(), f.end(), dp[u]);
}
for (int i = cnt[belong[u] ^ 1][u]; i >= 0; i--) {
dp[u][i + 1] += dp[u][i] * (cnt[belong[u] ^ 1][u] - i);
}
}
void _main() {
fac[0] = ifac[0] = 1;
for (int i = 1; i < N; i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
cin >> n, m = n / 2;
for (int i = 1; i <= n; i++) cin >> c, belong[i] = c ^ 48;
for (int i = 1; i < n; i++) {
cin >> u >> v;
add_edge(u, v), add_edge(v, u);
}
dfs(1, -1);
for (int i = 0; i <= m; i++) g[i] = dp[1][i] * fac[m - i];
for (int i = 0; i <= m; i++) {
for (int j = i; j <= m; j++) {
if ((j - i) & 1) f[i] -= C(j, i) * g[j];
else f[i] += C(j, i) * g[j];
}
cout << f[i] << '\n';
}
}
*P3270 [JLOI2016] 成绩比较
前置知识:第 24 章 Lagrange 插值求自然数幂和。
设
考虑求
复杂度
考虑消去值域限制。开始大力推柿子:
设
则
只要处理出
const int N = 105;
int n, m, k, u[N], r[N];
mint h[N], fac[N], ifac[N];
mint C(int n, int m) {return n < m ? 0 : fac[n] * ifac[m] * ifac[n - m];}
mint g(int k) {
mint res = C(n - 1, k);
for (int i = 1; i <= m; i++) res *= C(n - k - 1, r[i] - 1) * h[i];
return res;
}
namespace Lagrange {
mint a[N], p[N], s[N];
mint solve(int n, int k) {
p[0] = 1, s[k + 3] = 1;
for (int i = 1; i <= k + 2; i++) a[i] = a[i - 1] + mint(i).pow(k), p[i] = p[i - 1] * (n - i);
for (int i = k + 2; i >= 1; i--) s[i] = s[i + 1] * (n - i);
if (n <= k + 2) return a[n];
mint res = 0;
for (int i = 1; i <= k + 2; i++) {
mint cur = a[i] * p[i - 1] * s[i + 1] * ifac[i - 1] * ifac[k + 2 - i];
if ((k + 2 - i) & 1) res -= cur;
else res += cur;
} return res;
}
} using Lagrange::solve;
void _main() {
fac[0] = ifac[0] = 1;
for (int i = 1; i < N; i++) fac[i] = fac[i - 1] * i, ifac[i] = ~fac[i];
cin >> n >> m >> k;
for (int i = 1; i <= m; i++) cin >> u[i];
for (int i = 1; i <= m; i++) cin >> r[i];
for (int i = 1; i <= m; i++) {
for (int k = 0; k < r[i]; k++) {
mint cur = C(r[i] - 1, k) * mint(u[i]).pow(k) * solve(u[i], n - k - 1);
if ((r[i] - k - 1) & 1) h[i] -= cur;
else h[i] += cur;
}
}
mint res = 0;
for (int i = k; i <= n; i++) {
if ((i - k) & 1) res -= C(i, k) * g(i);
else res += C(i, k) * g(i);
} cout << res;
}