图 1:本文封面,合法路径与非法路径只差一条禁区线。
先说明属性:这是一篇笔试真题复盘,题目出自本地历史笔试资料,与任何公司的当前招聘无关。题面是:一个序列由 7 个 A 和 7 个 B 组成,要求任意前缀中 A 的个数不少于 B 的个数,问这样的序列有多少个。四个选项围着 429 打转,答案是 429——卡特兰数 C(7)。但直接背数列不算本事,把 429 推出来才是。
第一步:没有约束时,总数很好算。
14 个位置里挑 7 个放 A(剩下自然放 B),总数是组合数 C(14,7)=3432。麻烦的是「任意前缀 A 不少于 B」这个约束,它把计数问题变成了路径问题。
第二步:把序列画成格点路径。
从左下角 (0,0) 出发,每读到一个 A 就向右走一步,读到一个 B 就向上走一步,7 个 A、7 个 B 恰好走到 (7,7)。这时约束有了几何意义:任意前缀中 A 不少于 B,等价于路径上任意时刻「向右的步数 ≥ 向上的步数」,也就是路径始终不越过对角线 y=x 的上方。合法序列与「不越线的格点路径」一一对应,问题变成:数不越线的路径有多少条。
第三步:数非法的比数合法的容易——反射原理。
总数 3432 里减去非法路径就是答案,而非法路径有个漂亮的数法。非法路径必然在某个时刻第一次越线,也就是第一次碰到禁区线 y=x+1。抓住这个首触点,把路径从起点到首触点的这一段沿禁区线做镜面反射:起点 (0,0) 被反射到 (−1,1),首触点不变,后半段原样保留。反射后的新路径从 (−1,1) 走到 (7,7),需要向右 8 步、向上 6 步。
关键观察:这个反射是可逆的——任何一条从 (−1,1) 到 (7,7) 的路径必然碰到禁区线,把首触点之前那段反射回去,就唯一还原出一条非法路径。反射是双射而不是多对一,这正是整个证明的命脉:两个集合元素一一配对,数量必然相等。于是非法路径与「8 个 A、6 个 B 的全部排列」一一对应,数量是 C(14,6)=3003。这就是反射原理(也叫镜像法):不直接数合法的,而是给非法的找一个好数的双射。
收尾:合法 = 总数 − 非法。
3432−3003=429,恰好等于 C(14,7)/(7+1)。一般形式就是卡特兰数 C(n)=C(2n,n)/(n+1):n 个 A、n 个 B 的合法排列数,本题 n=7。
用代码对拍,把小规模也数一遍。
公式推导怕翻车,就让程序枚举小规模情形和公式逐项对拍:
// node catalan.js(Node.js 22 实测)
const C = (n, k) => { // 组合数
let r = 1;
for (let i = 0; i < k; i++) r = r * (n - i) / (i + 1);
return r;
};
const catalan = n => C(2 * n, n) / (n + 1);
function brute(n) { // 回溯枚举所有合法序列,剪枝即前缀约束
let cnt = 0;
(function dfs(a, b, diff) {
if (diff < 0) return;
if (a === n && b === n) { cnt++; return; }
if (a < n) dfs(a + 1, b, diff + 1);
if (b < n) dfs(a, b + 1, diff - 1);
})(0, 0, 0);
return cnt;
}
for (let n = 1; n <= 7; n++)
console.log(`n=${n} 公式=${catalan(n)} 枚举=${brute(n)}`);
实测输出从 n=1 到 n=7 公式与枚举完全一致:1、2、5、14、42、132、429。n=7 那一行同时给出总数 3432 与非法 3003,减法成立,答案坐实。
图 2:从组合总数到反射原理的三步推导与同源题型。
认识卡特兰数,一类题就有了锚点。
n 对括号的合法匹配数、n 个元素的出栈序列数、n 个节点的二叉树形态数,全是卡特兰数——它们都能归约成「任意前缀中一种符号不少于另一种」的计数:把左括号当 A、右括号当 B 就是原题;入栈当 A、出栈当 B 亦然;二叉树则靠递归分解 C(n)=ΣC(k)·C(n−1−k) 与卡特兰数共享同一个递推。反射原理的价值也不只在笔试:它是组合数学里「用双射把难数对象映射到好数对象」的范本,抽签问题、随机游走的首达概率里都能看到它的影子。
回到笔试现场,建议的答题结构:先报总数 C(14,7),再讲格点路径转化,然后用反射原理数出非法 C(14,6),相减得 429,最后点一句这是卡特兰数。四步走完,选择题做成了证明题。你还见过哪些表面无关、最后却归约到卡特兰数的题目?欢迎补充。