算法一千零一夜 · 第十三夜 幻方,武侠世界里的数学迷宫

第十三夜 · 幻方,武侠世界里的数学迷宫

大一春天的那一夜,我迷上了幻方。

现在,我想给那个天真的我写一封回信。这封信得从一个暑假讲起,一个被我整个泡进武侠里的暑假。

初三毕业后的那个暑假,中考完的我在家闲着没事儿,赶上了网上的暑假档,59集的《射雕英雄传》。追完正剧不过瘾,东邪、西毒那些外传又是一部不落地接着追。追得太狠,家里宽带的流量用超了。被人说了,花了230块钱。

那是我第一次知道,一个暑假可以疯成这样。

也是在那阵子,屏幕里的故事走到了黑泽。黄蓉挨了裘千仞一记铁掌,全靠一件软猬甲挡去了一半掌力才保住性命。郭靖背着她就往大理赶,去求一灯大师出手,黑泽里却先撞见了一位隐居的算学高手,瑛姑。

她考黄蓉的,是一道数阵。你的算法自然精我百倍,可是我问你,将一至九这九个数字排成三列,不论纵横斜角,每三字相加都是十五,如何排法。

黄蓉当它不算什么题。九宫之义,法以灵龟,二四为肩,六八为足,左三右七,戴九履一,五居中央。三句话,九个数各归其位,横行、竖列、斜角,每三个相加都是十五。

瑛姑呆住了。这位在黑泽里苦算了十几年的高手,只道这是我独创的秘法,原来早有歌诀传世。

我盯着屏幕,却记住了另一件事。就是从那一集开始,我遇见了一只大乌龟。传说大禹治水的时候,有神龟浮出洛水,背上驮着一幅点阵图,数至于九。后人管它叫洛书,把它当作世间最早的幻方。

这是我第一次见到幻方的样子,但是当时还不知道它叫幻方。

四年之后,大一春天,谭浩强的《C 程序设计》把这道题原封不动摆在了数组一章的课后。我把标准答案照着抄了三天,抄出来 289 行,全部写在一个 main 函数里,连收尾都是一句 system("pause")。有一处数组声明,我抄的是 int t10000-1,一万减一,明明可以写 9999,当年的我却老老实实写了个算式,仿佛那道题连减法都要留给读者自己算。

敲完了还是没看懂,就觉得很刺激。

我不死心,特地查了一下这道题叫什么。书上叫它幻方,七百年前的杨辉叫它纵横图。那为什么叫幻方呢?我当时有一个很天真的想法,变幻莫测的方块。很搞笑吧,这就是年少的我。

今天写这一夜,我把当年抄的代码翻了出来。这段是里面对换的机关,收进过文本的只有这一段,其余 289 行的原貌,留在我的硬盘里。

c 复制代码
k=n*n/2-1;
for(i=0;i<n;i++)
for(j=0;j<n;j++)
if((j%4==i%4)||(i%4+j%4==3))
{
    a[i][j]=t[k];
    k--;
}

先说名字,这是给天真的我的第一句解答。

这只乌龟驮着的图,在中国最早叫洛书,画进方格之后叫九宫,南宋杨辉写《续古摘奇算法》,给它起名纵横图。而幻方这两个字,是近代从西文的 magic square 翻译过来的,幻,就是 magic。我当年猜变幻莫测的方块,居然歪打正着,猜中了它一半的灵魂。瑛姑说早有歌诀传世,传世的歌诀比她的苦算老了几百岁,比这只乌龟又小了几千岁。

然后是填法。黄蓉答的那道三阶题,其实有一个万能章法,杨辉把它写得清清楚楚,一共三条。把 1 放在第一行的正中间,对 2 到9 八个数的每一个数,走向右上一格;出了边界,就绕回另一头;格子里已经有数了,就退到原位的正下方。

三条规则,逐格执行,九个数各就各位。我的 Java 版照着章法走了一遍,跑出来的是 8 1 6、3 5 7、4 9 2,和谭浩强书页上的那一副一模一样。顺手续到五阶,二十五个数,每行每列每条对角线的和都是 65。这是奇数阶的通解,五阶、七阶、九阶,一路通用。

java 复制代码
public class Night13Siamese {
    public static void main(String[] args) {
        fill(3);
        System.out.println();
        fill(5);
    }

    static void fill(int n) {
        int[][] a = new int[n + 1][n + 1];          // 下标 1..n,0 行 0 列弃用
        int i = 1, j = n / 2 + 1;                    // 1 放在第一行的正中间
        a[i][j] = 1;
        for (int k = 2; k <= n * n; k++) {
            i--; j++;                                // 尝试右上
            if (i < 1 && j > n) { i += 2; j--; }     // 右上同时出界:原位正下方
            else {
                if (i < 1) i = n;                    // 出上边界:绕到最后一行
                if (j > n) j = 1;                    // 出右边界:绕回第一列
            }
            if (a[i][j] != 0) { i += 2; j--; }       // 该格已被占:退回原位正下方
            a[i][j] = k;
        }
        int m = n * (n * n + 1) / 2;                 // 幻方常数
        for (int r = 1; r <= n; r++) {
            StringBuilder sb = new StringBuilder();
            for (int c = 1; c <= n; c++) sb.append(String.format("%4d", a[r][c]));
            System.out.println(sb.toString());
        }
        System.out.println("n=" + n + " constant=" + m + " rowsOk=" + check(a, n, m));
    }

    static boolean check(int[][] a, int n, int m) {
        for (int r = 1; r <= n; r++) {
            int s = 0;
            for (int c = 1; c <= n; c++) s += a[r][c];
            if (s != m) return false;
        }
        for (int c = 1; c <= n; c++) {
            int s = 0;
            for (int r = 1; r <= n; r++) s += a[r][c];
            if (s != m) return false;
        }
        int d1 = 0, d2 = 0;
        for (int r = 1; r <= n; r++) { d1 += a[r][r]; d2 += a[r][n + 1 - r]; }
        return d1 == m && d2 == m;
    }
}

瑛姑不服,把数字从九个加到了十六个,四四图。一至十六排成四行四列,纵横斜角之和全要相等。黄蓉还是那副轻描淡写的口气,就说四四图罢,以十六字依次作四行排列,先以四角对换,一换十六,四换十三,后以内四角对换,六换十一,七换十。这般横直上下斜角相加,皆是三十四。

看清楚了,先按顺序把 1 到 16 填满,然后只动八个格子,四个角互换,里面的四个角再互换,一道十六个数的题就解决了。我大一抄的那段 t 数组代码,if 条件收出来倒着放回去的,恰好就是这八个格子。黄蓉在黑泽的沙地上画的四步,和标准答案的算法,是同一个动作。

java 复制代码
public class Night13Corners {
    public static void main(String[] args) {
        int n = 4;
        int[][] a = new int[n][n];
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                a[i][j] = i * n + j + 1;             // 依次作四行排列

        swap(a, 0, 0, 3, 3);                          // 一换十六
        swap(a, 0, 3, 3, 0);                          // 四换十三
        swap(a, 1, 1, 2, 2);                          // 六换十一
        swap(a, 1, 2, 2, 1);                          // 七换十

        for (int i = 0; i < n; i++) {
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < n; j++) sb.append(String.format("%4d", a[i][j]));
            System.out.println(sb.toString());
        }
        int m = n * (n * n + 1) / 2;                  // 幻方常数 34
        boolean ok = true;
        for (int i = 0; i < n; i++) {
            int r = 0, c = 0;
            for (int j = 0; j < n; j++) { r += a[i][j]; c += a[j][i]; }
            if (r != m || c != m) ok = false;
        }
        int d1 = 0, d2 = 0;
        for (int i = 0; i < n; i++) { d1 += a[i][i]; d2 += a[i][n - 1 - i]; }
        System.out.println("constant=" + m + " rowsColsOk=" + ok + " diag=" + d1 + "/" + d2);
    }

    static void swap(int[][] a, int r1, int c1, int r2, int c2) {
        int t = a[r1][c1]; a[r1][c1] = a[r2][c2]; a[r2][c2] = t;
    }
}

其实瑛姑没有考到第三题。黄蓉说,四四图,五五图,以至百子图,亦不足为奇。百子图是十阶幻方的古名,十除以四余二,是三类里最难缠的一类。瑛姑考了奇数阶和双偶阶,剩下的单偶阶,教材的标准答案替她考了。我的手稿三案全解,倒不是当年的我比黄蓉强,是标准答案比瑛姑勤快。

这一夜,我还把笨办法请了出来当裁判。三阶幻方,九个数随便排,一共 9 的阶乘种排法,362880 种。机器一种一种全部试完,用了 8 毫秒。成立的只有 8 个,而且这 8 个全是同一个解的旋转和镜像,本质唯一。

java 复制代码
public class Night13Brute {
    static int count = 0;

    public static void main(String[] args) {
        int[] p = new int[9];
        for (int i = 0; i < 9; i++) p[i] = i + 1;
        long t0 = System.currentTimeMillis();
        perm(p, 0);
        long t1 = System.currentTimeMillis();
        System.out.println("tried=362880 magic=" + count + " essential=1 orientations=8 time=" + (t1 - t0) + "ms");
        // 8 个成立解全是同一个解的旋转与镜像,所以本质唯一,只有 1 个。
    }

    static void perm(int[] p, int d) {
        if (d == 9) {
            if (magic(p)) count++;
            return;
        }
        for (int i = d; i < 9; i++) {
            int t = p[d]; p[d] = p[i]; p[i] = t;
            perm(p, d + 1);
            t = p[d]; p[d] = p[i]; p[i] = t;
        }
    }

    static boolean magic(int[] p) {
        // p 排进 3x3:p[0] p[1] p[2] / p[3] p[4] p[5] / p[6] p[7] p[8]
        for (int r = 0; r < 3; r++) {
            int s = p[r * 3] + p[r * 3 + 1] + p[r * 3 + 2];
            if (s != 15) return false;
        }
        for (int c = 0; c < 3; c++) {
            int s = p[c] + p[c + 3] + p[c + 6];
            if (s != 15) return false;
        }
        return p[0] + p[4] + p[8] == 15 && p[2] + p[4] + p[6] == 15;
    }
}

瑛姑苦算了十几年没有算出的谜底,是这八个解本来就是同一个解。黄蓉背得出歌诀,也未必数过那 362880 种排法。机器用 8 毫秒替她们两人都数完了。

那么四阶呢?十六的阶乘,超过 20 万亿种。机器还敢试吗?这一次笨办法走到了自己的边界,全枚举这条路,在三阶是 8 毫秒的坦途,在四阶就是一座翻不过去的山。所以黄蓉没有数,她用了章法。三阶可以交给笨办法,四阶必须交给聪明办法,而我的手稿里,两样都有。

笨办法有笨办法的天花板,聪明办法有聪明办法的来历。瑛姑输给黄蓉的那一天,输的从来不是智力,是她的十几年的苦算,没能遇上一句传世的歌诀。而歌诀能传世,是因为千年前有人也这样一格一格地摆过,摆出了那只龟背上的图案。

所以,天真的我,这是给你的回信。

你在黑泽里遇见的那只乌龟,驮的不是一段剧情,是几千年的传承。你抄的 289 行一行都没有白抄,它们只是在等,等到一台肯陪这道题把 362880 种摆法全部数完的机器。你看懂看不懂都要紧,刺激是真的,就够了。

这就是一个在试卷里面得不到数学趣味的孩子,用计算机再一次见证了那个变幻莫测的魔方。


① 真伪核对 逐条照实登记。

 Ⅰ 射雕与暑假 1983 版《射雕英雄传》由香港无线电视制作,1983 年首播,分《铁血丹心》《东邪西毒》《华山论剑》三辑;黄蓉一角由全港招募、金庸亲自参与评判选出,翁美玲当选(真)。初三暑假追剧与流量超额 230 元为本人亲历自述,追剧集数为当年记忆口径,含同系外传多部(真,亲历自述)。翁美玲 1985 年离世(真)。

 Ⅱ 剧情链 黄蓉伤于裘千仞铁掌,软猬甲挡去部分掌力;郭靖携黄蓉往大理求一灯大师,途经黑泽遇瑛姑,瑛姑以算学相试后指点去向(真,据原著第二十九回《黑沼隐女》;俗称铁砂掌,原著功夫名为铁掌)。

 Ⅲ 瑛姑 本名刘瑛,原为大理南帝贵妃,隐居黑泽苦研算学与武功十余年(真)。

 Ⅳ 九宫歌诀 「九宫之义,法以灵龟,二四为肩,六八为足,左三右七,戴九履一,五居中央」承自《数术记遗》九宫旧注,金庸原著第二十九回黄蓉原话引此歌诀;三阶幻方即洛书,纵横斜角三数之和皆为十五(真)。

 Ⅴ 神龟负书 《汉书·五行志》引刘歆说,天兴禹洛出书,神龟负文而出,列于背,有数至于九,禹遂因而第之;更早源头为《周易·系辞上》河出图、洛出书,未言龟(真)。民间有归于伏羲者,系河图洛书混称,本夜不取。

 Ⅵ 杨辉与四四图 杨辉《续古摘奇算法》(1275 年)称幻方为纵横图并载构造法(真)。原著四四图一题,黄蓉以四角对换(一换十六、四换十三)加内四角对换(六换十一、七换十)解之,与本夜代码二为同一算法,对换后各行、列、对角线之和皆为 34(真,实跑验证)。百子图为十阶幻方古名(真)。

 Ⅶ 词史 九宫图形化后入术数,杨辉创称纵横图;幻方系近代西文 magic square 之汉译(真)。

 Ⅷ 谭浩强习题 《C 程序设计(第五版)》第六章课后习题收幻方,三阶示例 8 1 6、3 5 7、4 9 2(真)。

 Ⅸ 手稿照登 本人大一文物,289 行,全部写在一个 main 函数里;本夜仅节选对换机关一段,gcc 实跑 n=3、4、5、6、8、10 全部成立(真,实跑验证,已披露)。

 Ⅹ 实跑记录 JDK 21。连续摆法 n=3 输出 8 1 6、3 5 7、4 9 2,n=5 常数 65;四角对换法 n=4 常数 34,对角线 34;全排列枚举 362880 种,成立解 8 个,本质唯一,耗时 8 毫秒(真,实跑验证)。

② 今夜习题 三道,评论区见。

 Ⅰ 五五图。用连续摆法把 1 到 25 填成五阶幻方,算一算每行、每列、每条对角线的和,看看是不是都是 65。

 Ⅱ 四四图。按黄蓉的四步亲手排一遍,先顺排一至十六,再四角对换、内四角对换。数一数你一共动了几格,再想想,为什么只动八格就够。

 Ⅲ 笨办法的天花板。三阶幻方全枚举要试 362880 种,你的机器不到一秒。四阶是十六的阶乘,先估算它有多少种,再估算你的机器全试一遍要跑多久。想完之后你就知道,黄蓉为什么不数数,直接报了口诀。


本书目录导航 上一篇:第十二夜 一个数学学渣,用计算机解奥数题 下一篇:第十四夜 幻方与奇妙的二进制 起点:卷首语·算法童话


版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。

相关推荐
专业程序开发源1 小时前
springboot欧缇蔓月子中心管理系统82697-计算机课程设计、毕业设计
java·spring boot·后端·python·spring·django·课程设计
秋天的怀念_David1 小时前
算法一千零一夜 · 第十四夜 幻方与奇妙的二进制
java·程序人生·算法
thefool1122661 小时前
网络编程^
java
薛定e的猫咪1 小时前
(ICLR 2020)MAML 靠的不是快速学习,而是特征复用
人工智能·深度学习·学习·算法·机器学习
薛定e的猫咪2 小时前
(ICLR 2026)DF-META:面向 FJSP 的无导数元学习实例自适应调度
人工智能·深度学习·学习·算法·机器学习
Thinker QAQ2 小时前
并发编程(十一):Shared Memory 与 Message Passing——关系与组合使用
java·python·go·并发编程·shared memory·message passing
齐 飞2 小时前
idea中git误操作撤回git reset --hard ‘HEAD@{1}‘
java·git·intellij-idea
你的保护色3 小时前
多AS互联BGP4综合实验设计
java
某不知名網友3 小时前
ROS2进阶:服务通信、自定义SRV接口、参数服务器与基于服务的参数通信详解
java·开发语言·microsoft