去年帮部门排周末值班,题目是"12 个人里选 4 个"。我算出 495,隔壁工位的老周算出 11880。草稿拍在桌上互相验了一下午,才发现问的压根不是同一件事:我说的是"哪四个人",他说的是"哪四个人各守哪个岗"。
这之后我又碰到好几回同类争执:抽奖的覆盖率、比赛名单的搭配、四位数字锁。栽多了,我给自己定了一条很笨但有用的判据,下面这些数都逐个算过。
顺序算不算数,我做"交换测试"
把一个结果原样写下来,交换其中两个成员的位置,只问一句:这还算不算新的一种?算,顺序就算数,用排列;不算,用组合。听着像废话,做成动作后,绝大多数题目不用再犹豫。
| 场景 | 交换两个位置之后 | 该用哪个 | 数量 |
|---|---|---|---|
| 12 人取 4 人,只决定谁上 | 还是那四个人,名单没变 | 组合 | C(12,4)=495 |
| 12 人取 4 人,分守 4 个不同的岗 | 班次对不上人,值班表变了 | 排列 | A(12,4)=11880 |
| 52 张牌发 5 张,只看手里是哪五张 | 还是那五张,先后不同不算新的 | 组合 | C(52,5)=2598960 |
| 4 位数字锁,数字不许重复 | 1234 与 4321 是两把锁 | 排列 | A(10,4)=5040 |
| 4 位数字锁,数字可重复 | 每位独立,各有 10 种 | 两个都不是 | 10×10×10×10=10000 |
最后一行是我每次都要先问的第二件事:可不可重复。两个公式默认都不重复,一旦允许重复就既不是 A 也不是 C,而是每位独立相乘。
两个公式之间只差一个 r!
排列数 A(n,r) = n! / (n-r)!,组合数 C(n,r) = n! / (r!(n-r)!),差别只在分母多出的那个 r!,即 A(n,r) = C(n,r) × r!。这是一次现成的自检:两个数除一下,应该正好等于 r 的阶乘,除不平就说明题意认错了。
我核对过的几组:
- n=12,r=4:C=495,A=11880,比值 24,正好是 4!
- n=9,r=4:C=126,A=3024,比值 24
- n=7,r=3:C=35,A=210,比值 6,正好是 3!
- n=10,r=3:C=120,A=720,比值 6
- n=6,r=2:C=15,A=30,比值 2
组合数还有两条手算验错的路:对称性 C(n,r)=C(n,n-r),如 C(10,3) 与 C(10,7) 都是 120,所以手算挑较小的 r,乘法步数少一半;帕斯卡递推 C(n,r)=C(n-1,r-1)+C(n-1,r),如 C(8,3)=C(7,2)+C(7,3)=21+35=56。要列全部方案我不手敲,交给 排列计算器,它连公式带过程一起列。
我真正确实算错的三类
第一类就是"可重复"漏问。同一把四位锁有三种理解、三个数:数字可重复是 10×10×10×10=10000;不许重复且顺序算数是 A(10,4)=10×9×8×7=5040;不许重复、只问用了哪四个数字不计先后,是 C(10,4)=5040/(4×3×2×1)=210。题目只说"四位数字"时我吃过亏。
第二类是"至少"。5 男 7 女里选 3 人,要求"至少有 1 名女生"。我一开始先定 1 名女生再随便补 2 人:C(7,1)×C(11,2)=7×55=385,当答案交了。正确做法是整体减反面:C(12,3)-C(5,3)=220-10=210。多出的 175 不是笔误:两名女生的组被数两遍,三名女生的组被数三遍——按女生人数拆开是 70+105+35=210,我的算法得到 1×70+2×105+3×35=385。这种重复计数不会报错,只有换一种数法才看得见。
第三类是概率的分母。分母必须是完整玩法的组合数,不是其中一步:红球 6/33 加蓝球 1/16,总数 C(33,6)×16=1107568×16=17721088,只算红球就少了 16 倍;前后两区那种是 C(35,5)×C(12,2)=324632×66=21425712。尺度感:五张牌区分花色的组合 2598960 种,一天摸一种、按一年 365 天计要 7120.4 年;把四种花色视作等价只数牌型,才是 7462 种。
数字为什么爆得这么快
两个公式都靠阶乘,而阶乘的增长完全不讲道理。下面是我自己算的对照:
| 阶乘 | 值 | 是否还在 2^53-1 之内 |
|---|---|---|
| 15! | 1307674368000 | 在 |
| 18! | 6402373705728000 | 在 |
| 19! | 121645100408832000 | 越线 |
| 20! | 2432902008176640000 | 越线 |
这里的 2^53-1 是 9007199254740991,普通数值类型精确表示整数的上限。但越线不等于当场失真:我把 1! 到 25! 逐个大数对位比过,15! 到 22! 与 double 完全相同,第一次真丢位是 23!,精确值 25852016738884976640000,double 给 25852016738884978212864。它们越线还没丢,是末尾因子 2 多、有效位压得进 53 位,碰巧装得下,不能指望;判据还是 2^53-1,9007199254740993 就会被存成 9007199254740992。所以 C(20,10)=184756 这题,double 算 20!/(10!×10!) 得到的仍是 184756,没失真;我以前咬定"中间那步早就错了",错在把"结果看着正常"当成"中间量精确"——中间量换成 23! 往上,商就跟着错。C(100,50) 是 30 位数 100891344545564193334812497256,固定宽度整数装不下,这类题得用支持大数的算法:组合计算器 写的是基于 BigInt 算大组合数,阶乘计算器 写的是 0 到 1000 不溢出。
还有一件事我分得很清:要"一个数"和要"全部明细"是两个量级。后者行数按阶乘涨,8 个元素全排列 40320 行,10 个是 3628800 行,先压垮的是页面不是算术;上限是实现定的,不是数学限制。
Q:A(n,r) 一定比 C(n,r) 大吗?
大或者相等,不会小,因为 A = C × r!。r=1 时两者都等于 n,r=0 时都等于 1,这是确认公式没写反的两个边界。
Q:从 7 个人里挑 3 个开会,和挑 3 个人分别负责三个不同岗位,差多少?
35 种和 210 种,差 6 倍,也就是 3!。同三个人换一下分工就算新方案,那 6 倍必须乘上。
Q:数字大到算不动了怎么办?
先问是不是真要精确值。比较数量级用约等就够;要精确到个位就不能拿普通数值类型硬算,得换成支持大数的做法,或者先把公式约分再乘。
Q:交上去之前怎么自查?
我固定查三样:A 除以 C 是不是正好等于 r!;有没有把"可重复"的题错用 A 或 C;"至少"是不是改成了整体减反面。三样都过才敢交。
回到开头那张值班表。最后用的是"分岗位"口径的 11880,每人对应一个班次,交换位置就是一份新表;要是讨论"干脆随机凑人",该说的是 495。谁对谁错不重要,先说清交换之后算不算新的一种。