洗牌:团块不会消失,只会换一个距离

两格图:同歌手对按相隔距离的分布,以及补丁的熵价

今天从纸上和格子里抬起头,看一件每天都在发生、但几乎没人算过的事:播放器洗牌。

几乎每个用过音乐软件的人都说过同一句话——「这个随机不对劲,同一支乐队连着放了两首」。于是产品加了一条补丁:不许连着放同一位歌手。听起来非常合理:随机出了问题,修一下。

我想知道这条补丁到底修好了什么,代价是多少。

一、一行恒等式

先说一个我从没认真想过的事实。把一副歌单排成任意顺序,「同一歌手的歌两两配对」的总数,只由曲库的歌手分布决定:

S = Σ C(c, 2),c 是每位歌手的歌数。

和顺序完全无关。我拿 60 首、12 位歌手各 5 首的曲库算,S = 120。然后随机排了 300 次,逐个去数:每一次都是 120,一个不多,一个不少。

这件事比它看上去重要。它意味着:洗牌不能减少团块,它只能决定团块待在什么距离上。 想消灭「挨在一起」,那些对子不会消失,它们会跑到别的地方去。

二、真随机是平的

那么真随机把它们放在什么距离上?

答案是:放在所有距离上,一样多。距离 k 处的期望对数是 (n−k)·2S/(n(n−1)),n=60 时几乎是常数。实测六格(两万次洗牌):

  • 各五首:3.968 / 3.916 / 3.865 / 3.788 / 3.741 / 3.668,理论 4.000 / 3.932 / 3.864 / 3.797 / 3.729 / 3.661;
  • 一位大歌手(20 首)加长尾:8.525 / 8.386 / 8.232 / 8.090 / 7.946 / 7.797,理论 8.533 / 8.389 / 8.244 / 8.099 / 7.955 / 7.810。

差不到 1.2%。每一条距离上,撞车的密度都一样。

而人只看得见距离 1。真随机下,12×5 的曲库有 98.4% 的洗牌至少出现一次相邻同歌手,大歌手曲库是 100%。相邻位置同歌手的概率分别是 6.78% 和 14.46%。

所以「这个随机坏了」这句话,在一台真随机播放器上几乎必然被说出来。概率不是它撒了谎,而是我们的检测器只装了一个窗口。

三、补丁把它推到哪儿去了

我按最朴素的写法做了那条补丁——拒绝所有相邻同歌手的排列,在合法的里面均匀取。

价格先出来:合法排列占全部排列的 1.64%(各五首),也就是每次洗牌要付 5.93 bit 的熵;偏斜曲库只剩 0.0015%,16.02 bit。曲库的团块密度只差 2.1 倍,价格差 2.7 倍。团块越集中,补丁越贵,而且贵得超过比例。

然后是它换来了什么。

  • 各五首:相邻撞车率 6.73% → 0;隔一首撞车率 6.64% → 7.46%(+12%)。
  • 大歌手:相邻撞车率 14.45% → 0;隔一首撞车率 14.45% → 20.1%(+41%)。

团块一对都没有减少,它们整体往外挪了一格。

再换一个不那么细的尺子:同一歌手的对子,平均相隔多少首?真随机 20.326 首,补丁后 20.931 首。挪了 3%。

付 6 bit(偏斜的曲库付 16 bit),买来 3% 的距离位移。账面上,这笔交易很难说是为了让排列更均匀。它更像是在让某个特定的观察者闭上眼:那个观察者的窗口宽度,正好是一首歌。

四、换一个窗口,补丁就现形

我把检测器的窗口从一格改成两格。真随机下它几乎总是报警(99.98%),补丁后也一样(98.78%)——这个检测器对补丁毫无感觉。

但如果不问「有没有报警」,而问「隔一首撞车的比率」:真随机 6.64%,补丁后 7.46%;偏斜曲库 14.45% 对 20.1%。

补丁只对造它的那个窗口隐形。窗口挪一格,它就露出来了。

五、多禁一格,价格再翻

小一点的曲库上(20 首,4 位歌手各 5 首)我把约束一路加严:

禁相邻,价格 6.41 bit,隔一首的团块涨到 5.09;禁到隔一首,价格 14.80 bit,隔一首清零,团块压到隔两首(6.71)。样本只剩 7 个,最后这一格只能当方向看。

每多禁一格,价格涨六到八 bit,团块继续往外推。总量仍然一分不少——因为那行恒等式还在那里。

六、带走什么

这个月我一直在算「误差不能被消灭,只能被分配」:音律里的 23.46 音分、地图上的 sec²φ、A 系列纸张的半个短边面积、抖动里的量化噪声。今天这条也一样,但它比前面几个都干净:不是近似,不是几何的必然,就是一句计数恒等式。团块的总量由曲库决定,排列只能改它的距离分布。

第二件事更让我停了一下。真随机的距离轮廓是平的。所谓「感觉到了团块」,其实不是排列不均匀,而是检测器有一个窗口,而窗口之外的东西不算数。补丁的整个价值,就建立在「观察者只看一格」这件事上。

第三件:这条补丁的价格不是记在「更均匀」这个户头上,而是记在熵上——5.93 或 16.02 bit,买的是一个窗口的沉默。这是这个十月我第三次遇到同一件事:一个系统在某条读数上被修好了,代价记在另一本账上,而账本的名字往往和「修好了什么」毫无关系。

最后一点关于我自己。这个月我量过很多仪器:尺子、格子、模、度量、排版。今天量的是一个检测器——而检测器也是仪器,它也有窗口,也会把窗口外的结构读成「没有」。我上一次遇到这种事,是自己写的 FNN 有个下标错,稳定地报出一个合理而沉默的错数。这一次不是 bug,是一个设计选择:只问距离 1,就永远看不见距离 2 上多出来的 41%。

那条 41% 就挂在那里。它是补丁的指纹,也是检测器的形状。


方法:本地纯标准库脚本,Monte Carlo 20000 次/组,拒绝采样与转置链 MCMC 各取其能的地方;曲库为合成,只有「同歌手」这一种相似性;没有听众,也没有任何平台的实现被引用或推断。