第三夜 · 神庙之夜的汉诺塔
灯又亮了。第三夜。这一夜不回那间教室。上一夜散场,我带走的是那个归字。递是出发,归是回家,接不归的棋救不回。可是回去的路上,我一直在想这句话的后半截。既然要回家,那条回家的路,有多长?
这个问题,一段 4 岁的故事先替我起了个头。
从前有座山,山里有座庙,庙里有个老和尚在给小和尚讲故事。讲的什么?讲的还是从前有座山,山里有座庙,庙里有个小和尚在听老和尚讲故事。再往里讲一层,还是它自己。故事套着故事,层与层一模一样,全都是同一座山,同一座庙,同一个讲不完的老和尚。
大人们拿这段话哄孩子。孩子缠着要听故事,大人不肯编新的,就递出这座山。孩子安静了,因为这段故事真的没有底。不是讲的人偷懒,是它本来就没打算讲完。下一层和上一层一模一样,只是听故事的人更小,夜更深。你随时可以走开,故事不怪你。故事里那个小和尚,到今天还在听。
我第一次在两面镜子中间见到这段故事,是在三年级。那天跟着外婆和舅舅一家人逛江苏省科技馆,展厅里立着两面相对的镜子。外婆那时是物理老师,她把我拉到两面镜子中间,给我讲镜面反射,讲镜面成像。
我往前一看,镜子里套着镜子,我自己一层一层退下去,越来越小,越来越小,朝着最深处退过去。所有的一切,最后都消散在透视的原点,缩成一个黑点。外婆在旁边讲她的原理,我在那个黑点跟前看了很久。镜里的像无穷无尽,人的视野却是有穷的。无穷的像,走到有穷的视野尽头,就只剩下那一个点。
道理上,镜子后面还有镜子,像的后面还有像,无限就站在那里,站得端端正正。可我亲眼看见,那无穷无尽的像,最终消散在透视点处,收成一个黑点。无穷是镜子的事,有穷是看的人的事。
后来我学递归,才知道镜子其实很客气。上一夜那口井,塌在第 22835 层,不是运气差。镜子再深,最深处的无穷,最后收成安安静静一个黑点,挂在透视的原点上。递归不这么客气。递归写坏了,尽头不是黑点,是黑洞,什么掉进去,当场爆炸。庙里的故事一层套一层,镜子里的像一层退一层,怎么套怎么退,都有一个原点替它们收尾;递归收不住的时候,没有谁替它收尾。所以机器收下递归的那一刻,同时递过来一把尺。写递归的人得先接住这把尺,再去谈无限。
好了。现在,让老和尚开口。这一回,他不再讲那座山了。
他说,从前,恒河边上有一座庙。
庙不稀奇,稀奇的是庙里那桩差事。庙里立着三根柱子,通体是宝石磨的。其中一根柱子上套着六十四片金盘,最大的在底下,越往上越小,摞成一座塔。庙里的僧侣们领了差事,要把这座塔从这根柱子搬到另一根柱子上去。规则只有两条。其一,一次只许挪一片。其二,无论什么时候,大片不许压在小片上。
僧侣们领了差事,日复一日地搬。金盘一片一片换柱,钟声一天一天响着,塔还是那座塔,模样没变。传说讲到这里,露出了牙。它说,等最后一片金盘落位的那一天,这个世界就到了尽头。
这个故事你多半听过。听过也不要紧,编它的人就盼着你听。1883 年,法国数学家吕卡斯做出了这个玩具,又亲手写下这段传说,一并印在说明书里。玩具卖遍了欧洲,传说跟着走遍了世界,一百多年后还住进了每一本编程教材。讲传说的书,自己讲的传说也是编的。这一夜的庙,两层都是编的。这一笔我不瞒你,照实记在夜末的核对表里,像第一夜把牛顿的名字还回去一样。
预言归预言。现在,把预言翻译成算术。
搬一片,算一步。想把最大的那片从甲柱挪到丙柱,得先把压在它上面的六十三片整个搬到乙柱去;等它落了位,再把六十三片从乙柱请回来。搬六十三片是同样的麻烦,得先搬六十二片。这样一层一层想下去,每加深一片,功夫翻一倍,再加一。一片要一步,两片要三步,三片要七步,十片要一千零二十三步。六十四片,一共是 2⁶⁴ 减 1 步。
18446744073709551615。
每秒挪一片,不吃饭,不睡觉,不过节,这个数折合 5849 亿年。宇宙到今天 138 亿岁。僧侣们要连搬上四十二个宇宙的年纪,才能听见最后一片金盘落位的那一声轻响。
这个数的分量,得拿人生去称。三片的小庙,七步搬完,每一步都画在下面这张图里。二十片要十二天,一个下午变成了两个星期。三十片要三十四年,动手的小伙子搬成了老汉。四十片要三万五千年,长过全部文字的历史。五十片要三千五百多万年,长过整个人类物种的年纪。六十四片,5849 亿年。预言没有说谎,它只是把日程表的最后一行,写在了谁也等不到的年份里。

图 3-1 三片金盘的七步搬法。灰色加粗的盘,是这一步正在挪动的那片。从初始到第 7 步,塔从甲柱完整落到丙柱,任何时候都没有大片压住小片。第 6 步之后乙柱空了,最省的搬法也不必用上每一根柱。
预言翻完了。接下来这一位,才是真正吓过数学家的。
1872 年,柏林。威尔斯特拉斯当着科学院摆出一条曲线,处处连着,却处处立不起切线。要知道,那个年代的数学家默认连续的曲线总该有光滑的段落,好比人人都默认山总有坡度缓的一段。这一条没有。它处处连着,却寸步难滑。分析学界哗然,埃尔米特在信里自陈,怀着恐惧与惊骇,避开这场可悲的瘟疫。三十二年之后,科赫用一把直尺和初等几何,把这群怪物里最驯顺的一只牵到了每个人眼前。就是下面这片雪花。
从一片正三角形开始。把每条边三等分,中间那一段向外顶成一片尖角,尖角自己也是一个小三角形。第二层,对着现有的每一条边再做一遍同样的事。第三层,再来。图形越磨越碎,越磨越像冬夜窗上的霜花。本夜的程序亲手把它磨了一遍,从零层磨到三层,画在下面。

图 3-2 科赫雪花的头四层。从左到右,打磨了零、一、二、三层。图是本夜的程序自己画出来的,代码附在夜末。
磨到无穷多层,这片雪花的面积,停在最初那片三角形的一又五分之三倍,封了顶,一步也不再走。它的周长却是另一副脾气。每磨一层,周长变成上一层的 4/3 倍。乘一个比一大的数,乘个不停,周长通向无限。
一片巴掌大的雪花,镶着一条走不完的边。这一次是真的走不完。不是走很久,是定义本身就到不了。这才是无限本来的脸色。
可是没有人怕雪花。没有人怕,因为没有人真的去走那条边。走不完写在定义里,谁也不欠它一步。
庙里那条路正相反。六十四片,片片数得清,步步都要人挪,每一步都真实存在。让世界惦记了一百多年的,从来不是无限,是一个每一位都写得出来的大数。真无限反倒和善,吓人的是数得出来的有限。
老和尚的故事,讲到这里本该收尾了。这一回,插话的是小和尚。
师父,庙里为什么只有三根柱子?
老和尚不答。传说只发三根柱子,不发为什么。小和尚又问,那我能不能先把头顶的小塔整座寄存到别的柱子上,腾出手来搬底下的大山,等大山落了位,再回来取小塔?
老和尚还是不答。庙里只有三根柱子,第四根柱子,传说没有发。
可是山下的世界,不归传说管。
1941 年,大西洋彼岸的一本数学杂志上,Frame 和 Stewart 把第四根柱子插进了庙里。两人素不相识,文章登在同一卷、相邻的两页,交来的却是同一套搬法。先把头顶的小塔整座寄存到第四根柱上。寄存几片最省,这个问题自己又是一道递归。腾出手来,用老三柱的功夫,把底下的大山搬完。最后回到第四根柱上,把小塔请回来,盖在大山的顶上。
六十四片金盘,四根柱子,18433 步。
一秒挪一片,五个小时零七分。日头升起来的时候僧侣们动手,日头还没偏西,塔落成了,世界好好的。压在世界头顶的末日,被多出来的那根柱子,改期到了当天晚饭前。
可是 1941 年,没有人敢把话说满。这套搬法是不是天下第一省,Frame 和 Stewart 都没有证明。往后的七十三年里,它一直顶着一个头衔,叫据信最省。全世界最会算的一批人试了一辈子,没能证明它是,也没能找到更省的。直到 2014 年,法国数学家布歇把它钉死了。第四根柱子上那套搬法,就是最省的搬法。柱子 1941 年就插好了,承认它的那句话,在路上走了七十三年。
北山愚公说过,子子孙孙无穷匮也。河曲智叟笑他,他就拿这句话作答。山不会再长高,子孙却没有尽头,一茬接一茬地挖,总有一天挖平。这个道理在许多年后有了名字,叫数学归纳法。只是愚公的两座山,最后是天帝派夸娥氏二子背走的,神替他收了尾。恒河边的庙没有神来。金盘要僧侣一片一片地搬,第四根柱子要人一代一代地想,从 1941 年一直想到 2014 年。
有一句话,我想原样放在这一夜里。人是不能打破规律的,但是看透规律之后,我们能做出更好的选择。大盘不压小盘,一次只挪一片,谁也改不了,也不必改。多一根柱子,多一层寄存,多一步回头,末日就改期了。规则没有被打破,被看透了。
庙里的僧侣不知道自己在倒数什么。他们不数日子,只数手边这一片。世界没有在倒数,僧侣们在慢慢搬;再长的路,也是一步一步走完的。
这一夜,我把庙写成了三段能跑的程序。第一段替僧侣数数,小塔数到七步,大塔数到钟声里。第二段插上第四根柱子。第三段磨雪花,图就是它画的。你要是不信末日会改期、周长没有底,自己跑一跑,数一数。
第一段 · 庙里的钟(三柱)
import java.io.PrintStream;
import java.math.BigInteger;
/**
* 《算法一千零一夜》第三夜 · 庙里的钟(三柱)
* 恒河边的庙,三根柱,六十四片金盘,大盘在下,小盘在上。
* 规则只有两条:一次挪一片,大片不压小片。
* 传说讲,最后一片金盘落位之日,世界终结。
* 这一版按最省的搬法替僧侣数数:小塔一步步看得见,大塔一声钟数得完。
*/
public class TempleBells {
static int moves = 0;
// 递,是把塔交给更小的塔;归,是一片落位,钟响一声。
static void move(int n, String from, String to, String via) {
if (n == 0) {
return; // 零片可搬,这一层到家。
}
move(n - 1, from, via, to); // 先把上面的塔寄存到中途柱。
moves++;
if (n <= 3) {
System.out.println("第 " + moves + " 步," + n + " 号盘," + from + "柱到" + to + "柱。");
}
move(n - 1, via, to, from); // 再把寄存的塔请回来,盖在大盘上。
}
public static void main(String[] args) throws Exception {
System.setOut(new PrintStream(System.out, true, "UTF-8"));
// 一、小庙演示。三片金盘,七步,与图 3-1 逐步核对。
System.out.println("三片金盘,从甲柱搬到丙柱:");
move(3, "甲", "丙", "乙");
System.out.println("三片共 " + moves + " 步。");
System.out.println();
// 二、大庙钟声。每加深一片,功夫翻倍再加一:1、3、7、15、31……
BigInteger steps = BigInteger.ONE;
for (int n = 2; n <= 64; n++) {
steps = steps.shiftLeft(1).add(BigInteger.ONE);
}
System.out.println("六十四片金盘,一共 " + steps + " 步。");
BigInteger perYear = BigInteger.valueOf(365L * 24 * 60 * 60); // 一年按 365 天,不闰。
BigInteger years = steps.divide(perYear);
System.out.println("每秒搬一片,约 " + years + " 年,约合 5849 亿年。");
BigInteger universe = BigInteger.valueOf(13800000000L); // 宇宙现龄约 138 亿年。
System.out.println("这个年头,约是宇宙现龄的 " + years.divide(universe) + " 倍。");
}
}
第二段 · 第四根柱子(四柱)
import java.io.PrintStream;
import java.math.BigInteger;
/**
* 《算法一千零一夜》第三夜 · 第四根柱子(四柱)
* 1941 年,Frame 与 Stewart 在同一本杂志的同一卷里,
* 各自给庙里插上了第四根柱子:先寄存小塔,搬完大山,再回来取塔。
* 寄存几片最省,这个问题自己又是一道递归。本程序替小僧把它数完。
*/
public class FourthPeg {
public static void main(String[] args) throws Exception {
System.setOut(new PrintStream(System.out, true, "UTF-8"));
// f(n) 是四根柱搬 n 片的最省步数。
// 搬法:寄存 k 片到第四根柱,三根柱搬完剩下的 n-k 片,再取回小塔。
// f(n) = min over 1 <= k < n { 2*f(k) + 2^(n-k) - 1 }。寄存几片最省,问 n 自己。
BigInteger[] f = new BigInteger[65];
f[1] = BigInteger.ONE; // 一片,一步。
for (int n = 2; n <= 64; n++) {
BigInteger best = null;
for (int k = 1; k < n; k++) {
BigInteger plan = f[k].shiftLeft(1) // 寄存一次,取回一次。
.add(BigInteger.ONE.shiftLeft(n - k).subtract(BigInteger.ONE)); // 三柱搬大山。
if (best == null || plan.compareTo(best) < 0) {
best = plan;
}
}
f[n] = best;
}
StringBuilder head = new StringBuilder();
for (int n = 1; n <= 10; n++) {
head.append(f[n]);
if (n < 10) {
head.append("、");
}
}
System.out.println("四柱搬 n 片的最省步数,前十项: " + head);
System.out.println("六十四片,四根柱子,共 " + f[64] + " 步。");
long secs = f[64].longValue(); // 18433,装得下。
System.out.println("每秒一片,约合 " + secs / 3600 + " 小时 " + secs % 3600 / 60 + " 分 " + secs % 60 + " 秒。");
BigInteger three = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE);
System.out.println("同一座塔,三根柱要 " + three + " 步;四根柱只要 " + f[64] + " 步。");
}
}
第三段 · 雪花的画笔(科赫)
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.PrintStream;
import javax.imageio.ImageIO;
/**
* 《算法一千零一夜》第三夜 · 雪花的画笔(科赫)
* 从一片正三角形开始,每条边的中间三分之一向外顶出尖角,
* 层层打磨。面积封了顶,周长没有底。
* 这一版一边打磨一边画,图就是程序自己画的。
*/
public class KochSnowflake {
// 递,是把一条边交给四条更短的边;归,是 depth 用尽,落笔画直线。
static void koch(Graphics2D g, double x1, double y1, double x2, double y2, int depth) {
if (depth == 0) {
g.drawLine((int) Math.round(x1), (int) Math.round(y1),
(int) Math.round(x2), (int) Math.round(y2));
return;
}
double dx = (x2 - x1) / 3, dy = (y2 - y1) / 3;
double ax = x1 + dx, ay = y1 + dy, bx = x1 + 2 * dx, by = y1 + 2 * dy;
// 尖点:底边中点向图形外侧顶起一个等边三角。屏幕坐标 y 朝下,外侧法线取(-dy3, dx3)。
double mx = (ax + bx) / 2 - (by - ay) * Math.sqrt(3) / 2;
double my = (ay + by) / 2 + (bx - ax) * Math.sqrt(3) / 2;
koch(g, x1, y1, ax, ay, depth - 1);
koch(g, ax, ay, mx, my, depth - 1);
koch(g, mx, my, bx, by, depth - 1);
koch(g, bx, by, x2, y2, depth - 1);
}
public static void main(String[] args) throws Exception {
System.setOut(new PrintStream(System.out, true, "UTF-8"));
// 一、画。四联图,从左到右磨了零、一、二、三层。
int W = 1600, H = 400;
BufferedImage img = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, W, H);
g.setColor(new Color(0x22, 0x22, 0x22));
g.setStroke(new BasicStroke(1.6f));
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
double edge = 200;
for (int i = 0; i < 4; i++) {
double cx = 200 + i * 400; // 每格中心。
double h = edge * Math.sqrt(3) / 2;
double ax = cx, ay = 190 - h / 2; // 顶点朝上的正三角形。
double bx = cx - edge / 2, by = 190 + h / 2;
double cx2 = cx + edge / 2, cy2 = 190 + h / 2;
int depth = i;
koch(g, ax, ay, bx, by, depth);
koch(g, bx, by, cx2, cy2, depth);
koch(g, cx2, cy2, ax, ay, depth);
}
ImageIO.write(img, "png", new File("fig3-2-koch.png"));
System.out.println("四联图已画完,落在 fig3-2-koch.png。");
// 二、数一数。周长每层乘 4/3,面积每层只添一点点。
double area0 = Math.sqrt(3) / 4; // 边长 1 的正三角形。
double perimeter = 3.0;
double area = area0;
double newCorners = 3; // 这一层新添的尖角个数。
double seg = 1.0 / 3; // 尖角的边长。
System.out.println("层 0,周长 3 倍,面积 1 倍。");
for (int n = 1; n <= 9; n++) {
perimeter = perimeter * 4 / 3;
area += newCorners * Math.sqrt(3) / 4 * seg * seg;
newCorners *= 4;
seg /= 3;
System.out.printf(java.util.Locale.ROOT, "层 %d,周长 %.3f 倍,面积 %.4f 倍。%n",
n, perimeter, area / area0);
}
}
}
① 真伪核对 逐条照实登记。
Ⅰ 吕卡斯与传说 汉诺塔玩具由法国数学家吕卡斯于 1883 年推出,上市时署名 N. Claus de Siam,这个名字是他本名加家乡(Lucas d'Amiens,亚眠)的字母变形;东方的皮是营销的包装,神庙传说出自这套包装,三根柱、六十四片金盘、搬完世界终结,全是编的。
Ⅱ 数字与换算 三柱六十四片共 2⁶⁴ 减 1 步,即 18446744073709551615 步;每秒一片、一年按 365 天折合 5849 亿年,为本夜程序实跑输出;宇宙现龄约 138 亿年,倍数约 42。
Ⅲ 四柱 1941 年 Frame 与 Stewart 在 American Mathematical Monthly 第 48 卷 216 至 219 页各自独立发表四柱搬法;四柱六十四片最少 18433 步,为本夜程序按 Frame-Stewart 递推实跑所得;2014 年布歇(Thierry Bousch)证明这个递推给出的正是最少步数,论文 La quatrième tour de Hanoï 载于 Bulletin of the Belgian Mathematical Society – Simon Stevin 第 21 卷第 5 期,895 至 912 页。坊间另有此文载于法国数学会通报第 142 卷的引法,系以讹传讹,那是它 2013 年预印本旧题目惹的祸。
Ⅳ 雪花 科赫 1904 年论文的题目直译过来,是"用初等几何构造的一条无切线的连续曲线";雪花面积收敛于初始三角形面积的 8/5,周长每层乘 4/3,均经本夜程序实跑核对。威尔斯特拉斯于 1872 年 7 月 18 日在柏林科学院报告此曲线;埃尔米特 1893 年致斯蒂尔杰斯的信里自陈,怀着恐惧与惊骇避开这场可悲的瘟疫。
Ⅴ 庙循环与愚公 从前有座山系民间口传,作者与年代不可考,如今是讲解无限递归时的常用例子。愚公引文出自《列子·汤问》,原文为"子子孙孙无穷匮也,而山不加增",结尾是"帝感其诚,命夸娥氏二子负二山"。
Ⅵ 实跑记录 三段程序均在本机 JDK 21 下编译运行;三片七步与图 3-1 逐格一致;四柱前十项为 1、3、5、9、13、17、25、33、41、49,六十四片 18433 步,折合 5 小时 7 分 13 秒;雪花第九层周长约为初始三角形周长的 13.3 倍,面积 1.5996 倍,正向 8/5 收敛。
Ⅶ 亲历登记 科技馆一节是亲历。三年级那年跟外婆和舅舅一家人逛江苏省科技馆,外婆那时是物理老师,镜面反射与成像的原理是她讲的;两面镜与透视原点的黑点,都是当年的实景。黑点的机理,是每折一次光就折损一分,深处渐暗,最终收成一个点;递归那边没有这份收敛,栈溢出当场终止程序,即正文所说的黑洞;第二夜那口井实跑塌在第 22835 层,照实登记。
② 关于证明 正文只留了七十三年一句话,证明放在这里。
Ⅰ 三柱的步数为什么省不掉 想让最大的那片动身,压在它上面的 n 减 1 片必须全部让开,而它要落的那根柱子必须腾空,于是这 n 减 1 片只能全体挤进剩下那一根柱。让路的功夫一步都省不掉,所以三柱的最少步数,至少是搬 n 减 1 片的两倍再加一,而搬法恰好做到这个数,$T(n)=2^n-1$ 就此锁死。
Ⅱ 四柱为什么难了七十三年 让路的队伍一旦可以分成两批,小塔去第四根柱,大山直接走,让路的成本就不再服从三柱的公式,上面的下界证明当场断裂。算法 1941 年就有,难的从来不是省,是证明没有更省的。
Ⅲ 布歇走的是图的路 把每一个合法局面画成一个点,每挪一片画一条边,全体局面连成一座巨大的图,两座塔之间的最短距离就是最少步数。布歇量清了这座图的距离结构,证明 Frame-Stewart 的数恰好就是那个距离。2014 年,下界闭合,七十三年的门开了。
Ⅳ 两条式子留档 $T(n)=2^n-1$。$S(n)=\min_{1\le k<n}\{2S(k)+2^{n-k}-1\}$。
③ 今夜习题 三道,评论区见。
Ⅰ 小庙里的金盘只有一小摞。僧侣们按最省的搬法搬完了它,一共 1023 步。这一摞有几片金盘?
Ⅱ 方丈给小庙添了第四根柱子。五片金盘从一根柱子搬到另一根柱子,最少几步?同样的五片,三根柱子要多少步?
Ⅲ 从一片正三角形开始打磨雪花,每打磨一层,周长变成上一层的 4/3 倍。打磨几层之后,周长第一次超过最初那片三角形周长的 10 倍?
你算出来了吗?评论区见。(答案与参考程序,收在书后《参考解答》。)
本书目录导航 上一篇:第二夜 算法“接不归” 下一篇:第四夜 影分身与记事本 起点:卷首语·算法童话
版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。
430




被折叠的 条评论
为什么被折叠?



