计数题杂谈
鉴于被 noip2025 的 t2 创飞,决定恶补计数题。
题目比较简单。
需要组合数学基础。
trick
总结一些比较常见的 trick。
1.拆贡献
没什么好说的。
2.两个连通块大小之积 \(\iff\) 两个联通块内有序点对个数
有时候方便计算。两个连通块不一定是不同的。
3.计数转期望
根据期望定义,有:
可以将计数转换为期望,变得好算。
4.若有排列转换,可以将排列位置和转换到的位置连边分析
将排列问题转换为图论。
不太好描述,可以参考后面的题目。
5.打表
可以一大堆数学分析通过注意力全部略掉。
如果你没有注意力,可以让 oeis 帮助你。
6.二项式反演
将选 \(k\) 个转换为钦定至少选或至多选前 \(k\) 个。可以看这里。
7.生成函数
创造出了一堆结论题。
8.容斥
这个十分好理解。
9.正难则反
遇到正着难算,不妨算总方案数再减去不合理的。应该算容斥的一种吗。
题目
P3214 [HNOI2011] 卡农
trick:8 & 9.
思路
考虑 dp。
设 \(dp_i\) 表示有 \(i\) 个片段的合法种数。这是不好直接算的。我们注意到假设我们随意选了 \(i - 1\) 个不重区间,为了让每个音阶都被演奏偶数次,我们第 \(i\) 个集合是可以确定的,这样子的方案数应为:
\(2 ^ n - 1\) 表示 \(n\) 个音节构成的非空集合数。于其中选 \(i\) 个即为答案。
显然这个并不是 \(dp_i\) 真正的值。
首先,有可能第 \(i\) 个集合(即那个通过前 \(i - 1\) 个集合确定的集合)可能为空集,这样子的方案数字显然是 \(dp_{i - 1}\)。
其次,有可能第 \(i\) 个集合可能与前 \(i - 1\) 个集合有重复,这样的贡献为 \(dp_{i - 2} \times (2 ^ n - 1 - (i - 2))\),其中 \(2 ^ n - 1 - (i - 2)\) 为第 \(i\) 个集合可能的方案数。
最后,这样子我们考虑到答案应为无序的,那么每种方案数这样是被算了 \(i\) 次的。
综上,
做完了。
代码
注意到无法预处理 \(2 ^ n - 1\) 的阶乘,所以说组合数需要单独递推求。
void ACehomoxue() {
cin >> n >> m;
dp[0] = 1, dp[1] = 0;
binomm = 1;
for(int i = 2; i <= m; i++) {
binomm = binomm * Mod(ksm(2, n) - i + 1) % mod * ksm(Mod(i - 1)) % mod;
dp[i] = Mod(binomm - dp[i - 1] - dp[i - 2] * (ksm(2, n) - 1 - (i - 2)) % mod) * ksm(i) % mod;
}
cout << dp[m], el;
}
P13310 染紫
trick:1 & 2 & 3 & 8.
思路
发现直接算是不好算的,考虑转化。考虑使用 trick2 转换成有序点对,并且用 trick1 把贡献拆到 lca。由于有的点可能为蓝,可能为红,我们使用 trick3 直接化为期望。
于是原问题就转化为 \((u, v, x, y)\) 使得 \((u, v)\) 在同一个红色连通块中,\((x, y)\) 在同一个蓝色连通块中。限制就相当于 \((u, v)\) 路径上的点全部得是红色,\((x, y)\) 上的点全部都是蓝色。因为显然 \((u, v)(x, y)\) 是独立的,只需要分别求解期望再相乘即可。我们将所有 \((u, v)\) 的为红色路径期望的和与所有 \((x, y)\) 的为蓝色路径期望的和相乘,这样就得到了所有 \((u, v, x, y)\) 的期望乘积,但是这并不是答案,因为我们还多算了 \((u, v)(x, y)\) 相交的不合法情况,需要 trick8 容斥。
对于具体地算点对个数,\(c_{i, 0 / 1}\) 表示 \(i\) 为红或蓝的概率。对于每一对 \((u, v)\) 或 \((x, y)\) 只需在其 lca 处算一下路径上的点皆为红/蓝的概率即可,简单的 dp 一下就行。
显然 \((u, v)(x, y)\) 相交相当于 \(lca(u, v)\) 在 \((x, y)\) 上或 \(lca(x, y)\) 在 \((u, v)\) 上,容斥,分别计算出以上两者的概率加起来再减掉两者同时成立即 \(lca(u, v) = lca(x, y)\) 的部分,换根即可。
太久之前做的了,思路都有点忘了,就不给代码了。
P4778 Counting swaps
trick:4 & 5.
思路
这种将排列换来换去的果断使用 trick4 转换,将 \(i\) 向 \(p_i\) 连边。那么我们发现变成了一堆环,最后合法的情况应该是整个图变成了 \(n\) 个自环。
我们假设图中产生了 \(k\) 个环,第 \(i\) 个大小为 \(a_i\)。
假设一个大小为 \(i\) 的环要交换 \(h_i\) 次,我们发现显然有 \(h_i = h_{i - 1} + 1\) 且 \(h_1 = 0\),得到 \(h_i = i - 1\)。
假设 \(f_i\) 表示一个大小为 \(i\) 的环有多少种操作方式能使其以操作 \(i - 1\) 次变成 \(i\) 个自环,那么我们可以考虑将 \(i\) 拆成 \(i = x + y\) 递推求。我们设 \(G(x, y)\) 表示将 \(i\) 拆成 \(x + y\) 的方案数,打表手模注意到 \(x \ne y\) 时 \(G(x, y) = x + y\),\(x = y\) 时 \(G(x, y) = x\)(\(x = y\) 时会有重复)。由于 \(x\) 与 \(y\) 各进行 \(x - 1\)、\(y - 1\) 此操作为独立的,那么两边操作的顺序可以交替进行,方案数应为多重集排列数 \(\frac{(i - 2)!}{(x - 1)!(y - 1)!}\)。整理一下得到:
这样转移是 \(n^2\) 的,考虑优化。你可以选择生成函数推式子,但打表显然是简单的。打出前 10 项为:1 1 3 16 125 1296 16807 262144 4782969 100000000。注意到 \(f_i = i ^ {i - 2}\)。
由于我们有 \(k\) 个环,由于各个独立所以每个环操作是可以交替进行的。我们发现这样的方案数也是一个多重集排列数 \(\frac{(\sum a_i - 1)!}{\prod (a_i - 1)!} = \frac{(n - k)!}{\prod (a_i - 1)!}\)。于是最终答案为:
代码
int n, p[maxn];
ll fact[maxn], inv[maxn];
ll ksm(ll a, int k = mod - 2) {
ll res = 1;
while(k) {
if(k & 1) res = res * a % mod;
a = a * a % mod;
k = k >> 1;
}
AK + res;
}
ll binom(int m, int n) { AK + fact[m] * inv[n] % mod * inv[m - n] % mod; }
void pre() {
if(fact[0]) AC;
fact[0] = 1;
for(int i = 1; i < maxn; i++) fact[i] = fact[i - 1] * i % mod;
inv[maxn - 1] = ksm(fact[maxn - 1]);
for(int i = maxn - 2; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
}
bool vis[maxn];
int search(int x) { int res = 0; while(!vis[x]) { vis[x] = true, x = p[x], res++; } return res; }
ll f(int x) { return x <= 1 ? 1 : ksm(x, x - 2); }
ll ans = 0;
vector <int> vec;
void ACehomoxue() {
pre();
vec.clear();
memset(vis, false, sizeof(vis));
cin >> n;
for(int i = 1; i <= n; i++) cin >> p[i];
for(int i = 1; i <= n; i++) if(!vis[i]) vec.push_back(search(i));
ans = fact[n - vec.size()];
for(auto x : vec) ans = ans * inv[x - 1] % mod * f(x) % mod;
cout << ans, el;
}
P4492 [HAOI2018] 苹果树
trick:1.
思路
注意到 \(N!\) 表示这颗苹果树的所有形态的总方案数,那么 \(E \times N!\) 其实是表示的计数(感性理解一下)。
直接做发现不好做,考虑将贡献拆到每个点到父亲的边。那么对于点 \(u\) 其子树大小为 \(siz_u\),贡献为 \(siz_u \times (n - siz_u)\)。
但我们要算总方案数,我们就要对于每个 \(u\) 枚举 \(siz_u\),假设 \(u\) 点子树大小为 \(siz_u\) 的方案数为 \(f_{u, siz_u}\),那么总答案就应为:
假设 \(i = siz_u\) 方便表示。由于枚举 \(u\) 和 \(i\) 已经是 \(n^2\), 我们需要快速的求 \(f_{u, i}\)。我们考虑直接算方案数。首先要注意 \(u\) 加到树中时外面已经有了 \(u - 1\) 个点,所以说其实 \(i \in [1, n - u + 1]\)。首先大小为 \(i\) 的树会有 \(i!\) 种形态,并且除了 \(u\) 以外其子树内的点的构成的方案数为 \(\binom{n - u}{i - 1}\),这是子树内的点的方案数。考虑算子树外。将 \(u\) 及其子树是为一个点,方案数显然为 \(u!\),由于子树外的点不可能放在 \(u\) 的子树中这是废话吗所以对于后面的点每个点的生成的方式分别为:\((u - 1 + 0),(u - 1 + 1),(u - 1 + 2),\dots,(n - i - 1)\),算的方法为找有多少可能的点可以作为加进来的点的父亲。整理一下,得:
做完了。
代码
注意没有说 \(P\) 是质数所以不能用阶乘预处理搭配其逆元求解(不一定有逆元),要用杨辉三角预处理。
ll n, mod, binom[maxn][maxn], fact[maxn], ans = 0;
void pre() {
if(binom[0][0]) AC;
binom[0][0] = 1, fact[0] = 1;
for(int i = 1; i <= n; i++) {
fact[i] = fact[i - 1] * i % mod;
binom[i][0] = binom[i][i] = 1;
for(int j = 1; j < i; j++) binom[i][j] = (binom[i - 1][j - 1] + binom[i - 1][j]) % mod;
}
}
void ACehomoxue() {
cin >> n >> mod;
pre();
for(int i = 2; i <= n; i++) {
for(int j = 1; j - 1 <= n - i; j++) {
ll cnt = fact[j] * binom[n - i][j - 1] % mod * i % mod * (i - 1) % mod * fact[n - j - 1] % mod;
ans = (ans + j * (n - j) % mod * cnt % mod) % mod;
}
}
cout << ans, el;
}
P3228 [HNOI2013] 数列
trick:9.
思路
好水。
真难则反。不考虑末尾的数是否 \(\le n\),运用差分的思想,我们枚举起点可以得到所有方案数为 \(nm ^ {k - 1}\)。考虑去除不合法的情况。对于每一个不法情况,一定存在 \(1 \le i \le k\) 使得 \(a_{i - 1} \le n \le a_i\),我们考虑对这样的 \(i\) 计数。首先对于每个 \([a_{i - 1}, a_i]\),设其长度为 \(l\),那么 \(n\) 与区间中的位置的方案数就为 \(l\) 个,总方案数就为 \(\sum_{1 \le l \le m} l = \frac{m(m + 1)}{2}\)。对于另外 \(k - 2\) 个差分区间显然长度有 \(m ^ {k - 2}\) 种取值,其中将含 \(n\) 的区间插到这 \(k - 2\) 个区间中应有 \(k - 1\) 种方案(因为一头一尾也可以),那么不合法的总方案数就为:\(\frac{m(m + 1)}{2} \times m ^ {k - 2} \times (k - 1)\)。那么这不就做完了?答案就为:
代码
真的有给的必要嘛。。。
void ACehomoxue() {
cin >> n >> k >> m >> p;
cout << ((n % p * ksm(m, k - 1, p) % p - (k - 1) % p * ((m) * (m + 1) / 2 % p) % p * ksm(m, k - 2, p) % p) % p + p) % p;
el;
}
P14364 [CSP-S 2025] 员工招聘
trick:1 & 8 & 9.
思路
感觉很 atcoder。
设 \(dp_{i, j, k}\) 表示前 \(i\) 个位置,有 \(j\) 个人被淘汰,有 \(k\) 个人因耐心小被淘汰。
显然有 \(dp_{0, 0, 0} = 1\),并且在 \(s_i = 0\) 的情况下有显然的转移 \(dp_{i + 1, j + 1, k} = dp{i, j, k}\),并且答案为:\(\sum_i \sum_j dp_{n, i, j}\)。
考虑当 \(s_i = 1\) 时将 \(dp_{i, j, k}\) 转移出去。
直接算不好算,考虑容斥成正难则反。
为了方便设 \(f = dp_{i, j, k}\),\(d = (\sum_i [c[i] \le j]) - k\) 即未钦定位置的人的个数。
转移 1:\(dp_{i + 1, j, k} = dp_{i + 1, j, k} + f \times x\),\(x\) 表示剩的人的个数,假设全部都是随便取都不会被淘汰,反正不合法的状态我们都会容斥。\(x\) 我们可以现在不用算,可以直接 \(dp_{i + 1, j, k} = dp_{i + 1, j, k} + f\),最后在给答案加和的时候可以加 \(dp_{n, i, j} \times (n - j)!\)。
转移 2:\(dp_{i + 1, j + 1, k + 1} = dp_{i + 1, j + 1, k + 1} + f \times d\),显然有 \(d\) 个人是会被淘汰的。
转移 1 和转移 2 构成了全部答案,接下来我们用转移 3 弄掉不合法的。
转移 3:\(dp_{i + 1, j, k + 1} = dp_{i + 1, j, k + 1} - f * d\) 显然这 \(d\) 个人中有的人是在这样的限制下无法被淘汰的,直接减去 \(f * d\) 即可因为是容斥。
代码
使用了滚动数组。
#define p(x) ((x) & 1)
void ACehomoxue() {
prefact();
cin >> n >> m >> s;
for(int i = 1, x; i <= n; i++) { cin >> x; c[x]++; }
for(int i = 1; i <= n; i++) c[i] += c[i - 1];
dp[p(0)][0][0] = 1;
for(int i = 0; i < n; i++) {
for(int j = 0; j <= n; j++) for(int k = 0; k <= i; k++) dp[p(i + 1)][j][k] = 0;
if(s[i] == '0') {
for(int j = 0; j <= i; j++) for(int k = 0; k <= i; k++) dp[p(i + 1)][j + 1][k] = (dp[p(i + 1)][j + 1][k] + dp[p(i)][j][k]) % mod;
continue;
}
for(int j = 0; j <= i; j++) for(int k = 0; k <= i; k++) {
int &f = dp[p(i)][j][k], d = c[j] - k;
dp[p(i + 1)][j][k] = (dp[p(i + 1)][j][k] + f) % mod;
dp[p(i + 1)][j][k + 1] = Mod(dp[p(i + 1)][j][k + 1] - f * d % mod);
dp[p(i + 1)][j + 1][k + 1] = Mod(dp[p(i + 1)][j + 1][k + 1] + f * d % mod);
}
}
for(int j = 0; j <= n - m; j++) for(int k = 0; k <= n; k++) ans = (ans + dp[p(n)][j][k] * fact[n - k] % mod) % mod;
cout << ans, el;
}

浙公网安备 33010602011771号