容斥原理
先从容斥原理开始。
容斥原理的结论如下:
证明的思路是考虑一个元素在每一个 \(\bigcap\limits_{i = 1}^{m}S_{a_{i}}\) 里出现的次数,然后通过一番暴算,我们能够发现每个元素都只出现了 \(1\) 次,这样,每个元素合起来就变成了总的并集。
详见 OI-wiki
二项式反演
反演的定义
现在我们有两个数组:\(g\) 和 \(f\)。而 \(f\) 和 \(g\) 之间有对应关系:
然而题目里不可能这么简单,一般是已知 \(f\) 让我们推 \(g\)。这种情况解个方程就可以。但是如果只有解方程的话,还不足以搞出反演这种东西出来,而上面的柿子在某些情况下会化出许多种优美的形式,所以才搞出了反演这种东西。
二项式反演的公式
我们可以从容斥原理推到二项式反演的公式。
有全集 \(U = S_{1} \cup S_{2} \cup S_{3} \cup \cdots \cup S_{n}\),且任意 \(i\) 个集合的交集和并集大小相同。设 \(f_{i}\) 表示任意 \(i\) 个集合的并集的大小,\(g_{i}\) 表示任意 \(i\) 个集合的补集的大小。特别的,\(g_{0} = f_{0} = |U|\)。根据补集的交集和原集的并集的容斥关系可以得出:
然而,我们惊奇的发现:\(|\bigcap\limits_{i = 1}^{n}S_{i}| = f_{n},|\bigcap\limits_{i = 1}^{n}\overline{S_{i}}| = g_{n}\),于是我们便得到了二项式反演的第一个公式:
如何用数学方法证明这个公式是正确的呢?直接代入就行。
将 \(f_{n} = \sum\limits_{i = 0}^{n}(-1)^{i}\binom{n}{i}g_{i}\) 代入:
显然成立。当然,这个柿子里还有一些不那么显然的东西
- \(\displaystyle\binom{n}{i}\binom{i}{j} = \binom{n}{j}\binom{n - j}{n - i}\)
这个可以用数学方法证明,也可组合意义的方法证明,这里主要说明组合意义的证法。
求 \(|U| = n,|A| = i,b = |j|,B \subseteq A \subseteq U\) 的方案数。这里有两种方法:
法一:先在 \(U\) 里取 \(i\) 个元素构成集合 \(A\),方案数为 \(\binom{n}{i}\),然后在 \(i\) 个元素里再取 \(j\) 个元素构成集合 \(B\),方案数为 \(\binom{i}{j}\),合起来就是 \(\binom{n}{i}\binom{i}{j}\)
法二:先在 \(U\) 里取 \(j\) 个元素构成集合 \(B\),方案数为 \(\binom{n}{j}\),再在剩下的 \(n - j\) 个元素里取 \(i - j\) 个元素,方案数为 \(\binom{n - j}{i - j}\),又有 \(\binom{n - j}{i - j} = \binom{n - j}{n - i}\),所以方案数就是 \(\binom{n}{j}\binom{n - j}{n - i}\)。
得证。 - \(\displaystyle\sum\limits_{i = 0}^{n - j}(-1)^{i}\binom{n - j}{i} = [j = n]\)
设 \(k = n - j\),令 \(k = 0\),原式的值为 \(1\),令 \(k > 0\),原式为 \(\sum\limits_{i = 0}^{k}\binom{k}{i}(-1)^{i}\),我们来添一个项:\(\sum\limits_{i = 0}^{k}\binom{k}{i}(-1)^{i} 1^{i}\),发现,这玩意不就是二项式定理的的右边那坨吗?那么原式就是 \((-1 + 1)^{k} = 0^{k} = 0\)。得证。
现在再看那个式子就比较好懂了。注意再第二点那里,因为数学老师告诉我们 \(0^{0}\) 没有意义,所以在证明的时候为了严谨需要分类讨论。在做题的时候,为了方便可以钦定 \(0^{0} = 1\)。
小结
二项式反演的 \(4\) 种形式:
形式一
形式二(形式一的变形,比较常用):
形式三:
形式四(形式三的变形,非常常用):
为什么形式四非常常用呢?因为题里多半都是求恰好满足一些条件的方案数。而我们好求的一般都是钦定满足一些条件的方案数。比如:有 \(m\) 个条件,\(g_{i}\) 表示恰好满足 \(i\) 个条件的方案数,\(f_{i}\) 表示钦定满足 \(i\) 个条件,剩下的随便的方案数。从 \(f_{i}\) 的表述里就可以看出,\(f_{i}\) 比 \(g_{i}\) 好求很多,我们就可以用 \(f\) 推得 \(g\)。
推导思路和证明思路来自 __allenge 的博客。
例题 for 暴力容斥
P1450 [HAOI2008] 硬币购物
我们先考虑没有限制的情况:\(dp_{i}\) 表示没有限制的情况下买价格为 \(i\) 的物品的方案数,容易发现这就是一个完全背包,可以在 \(\mathcal{O}(val)\) 的复杂度下预处理,其中 \(val\) 表示最大价格。然后接下来考虑限制的情况。发现正着考虑不超过限制的情况不太好做,正难则反。
我们考虑超过限制的情况。超过限制就是第 \(i\) 个硬币强制选 \(d_{i} + 1\) 个,剩下的依然随便选,那么情况数就是 \(dp_{s - (d_{i} + 1) \times c_{i}}\),答案就要减去 \(\sum\limits_{i = 1}^{4}dp_{s - (d_{i} + 1) \times c_{i}}\)。这是单个硬币超出限制的情况,如果有多个硬币,就得请出我们的容斥原理了。但是,容斥原理的公式要求任意两个集合的交集和并集大小相等,这很明显不相等啊,我们又发现:只有 \(4\) 种硬币,那我们直接枚举不就行了吗?无非就是带一个 \(16\) 的常数嘛。
核心代码:
int ans = dp[s];//dp是预处理好的完全背包
for (int k = 1; k < (1 << 4); k++)
{int tmp = s;for (int i = 0; i < 4; i++)if (k & (1 << i))tmp -= (d[i] + 1) * c[i];if (tmp >= 0)ans += (__builtin_popcount(k) & -1 : 1) * dp[tmp];//容斥
}
P6521 [CEOI2010 day2] pin
发现考虑不同的方案数有点难,正难则反。
我们设 \(cnt_i\) 为钦定有 \(i\) 位相同的方案数。这个可以用哈希求解。我们设哈希函数:
int hsh(char ch){return isdigit(ch) ? ch - '0' : ch - 'a' + 10;}
int hsh(char ch1,char ch2){return hsh(ch1) * 36 + hsh(ch2);}
int hsh(char ch1,char ch2,char ch3)
{return hsh(ch1) * 36 * 36 + hsh(ch2) * 36 + hsh(ch3);}
分别用于求解一个字符,两个字符,三个字符的哈希值。很明显,这个哈希函数生成的哈希值不会超过 \(6 \times 10 ^ 4\)。那么我们记录 \(tmp_{i}\) 表示有多少个字符序列的哈希值是 \(i\),然后要求解方案数呢,就相当于求解在 \(tmp_{i}\) 个元素里随便取两个不同元素的方案数,很明显是 \(\frac{tmp_{i} \times (tmp_{i} - 1)}{2}\)。最后,我们求得了钦定有 \(i\) 个元素相同的方案数,因为最多有 \(4\) 个元素相同,所以我们直接暴力容斥就行。
code:
for (int x = 1; x <= 4; x++)//一个字符相同的
{memset(tmp,0,sizeof tmp);for (int i = 1; i <= n; i++)tmp[hsh(s[i][x])] ++;for (int i = 0; i <= 6e4; i++)cnt[1] += (tmp[i] * (tmp[i] - 1)) >> 1;
}
for (int x = 1; x <= 4; x++)//两个字符相同
{for (int y = x + 1; y <= 4; y++){memset(tmp,0,sizeof tmp);for (int i = 1; i <= n; i++)tmp[hsh(s[i][x],s[i][y])] ++;for (int i = 0; i <= 6e4; i++)cnt[2] += (tmp[i] * (tmp[i] - 1)) >> 1;}
}
for (int x = 1; x <= 4; x++)//三个字符相同
{for (int y = x + 1; y <= 4; y++){for (int z = y + 1; z <= 4; z++){memset(tmp,0,sizeof tmp);for (int i = 1; i <= n; i++)tmp[hsh(s[i][x],s[i][y],s[i][z])] ++;for (int i = 0; i <= 6e4; i++)cnt[3] += (tmp[i] * (tmp[i] - 1)) >> 1;}}
}
//以下是暴力容斥
ans[3] = cnt[3];
ans[2] = cnt[2] - 3 * ans[3];
ans[1] = cnt[1] - ans[2] * 2 - ans[3] * 3;
ans[0] = ((n * (n - 1)) >> 1) - ans[1] - ans[2] - ans[3];
printf("%lld",ans[4 - d]);//由于求解的是相同的,所以不同的就是4 - d个
例题 for 二项式反演
P10986 [蓝桥杯 2023 国 Python A] 2023
设 \(f_{m}\) 表示钦定有 \(m\) 个 2023
的方案数。用插板法,在这些 2023
的缝里插入那些乱七八糟的数。在 \(m\) 个 2023
中插入 \(n - 4m\) 个数的方案是
设 \(g_{m}\) 表示恰好有 \(m\) 个 2023
的方案数,那么有:
根据二项式反演公式,我们得到:
那么我们就求得了 \(g_{m}\),就是答案。时间复杂度 \(\mathcal{O}(n)\)
BZOJ2839 集合计数
设 \(f_{k}\) 表示钦定有 \(k\) 个相同元素的情况,那么,剩下的 \(n - k\) 个元素能够组成 \(2^{n - k}\) 个集合,而,这 \(2^{n - k}\) 个集合又能够再组成 \(2^{2^{n - k}}\) 个集合。那么显然有
设 \(g_{k}\) 表示恰好有 \(k\) 个相同元素的情况,那么有:
反演一下得到:
我们就得到了 \(g_{k}\)。时间复杂度 \(\mathcal{O}(n)\)。
P5505 [JSOI2011] 分特产
首先设 \(f_{i}\) 表示钦定有 \(i\) 个同学没有拿到特产的方案数,那么有:
后面那坨表示将 \(a_{j}\) 个物品分成 \(n - i\) 个可空集的方案数,而前面的是因为我们并没有钦定哪几个人没有,所以要乘一个在 \(n\) 个人里面选 \(i\) 个的方案数。
接下来我们开始反演。设 \(g_{i}\) 表示正好有 \(i\) 个人没有,那么有反演公式:
而我们要让每一个人都拿到,答案就是 \(g_{0}\)。这样,我们就做完了。时间复杂度 \(\mathcal{O}(n)\)
BZOJ4665 小 w 的喜糖
依然是正难则反 + 二项式反演 演都不带演
设 \(f_{i}\) 表示钦定有 \(i\) 个位置相同,\(g_{i}\) 表示正好 \(i\) 个位置相同。那么有反演:
我们的答案就是 \(g_{0}\)
现在的问题就在于怎么求 \(f_{i}\)。发现初始数组的顺序对于答案来说没有影响,那么我们就记录每种颜色的数量并去重,然后开始 dp。设 \(dp_{i,j}\) 表示循环到第 \(i\) 个颜色,钦定有 \(j\) 个元素相同的方案数。那么有
初学时属实没想到这玩意还能带 dp
很明显,\(f_{i} = dp_{m,i}\),其中 \(m\) 表示颜色总数。然后我们就得出了答案,时间复杂度为 \(\mathcal{O}(n^2)\)。
code:
sort(a + 1,a + n + 1);
int m = unique(a + 1,a + n + 1) - a - 1,tmp = 0;
dp[0][0] = 1;
for (int i = 1; i <= m; i++)
{tmp += cnt[a[i]];for (int j = 0; j <= tmp; j++)for (int k = 0; k <= min(j,cnt[a[i]]); k++)dp[i][j] = (dp[i][j] + dp[i - 1][j - k] * c(tmp - j,cnt[a[i]] - k) % mod * c(cnt[a[i]],k) % mod) % mod;
}
int ans = 0;
for (int i = 0; i <= n; i++)ans = ((ans + (i & 1 ? -1 : 1) * dp[m][i]) % mod + mod) % mod;
好题推荐
P4448 [AHOI2018 初中组] 球球的排列
P3158 [CQOI2011] 放棋子
P3160 [CQOI2012] 局部极小值