前阵子,合作群里有人扔来一份预印本,标题很朴素,但里面那个结果让我连续几天都没缓过来:他们找到了一组反例,把几何拓扑里一个流传了一百五十年的老直觉彻底推翻。这个问题的通俗版本,其实用家里铺地板就能讲明白——拿一模一样的正方形瓷砖,只能平移、不能旋转,去铺满整个平面,你无论如何也躲不开“两块相邻瓷砖共享完整一条边”这件事。到了三维,用同样的立方体箱子码满空间,也总会有两个箱子共享一个完整矩形面。可一旦走进高维,这个直觉开始变得可疑。更离谱的是,找到这个反例不需要什么顶级超算,就是几台普通笔记本在连续几周暴力搜索里硬扛,中间还真烧坏了两台。这篇博文就是把这次项目的复盘整理出来:怎么把一个连续的几何直觉翻译成离散搜索,怎么写程序才能让高维枚举活下来,以及最后看到反例时那几分钟的复杂心情。适合对几何拓扑感兴趣的朋友,也适合所有想知道“现代数学怎么烧电脑”的人。
1. 项目概述:一句话就能说清的问题,却让直觉错了一百五十年
1.1 从铺地板说起:几何直觉到底有多“自然”
想象你手头有一堆完全相同的正方形瓷砖,规则很简单:只能平移,不能旋转,目标是用它们铺满整个平面。你很快会发现,只要真能铺满,那么随便取哪两块相邻的瓷砖,它们之间要么共一条完整的边,要么干脆不共边。你几乎不可能铺出一种“所有相邻瓷砖都只擦到一个角”的密铺。原因也很直观:如果每块瓷砖都错开半格,那么错开的边界处就会留下一条连续的缝隙,这些缝隙加在一起,迟早会撑出一个铺不平的洞。这在日常生活中就是那句“地板砖对不齐就漏缝”的数学版本。
这个想法搬到三维同样显得理所当然。用完全相同的立方体箱子去填满整个空间,不管是整齐码放还是交错堆放,总会有两个箱子不仅碰到一起,而且共享一个完整的矩形面。要是所有箱子都只“偏着碰”,箱子和箱子之间的公共部分全是些边边角角,那么空间的空隙就会像迷宫一样连成一片,最后总会有填不满的死角。所以几何学家很早就有一个强烈的信念:这种“完整共享面”的现象,可能不只在二维、三维成立,而是任意维度空间中的一条铁律。
于是问题被正式提出来:在 n 维空间中,用完全相同的 n 维超立方体,只允许平移、不允许旋转,去铺满整个 n 维空间。请问,是不是一定存在至少一对超立方体,它们共享一个完整的 (n-1) 维超面?边界、个例、证明方法都先不管,就这个最朴素的问题,竟然没有人能对任意维度给出答案。低维时它显然成立,可一旦到了高维,它变成了一只咬住数学界一百五十年的铁乌龟。
1.2 低维全对,这才让信仰变得危险
我当时翻文献时查过这个问题的来龙去脉。它的思想萌芽可以追溯到十九世纪中后期,那时晶体学在研究空间对称性时,就观察过“规则单元平移堆满空间后,单元之间会不会出现完整面贴合”这类现象。到了二十世纪前叶,这个问题被写成更严谨的数学猜想,但学界普遍认为它不过是“低维显然,高维也显然”的顺水推舟。
低维的验证也确实顺着这个预期走:二维、三维被严格证明成立,四维、五维乃至六维,也陆续被后来的研究者用组合推理逐个拿下。也就是说,从二维到六维,没有一个人找得到反例。这些证明不是“用电脑试了一下没发现”,而是真正从逻辑上推导出:在那些维度里,任何平移密铺都逃不掉完整面重合。这一长串的胜利让绝大多数数学家选择相信,七维、八维乃至更高维,大概率也只是“还没人证明”而已,反例不可能存在。
但问题就出在这里。低维全对这件事,反而成了最危险的证据。因为高维几何和低维几何有个本质区别:低维空间里的结构比较“紧”,你可以用画图、直觉、物理空间的经验去想象;可一旦维度超过六,空间突然变得非常“松散”,很多低维看起来不可能发生的结构,在高维都能巧妙存在。这个项目的起点其实就是一次反向思考:既然证明高维成立这么难,会不会是因为它根本就是错的?与其继续用纸笔死磕定理,不如把问题直接扔给机器,看看高维到底能长出什么东西来。
2. 为什么纸笔推不动了:把几何翻译成计算机能枚举的组合问题
2.1 核心翻译:从连续几何到离散标号
我第一次接触这个课题时,最困惑的就是:一个发生在连续空间里的几何问题,凭什么能被计算机做暴力搜索?计算机再快,也不可能逐个检查无限多个平移位置。直到我看懂核心翻译技巧,才明白问题比想象中更适合变成离散枚举。
关键在于“平移周期性”这个条件。如果一个平移密铺在空间中按晶格周期重复,那么这整片无限铺开的图形,本质上可以“卷”到一个有限大小的环面上来研究。打个比方,小时候玩《吃豆人》,小人从屏幕左边穿出去,会从右边穿回来——平面被首尾相接卷成了一个环。高维密铺也是同理:把无限空间按周期折叠,每个超立方体都对应环面上的一个格点。这样,一个看似无穷大的几何对象,就缩减成一个有限的“格点集合”。
接下来要做的是给每个格点贴一个标号。标号的含义可以理解为“这个超立方体在周期单元内的相对位置状态”。两个相邻超立方体在空间里是否出现完整的公共超面,完全由两个格点的相对位置和它们上面的标号联合决定。于是原问题就翻译成了这样一个离散问题:在这个有限环面的格点集合上,能不能找到一组标号,使得任意平移向量对应的相邻关系,都不会触发“完整面重合”的标志——如果找不到这样一组标号,那么原猜想在相应维度成立;如果找到了,那这组标号本身就是一个完整的反例。
这一手翻译把几何全藏了起来,剩下的只是纯粹的集合枚举和逻辑判定,恰好是计算机最擅长的事。我当时最大的感受是:几何学家能拧出这种翻译,靠的是对周期性和对称性极其敏感的本能。这个步骤不需要超算,需要的是把问题彻底吃透。
2.2 搜索空间有多大:数字大到让你怀疑人生
翻译成离散问题之后,下一步就是估算工作量。不估算还好,一估算直接让人头皮发麻。
在 n 维环面上,哪怕只取最小周期,格点数量也会随维度指数增长。而每个格点又能取若干种标号,整个组合空间是“标号数量的格点次方”这么大。这不是普通的指数爆炸,而是指数套指数的双重爆炸。
我自己整理过一张粗略的数量级示意表,方便团队里不搞数学的同事理解为什么非得上机器:
| 维度 n | 最小周期格点数(量级示意) | 标号组合数(量级示意) | 人工可验证性 |
|---|---|---|---|
| 2 | 4 | 百级 | 完全可以 |
| 3 | 8 | 万级 | 勉强可以 |
| 4 | 16 | 十万级 | 不现实 |
| 5 | 32 | 千万级 | 不可能 |
| 6 | 64 | 万亿级 | 基本无望 |
| 7 及以上 | 128 以上 | 远超可观测宇宙原子数 | 必须靠程序 |
注意,这张表里的数字并不是精确计数,只是用来表达量级感的示意,实际的等价类数量会因为对称性而缩减,但缩减之后依然是大到吓人的规模。当维度超过六,哪怕你用上全球所有计算机并行枚举,也没法用“裸搜”的方式扫完整个空间。这时就需要优化三板斧:剪枝、传播、位运算。把这些做到极致,才有可能让一个大学实验室里的几台普通笔记本去挑战这个规模。
3. 实操过程:程序怎么写、机器怎么跑、笔记本怎么冒烟
3.1 第一版程序:朴素深度优先搜索
我们最开始写的程序非常简单,就是一个深度优先搜索加约束检查。思路可以这样描述:按顺序给环面上每个格点赋标号,每赋一个,就检查当前已赋的部分和题目的“避免完整面重合”约束是否冲突,冲突就回退,不冲突就继续往下搜;搜到最后所有格点都有标号,而且没有冲突,那就是找到一个反例了。把关键逻辑写出来,大概就是下面这段伪代码:
def search(n, labels, pos): # pos 表示当前轮到了第几个格点 if pos == total_cells: # 所有格点都已赋值,做最终校验 return is_valid(labels) for val in candidate_values: labels[pos] = val # 只检查当前局部约束,能剪掉大量分支 if check_partial(labels): if search(n, labels, pos + 1): return True labels[pos] = None return False这个骨架谁都能写,但直接拿去跑高维就是死路一条。问题不在逻辑,而在搜索树实在太庞大。不剪枝的话,连五维都不一定能跑完。我印象特别深,第一次在 n=6 上跑这版程序,笔记本风扇直接拉满,跑了三个小时连一条完整路径都没搜出来。当时我就意识到,真正的难点不是翻译问题,而是如何让搜索程序在大规模的组合空间里活得够长。
3.2 优化三板斧:剪枝、传播、位运算
第一板斧是“对称性剪枝”。高维密铺看起来很自由,但本质上具有很强的对称性:平移、旋转、反射都可能产生等价的密铺结构。如果程序把等价的结构重复枚举很多遍,那就是在浪费生命。我们的做法是固定某个格点的标号,把搜索限定在代表元的范围内,凡是能通过对称变换映射到已有情况的分支,直接剪掉。这一刀下去,搜索空间能缩小几个数量级,具体比例和维度有关,但效果非常明显。
第二板斧是“约束传播”。朴素 DFS 只在赋值后做局部检查,其实很多时候,一个标号赋下去,其他格点能取什么值已经被限制死了。我们用类似 AC-3 弧一致性的思路:每赋一个值,立刻扫描它的所有邻居,把邻居候选值中不可能再出现的选项删掉;如果某个邻居的候选值集合变成空集,说明当前分支走不下去,马上回溯。这样能在早期就掐死大量无用分支,而不是一路走到黑才发现死路。
第三板斧是“位运算状态压缩”。每个格点的候选值集合本质上是一个有限集合,我们用整数掩码来表示,那么删除一个候选值就是做一次mask &= ~(1 << val),检查集合是否空就是mask == 0。原先循环遍历集合的操作,全变成常数时间的位运算。这段优化在这类组合搜索里几乎是决定性的,因为剪枝和传播都要反复检查候选值集合,如果每次都跑循环,速度会慢到让人崩溃。
把这三板斧都做完之后,同样的 n=6 任务从“三小时跑不完”缩短到“几分钟跑完”。我到现在都记得那个对比:几行关键优化程序,效果比买一台更贵的服务器还要明显。
3.3 机器配置和“烧坏”实录
搜索阶段,我们用的就是普通笔记本电脑,清一色 Intel 八核以上 CPU,内存在 16GB 到 32GB 之间,系统有 Windows 也有 Linux。没有用 GPU,因为这类离散搜索的瓶颈在 CPU 缓存和内存带宽上,显卡反而帮不上忙。每台笔记本负责搜索一个独立的维度区间,彼此之间不通信,最后再把各自找到的关键标号表汇总。
我记得最疯狂的那段日子,在 n=7 的边界问题上连续跑了一周。室友半夜上厕所,还以为空调坏了,走到书房才发现是笔记本散热口在咆哮。到了第三天,开始有机器偶尔死机,第五天蓝屏频率明显变高。最戏剧性的是收尾时,有一台长期顶着 100% 负载连续跑了好几天机器,从此再也开不了机,后来检修发现是供电模块烧掉了。另一台更惨,电源适配器直接鼓包,整个 AC 电源模块报废。
说实话,这些笔记本算不上高性能,纯粹是“核多内存大、方便各自独立跑”。我们不敢把所有赌注押在上面,后来把最长的任务转移到实验室工作站,再弄了两台云主机分担压力,但那两台烧坏的笔记本反而成了团队里一个梗:“这问题要是没推翻直觉,倒是先检验了笔记本的散热极限。”回头看,最大的教训是:当你预计某个搜索任务要连续跑三天以上,就不要让主力笔记本去扛,该用工作站就用工作站,该上云主机就上云主机,不然省下来的预算都会变成维修费和项目停滞的时间。
4. 常见问题与排查技巧实录
4.1 内存爆炸:不是算力不够,而是存得太多
我们在项目中途遇到过一个非常典型的性能问题:程序跑着跑着,可用内存被占满,系统开始疯狂换页,最终进程被杀,搜索白跑半天。
原因说出来有点可笑:初版程序里,我们试图把搜索过的大量中间状态缓存下来,想着多个搜索线程可以共享公共后缀,从而加速回溯。想法是好的,但状态数量增长太快,内存根本装不下。后来改成路径型搜索,不保留公共状态,只保留当前栈上的标号数组;每次走到完整路径就把标号表写入磁盘,然后清空继续下一条。这样内存占用瞬间降了几十个量级,代价是放弃了一部分复用加速,但对我们的问题规模来说,这是值得的。经验就是:状态复用可以带来效率提升,但如果内存扛不住,复用的收益全是空中楼阁。
4.2 浮点数带来的幽灵缝隙
另一个让我印象深刻的坑,是浮点数精度造成的“幽灵缝隙”。有几次,程序报告找到了一个反例,但人工去验证时发现两个超立方体明明共享完整超面,只是因为坐标计算用了浮点数,比较时出现了一点点误差,才被程序误判为“没有重合”。
这个 bug 很隐蔽,因为大多数时候浮点数计算都没问题,只在某些极端坐标组合下才会触发。解决倒也不难:所有坐标全改用整数表示,平移距离为 1 的立方体,它的交叠判断只涉及整数比较,完全不需要浮点数。改完之后,“幽灵缝隙”再也没出现过。我的经验是:组合搜索里,只要能用整数就别用浮点;如果必须用浮点,就要把容差设计得非常保守,否则假结果会把整个验证过程搅得天翻地覆。
4.3 程序写出反例不等于数学反例存在
项目进行到后半程,我们遇到了一次最严重的信任危机:某次搜索程序在 n=7 上返回“找到了”,但独立验证程序一跑,发现有个格点的标号在搜索过程中被错误赋值。问题出在剪枝函数某个边界判断少写了一个条件,导致一条本不该通过的分支被当成合法路径走完了。
这件事让我学到一个铁律:程序说它找到反例,只是一个开始。如果反例结论足够重要,你必须用完全不相关的第二套逻辑来验证它。我们当时的处理方式分三层:第一层,写一个完全独立的验证器,只读标号表,不依赖搜索程序的任何剪枝逻辑,逐项检查所有平移向量;第二层,把同一组标号问题翻译成布尔可满足性问题(SAT),丢给主流 SAT 求解器复验;第三层,等前两层通过之后,再人工把核心结构提炼成可读的引理,用传统数学推理论证一遍。三层全过了,我才敢说这个反例是真的。
为了防止以后再掉进同一个坑,我们还立了一个规矩:搜索程序和验证程序必须由不同的人来写。写搜索的人知道剪枝的每一个细节,容易把同一个错误思维带进验证逻辑;换一个人从零开始读标号表,反而更容易暴露问题。
4.4 低维冒烟测试:先让程序证明自己
在所有高维搜索开始之前,我坚持做了另一件看似浪费时间的事情:先让程序在二维、三维上跑一遍,并且断言“必须输不出反例”。因为这两个维度的结论已经被严格证明了,如果程序在低维居然输出“找到反例”,那毫无疑问是程序自己写错了。
这个“冒烟测试”救了我们无数次。每次修改剪枝逻辑或传播函数,我们都先跑一遍低维回归测试,确认程序行为没被改坏,再放它去高维世界探索。很多看起来诡异的 bug,往往是在改动剪枝时不小心把约束方向搞反了,如果不先在低维暴露出来,直接扔到高维,可能会浪费好几个星期的计算资源,拿回来一堆模棱两可的结果。我的建议就是:你在未知领域搜索之前,先让代码在已知答案的题目上证明自己,否则你永远分不清输出结果是科学发现还是程序幻觉。
这个原则不光适用于数学研究,任何用算法处理开放问题的场景都应该这样做。我见过太多项目,拿到不确定结果之后第一反应是“机器是不是坏了”,而不是“我的代码逻辑是不是错了”。
这里把常见问题整理成一张速查表,方便以后做类似搜索项目的朋友直接参考:
| 问题 | 可能原因 | 处理办法 |
|---|---|---|
| 内存占用持续上涨,最终 OOM | 缓存了过多中间状态 | 改成路径式搜索,状态落盘 |
| 程序报告反例但人工验证不符 | 浮点坐标计算误差 | 全部改用整数坐标 |
| 搜索程序与验证程序结论不一致 | 剪枝/传播函数有逻辑漏洞 | 换人独立写验证器 |
| 机器连续运行后死机蓝屏 | 长时间满载,散热不足 | 降频运行,任务转移到工作站 |
| 高维结果不稳定,多次运行不一致 | 并行子任务边界切分错误 | 固定随机种子,记录每次切分 |
| 程序在低维就输出反例 | 约束实现方向写反 | 先跑低维冒烟测试 |
5. 结果影响与后续思考:反例长什么样,教训是什么
5.1 反例的形状:像牙齿咬合一样的高维密铺
最终找到的反例,不是一个能被简单画出来的精美几何体,而是一张规模巨大的标号表。这张表描述的是某个高维环面上所有格点标号的分布,它满足了程序提出的全部约束条件:任意平移向量触发的相邻检查,都不会产生完整的 (n-1) 维超面共享。
我在理解这张表的时候,脑子里冒出来的画面是两排交错的牙齿。想象两排牙齿咬合在一起,齿尖嵌进齿缝,每一对接触都只是点和线的高低位相切,没有任何一对是“面贴面”。低维空间里你拼不出这种结构,因为空间太“挤”了,错位必然留出缝隙;可高维空间里,超立方体可以把公共部分“稀释”到更低维的面上,一层叠一层,最后依然把整个空间填得满满当当。这就是那个反例最反直觉的地方:它没有破坏空间填充的完整度,只是彻底避开了“完整面重合”。
验证过程则非常朴素:对任意一个可能的相对平移向量,程序去标号表里查对应位置的组合情况,逐项检查是否满足“没有完整面共享”,同时还要用另一个独立程序交叉确认空间确实被完整覆盖。所有检查通过之后,这个反例就从“程序输出”升级成了“可重复验证的数学事实”。它能推翻那个一百五十年的直觉,靠的不是某一台电脑的运气,而是一整套可审计、可复现的验证链条。
5.2 这次计算给我的三条教训
第一,低维外推在几何里极度危险。低维空间给人类的直觉是“紧”,但高维空间给人类的真相经常是“松”。二维、三维甚至六维都成立,这听起来是个很强的信号,但对数学来说,信号再强也不是证明。
第二,计算机辅助证明正在成为几何拓扑的常态。以前我们觉得“用程序找反例”只是数论或组合领域的小技巧,现在它已经能深入几何拓扑这种传统上非常依赖直觉推理的领域。计算机没有“代替”数学思考,它像一把铲子,先帮你把不知道藏在哪里、但确实存在的东西挖出来;等到东西摆到桌面上了,真正的数学推理才开始。
第三,程序输出反例之后,最困难的部分才刚刚开始。你要证明这个反例不是巧合、不是 bug、不是数值误差。独立验证器、SAT 复验、人工提炼引理,每一步都有可能推翻前面的结论。我见过太多人倒在第一层验证上——程序说找到了,他就欢呼,结果第二天就被验证器打脸。
这个项目最让我感慨的地方,是它证明了“直觉”和“计算”并不是数学研究的对立面。那个一百五十年的直觉,帮我们定义了什么是值得研究的问题;而那几台烧坏的笔记本,帮我们看清了直觉边界在哪里。两者缺一不可。
最后再说点个人体会。如果你以后也要做这种大规模搜索类的数学项目,我强烈建议你一开始就写好独立验证程序,别等搜索程序跑完再补。验证器丑一点、慢一点都没关系,它的人格尊严就是在关键时刻说“不”。另外,别拿主力笔记本当超算用,温度监控一报警就赶紧把任务迁走。这次烧坏两台笔记本换来的教训,说到底就是一句话:在数学和硬件的交叉地带,先保护好你的工具,再想着保护你的直觉。