【1 月小记】Part 6: 组合数学
组合数学
update 2026.1.17: 更改了部分题目顺序,优化了 \(\LaTeX\)
计数原理与排列组合
可以算出总的情况,然后减去信仰宗教不同的情况。
总的情况就是每一个人可能信仰 \(m\) 种宗教,显然总的情况数就是 \(n^m\)。
至于不同的情况,我们可以这么考虑:如果第一个人有 \(m\) 种选择方式,第二个人为了不跟他重复,只有 \(m-1\) 种选择方式,第三个人为了不跟第二个人重复,也只有 \(m-1\) 种选择方式……总之,如果所有相邻的人信仰的宗教都不相同的情况有 \(m(m-1)^{n-1}\) 种。所以,答案就是
组合数问题,需要使用多种技巧求解。
因为上一步的树苗们已经占用了 \(m\) 个空位,所以现在还剩下 \(n-m\) 个空位。
因为任意两个树苗之间必须有空位,所以我们可以把剩下的 \(n-m\) 个空位看作物品,把树苗看作隔板,进行隔板法计算。
这样做的原因是,因为多个隔板不能放在同一个位置,所以将树苗看作隔板这一操作,确保了相邻两树苗之间必定至少有一个空位。
于是 \(n-m\) 个空位这些物品中,供隔板放置的空位就有 \(n-m+1\) 个,要把上面的 \(m\) 个树苗放在这些空位里。
因为树苗是有编号的,所以这里使用排列数计算。答案为
设在 \(n\) 个数构成的排列 \(\{a_i\}_{i = 1}^n\) 中,使得 \(\forall i, a_i \ne i\) 恒成立的排列方案数为 \(D_n\)(这种排列称作错排)。
下面考虑 \(D_n\) 的递推式。
举例:假设 \(n=4\)。首先考虑 \(i=1\) 的情况,此时它不能且仅不能被放到 \(1\) 号位置,有 \((n - 1)\) 种选择。
对于 \(i=2\),以下分两种情况讨论:
- 当它没被放到 \(1\) 号位置时,\(2\) 号不能被放到 \(1\) 号位置,那么就可以把现在的 \(1\) 号位置看作一个 \(2\) 号位置使得 \(2\) 号物品不能被放进去,也就是 \(2\) ~ \(4\) 号物品做错排,方案数为 \(D_{n - 1}\)
- 当它被放到了 \(1\) 号位置时,相当于 \(3\) 和 \(4\) 号做错排,方案数为 \(D_{n - 2}\)
根据计数原理,有
考虑从 \(n\) 个位置中挑选 \(m\) 个位置使得它们固定,不遵循错排规则;其余位置遵循错排规则。所以答案为
注意:1. 逆元;2. 特判 \(m=0\) 的情况,不然你会获得 0 pts!
如果忽略没有空的条件,答案显然是 \(r^n\)。但如果我们考虑上这个条件呢?
我们要钦定恰好有 \(k\) 个盒子为空,求方案数。钦定 \(k\) 很难做,但我们不妨钦定转至少。
那么现在考虑一种放法,使得该方法内,至少有 \(k\) 个空盒子。
在 \(r\) 个盒子内,选出 \(k\) 个盒子,使它们一定为空,显然有 \(\dbinom{r}{k}\) 种选法。所以,现在还剩下 \(r-k\) 个可不空也可空,即忽略限制的盒子。
对于那 \(n\) 个球,放入 \(r-k\) 个盒子,每个球都有 \(r-k\) 种放法,那么这 \(n\) 个球放入这 \(r-k\) 个盒子中的总方案就是 \((r-k)^n\)。
刚才叙述的两个方面是解决同一问题的两个步骤,故使用乘法,因此得到:某种放法内,至少有 \(k\) 个空盒子的总方案数为 \(\dbinom{r}{k}(r-k)^n\)。
因为这里考虑的是至少,所以至少转钦定,要用容斥原理容斥一下。
分别考虑 \(0\) 和 \(1\) 的放置方案,然后相乘即可。
考虑隔板法,这里允许袋子为空,所以添加 \(n\) 个虚拟物品,然后跑隔板法,相当于在 \(n+a+1\) 个物品中,即 \(n+a\) 个间隔中,插入 \(n\) 个隔板,即 \(\dbinom{a+n}{n}\)。
Lucas 定理
Lucas 定理表明:对于素数 \(p\),有
证明非常复杂,此处略。
卡特兰数
考虑一种递推写法。设 \(f_{i,j}\) 表示目前有 \(i\) 个元素未入栈、\(j\) 个元素已入栈的出栈方案数。
如果目前栈外存在元素(即 \(j>0\)),则可以让这个元素从输入序列挪入栈中,决策来源于 \(f_{i-1,j+1}\)。
如果目前栈内存在元素(即 \(i>0\)),则可以让这个元素从栈中移出到输出序列中,不会改变未入栈的数字个数,决策来源于 \(f_{i,j-1}\)。
将上两式求和,可得
答案为 \(f_{n,0}\)。
让我们再把这道题抽象成更抽象的数学模型。
我们知道,对于一个合法的栈,任意时刻,入栈的次数不得少于出栈的次数。
定义 \(C_n\) 表示序列长度为 \(n\) 的输出序列方案数。
-
不妨记第 \(1\) 个入栈的元素是第 \(k\) 个出栈的元素,则前面的元素 \([1,k-1]\) 必须在 \(k\) 之前完成入栈并出栈。因此,方案数依赖于 \(C_{k-1}\)。
-
\(k\) 后面的数则有 \(C_{n-k}\) 种方案。
\(\forall k \in [1,n]\),\(C_n\) 的转移都从上面两种情况而来。因此,\(C_n\) 的递推式应为
事实上,比利时数学家 Eugène Charles Catalan 在 1958 年研究括号序列计数问题时发现了这一数列,它也因此得名 Catalan 数(卡特兰数)。
事实上,卡特兰数还有如下的计算方式
以及其顺次递推式
它能解决什么问题呢?
-
路径计数问题
有一个大小为 \(n\times n\) 的方格图,左下角为 \((0,0)\),右上角为 \((n,n)\)。从左下角开始,每次都只能向右或者向上走一单位,不能走到对角线 \(y=x\) 的上方(但可以触碰),则到达右上角的路径总条数为 \(C_n\)。
证明1 存在一个性质:一条合法路径迟早会碰到直线 \(y=x\),且碰到至少一次。
设方案数为 \(C_n\),设路径第一次碰到 \(y=x\) 的点为 \((k,k)\)。
称一条路径是好的,当且仅当它从 \((0,0)\) 到 \((k,k)\) 除起点和终点外,中间的点从未经过或触碰 \(y=x\)。
可以发现,对于所有好的路径,它的第一步一定向右,最后一步一定向上;即这些好的路径就是从 \((1,0)\) 到 \((k,k-1)\) 的不越过直线 \(y=x-1\) 的路径,方案数为 \(C_{k-1}\)。同理,对于 \((k,k)\) 到 \((n,n)\) 的方案数就是 \(C_{n-k}\)。
枚举所有的 \(k\),可得 \(C_n = \sum \limits _{k=1} ^n (C_{k-1} C_{n-k})\),恰好满足卡特兰数的递推式。
证明2 直接求合法的路径数目不太容易,考虑正难则反,用总路径数目减去不合法的路径数目求解。
-
无视限制的总路径数目
一共要走 \(2n\) 步,其中 \(n\) 步是向右走的,所以方案数为 \(\dbinom{2n}n\)。
-
不合法的路径数目
注意到一条路径不合法,当且仅当它越过了直线 \(y=x\),即碰到了直线 \(y=x+1\)。对于任意一条不合法的路径,将其第一次碰到直线 \(y=x+1\) 的位置沿着这条直线对称,会发现这条路径的终点变为了 \((n-1,n+1)\)。
因此,任意一条从 \((0,0)\) 到 \((n-1,n+1)\) 的路径都要穿过直线 \(y=x+1\),进而每条这样的路径都映射着一条从 \((0,0)\) 到 \((n,n)\) 的非法路径。
这样的路径方案数为 \(\dbinom{2n}{n+1}\)。
综上,用总路径数目减去不合法的路径数目,我们成功证明了 \(C_n = \dbinom{2n}n - \dbinom{2n}{n+1}\)。
-
-
括号序列计数问题
由 \(n\) 对括号构成的合法括号序列数为 \(C_n\)。
一个括号序列是合法括号序列,当且仅当在任意位置,左括号的数量不少于右括号的数量。
我们发现了形如”在任意位置,向右走的步数不少于向上走的步数“或者”在任意位置,左括号的数量不少于右括号的数量“这种限制,就说明这个问题应当使用卡特兰数求解。
-
二叉树计数问题
含有 \(n\) 个节点的不同的二叉树有 \(C_n\) 种。
请读者自行证明。
-
\(\cdots \cdots\)
为什么最开始的递推式算出来的就是卡特兰数呢?涉及到生成函数的知识,这里不过多展开(其实是因为我不会。
容斥原理
考虑把贡献挂到二元组上。
定义 \(f_k\) 表示满足 \(\gcd(i,j)=k\) 的二元组 \((i,j)\) 的个数。
记 \(F\) 表示满足 \(k|\gcd(i,j)\) 的二元组个数。可以看到,这个条件明显比上一个条件更弱,这是因为我们钦定转至少了。对于含有 \(n\) 个元素的序列,满足 \(k|a_i\) 的 \(a_i\) 一共有 \(\Big \lfloor \dfrac nk \Big \rfloor\) 个;满足这个条件的二元组就有 \(F = \Big \lfloor \dfrac nk \Big \rfloor ^2\) 对。
根据容斥原理,有
每一组 \(\gcd(i,j)=k\) 的二元组 \((i,j)\) 都会产生 \(k\) 的贡献,因此答案为
数据范围太大,跑多重背包必死。于是考虑可以先跑出来一个完全背包,计算不加硬币数量限制时的方案数。
定义 \(f_j\) 表示要买的物品价格为 \(j\) 时的付款方案总数。对于每一种 \(c_i\),有
但是这里我们有硬币数量的限制。我们知道,当第 \(i\) 种硬币用数超限时,此时的硬币用数 \(>d_i\),即 \(\geq d_i + 1\)。因此,状态一定从 \(s-c_i(d_i+1)\) 及更大的用数转移而来。
由此可得,至少存在一种硬币超限的状态都从 \(f_{s-c_i(d_i+1)}\) 转移而来,所以这些东西我们要从答案 \(f_s\) 中减去;然后发现至少存在两种硬币超限的状态(即形如 \(f_{s-(c_1(d_1+1)+c_2(d_2+1))}\) 的东西)算重了,把它们加回来………这不就是容斥原理么!
为什么这里会用到容斥原理呢?因为我们算的是至少,一个”至少“的状态里包含很多内容,从至少转到钦定就需要用容斥原理把正确答案容斥出来。
想法有了,代码怎么写呢?因为我们只有四种硬币,考虑枚举四位二进制数,通过二进制数的 \(0/1\) 状态代表子集情况,根据二进制数的 \(1\) 的个数判断这一项是应该被加还是减到贡献里。
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N = 1e5;
int c[5], d[5], f[N + 5], n, s;
int sign(int x) {
int res = 0;
while (x) {
res++;
x -= x & -x;
}
return ((res & 1) ? -1 : 1);
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
for (int i = 1; i <= 4; i++) {
cin >> c[i];
}
cin >> n;
f[0] = 1;
for (int i = 1; i <= 4; i++) {
for (int j = c[i]; j <= N; j++) {
f[j] += f[j - c[i]];
}
}
while (n--) {
int ans = 0;
for (int i = 1; i <= 4; i++) {
cin >> d[i];
}
cin >> s;
for (int i = 0; i < 16; i++) {
int sum = 0;
for (int j = 1; j <= 4; j++) {
if (i & (1 << (j - 1))) {
sum += c[j] * (d[j] + 1);
}
}
if (s - sum >= 0) {
ans += sign(i) * f[s - sum];
}
}
cout << ans << '\n';
}
return 0;
}

浙公网安备 33010602011771号