听故事学语言 · 数据结构篇 · 序章一 算法之问

本书是《听故事学语言》第二册。开卷之前,建议先读第二册前言。

听故事学语言 · 数据结构篇 · 序章一 算法之问

进入正文之前,先照录一篇旧文。2017 年 5 月 3 日,一个大一学生在 QQ 空间写下下面这篇《浅谈计算思维与文学思维》。九年过去,初衷未改,初心未变,当年认定的两句话至今还站在这里,算法是一种艺术,数据结构是意图的载体。稚嫩一字未删,因为这本书,本来就是要用文学的思维写成的。文章开头那道找假币的题,背着一个十二年的困惑,答案就在下面的正文里;至于文章里的另一种思维,文学思维,序章不谈它,整本书都在谈它。其后的正文《算法之问》,原样收录自我的另一部连载《算法教练陪练》开发笔记的第一期。

这本书叫数据结构篇,开篇却是一篇算法之问,读者难免要问,是不是走错了门。没有走错。程序设计有一道老等式,程序等于算法加数据结构,这一对名字从来是一枚硬币的两面,算法负责想,数据结构负责记,想得再好,记法不对,也是白想。在我的求学生涯里,这两问本来就有先后,先有高中的假币之问,问程序靠什么想;后有期末的链表之问,问程序靠什么记。前一个问题就住在下面这篇正文里,后一个问题你会在序章二撞见,它才是这本书的正题。回头看整个序章你还会发现,每一道算法题的身后都有结构作为载体,假币题的 Java 版用数组划开区间,逆波兰的下午靠两只栈接住表达式,工业现场的消息队列底下牵着链表。它们在这里都还没有名字,正文里一个一个报到。算法之问是门,数据结构之问是门里的世界。现在,敲门。

浅谈计算思维与文学思维

大家也许都曾经做过这样一个奥数题,有n个金币,这n的金币中有一个是假币。重量比其他n金币轻,现给你一架天平,请你用最少的步骤找出那枚假币,问方案是什么,需要经过多少步?

我想,大多数人也许会把这些金币尽可能平均的分成两堆,找出重量较轻的那一堆,继续以上步骤直至找出那枚假币。但是,这也许并不是最好的办法。最好的办法就是尽可能平均分成三堆,拿两堆相等的金币称重量,若相等则假币一定在剩下的那一堆里,若不相等则取出较轻的那一堆,继续以上步骤,直到找出假币为止。大家可以比较一下,这两个办法孰优孰劣一目了然。其实,这就是计算思维。

计算思维有很多优点。它的存在,在我看来是为了寻找解决问题的最佳途径。但还是存在一个问题,我找到了这个方式是否真的就是最佳的解决方式呢?这个方法是否一定正确呢?这样我们就要通过数学归纳法进行证明。只有这样,才能够形成一个逻辑上严密,并且具有可操作性的系统。通过计算思维,我们看待事物,会有一个别样的角度。这个角度,是理性的,是严谨的。通过自己大脑的思考和创新,我们也许可以创造出一个具有可行性并且与众不同的世界。这就是计算思维的妙处,可以说,计算思维不仅能够帮助我们解决实际问题而且也能够让我们创造一个属于自己的世界。并且把它付诸现实。做我们想到却做不到的事。通过设计最优的策略,并且分治处理和并行处理相互结合,能够大大提高处理事务的效率。甚至也可以将世界紧密联成一张巨大的网,让我们更快速更客观地了解世界。

但我想,对于本章开篇的问题,如果没有经过专业训练的人,是无法想到第二种方法的。同样一个问题,作为普通人的我们,只会按照,一般的思路去解决问题,而不会去想,哪一种方式是最有效的。我想,在第二种方法发明之前,人们同样也可以解决这一类的问题,只不过耗的时间精力较多。也许我们当中的某些人有着与常人不一样的思考方式,比如牛顿爱因斯坦。但并不是每一个人都有成为牛顿和爱因斯坦的能力,所以我们中的大多数人,可能永远无法找出解决问题的最优策略,所以我们只能通过前人的经验,去学习,去了解,学会应用。对于我这样普普通通,没有天分的人来说。这,也许才是我应该去做的事。

说到底,计算思维不过就是一种工具。某些人创造这个工具,我们只要使用它来,帮助我们解决问题就够了。在我看来,我们没有时间,也没有必要整日沉浸在计算思维中,因为当你的眼里只剩下计算,这世界里一切在你眼中不过是一大堆数据而已。但请不要忘记,人之所以为人,除了智慧,更重要的是情感。如果世间的一切只剩下计算数据,那么所有的一切都将会是冰冷无情,机械呆滞的。那么人也就不能称之为人而是机器了。这也就是那些自然科学的顶尖人物或多或少都有些刻板古怪的原因。就如同很多科幻电影中描绘的场景那样,被机器,统治的世界注定是单调乏味的,甚至可以说是一场灾难。所以,计算思维固然重要, 但它仅仅是一个工具,计算思维并不能代替思维的全部。在计算机的世界中,没有丰富多彩的变化,也没有千奇百怪的想象,有的只是唯一确定的代码,通过这些代码的组合构造出一个又一个模块,仅此而已。

如果说,计算思维是人类理性思维的代表,那么,人类又怎样拓宽自己的情感世界呢?这就要说到另一个话题,也就是文学思维,某种程度上来说,可以称之为文化思维。马克思曾说过,人是一切社会关系的总和。而文学,则从侧面反应了一个时代,一个社会特有的文化。文学的创作并不是凭空而谈的,它源于生活,又高于生活。源于生活,自然脱离不了社会。高于生活,自然脱离不了人类丰富的想象。它并不像计算那样唯一,而是千变万化。更重要的是。文学思维可能是随着个人的经历的不同而不同。比如说,同样一件事,不同的人站在不同的角度来写,可能会产生,不一样的态度。如红楼梦中两百多个人物,各自有各自不同的性格,作者在写作的时候,不可能将自己的好恶倾注于其中,而是要用心的带入每一个角色,只有这样的人物才能够鲜活,也就是说,作者必须把自己代入到这些人物行为处事的思维中,这没有一定的经历和倾入其中的感情是办不到的,这也就造成了文学思维的多变。尽管行文有一定的章法,有一定的规则。但文学更多反应的是作者个人的,态度体验以及所处时代的精神。正所谓,一时代有一时代之文学,这和计算思维有大大的不同。自古以来一就是一二就是二,无论在哪个朝代都是如此。但文学的思维,却有可能因为时代的变化而变化,因此对于文学的解读,也有可能因时代的不同而不同。从文学作品中,我们可以看见人性的复杂,也可以看见时代的潮流。而这些,都不是计算思维所能具备的。也正因为如此,人才更有人情味儿,才更有一份属于自己的感情。

但是,如果只有文学思维,没有计算思维,人就有可能一辈子活在想象当中,就像乌托邦一样,脱离实际。这时就需要计算思维,来加以辅助,计算思维让人回归理性,让这些原本存在于想象中的东西得以付诸现实。可以说如果没有计算思维,那么一切都是空谈。也正因为如此,在当今社会,计算思维和文学思维都是不可或缺的,他们共同组成了多彩缤纷的世界

综合来看,计算思维和文学思维是人类智慧的两大高峰。文学思维是感性的,计算思维是理性的。文理结合,贮存人类多姿多彩的别样文明,也造就了人类,至高无上的统治地位。我们应该感谢这个世界,正是从五彩缤纷的大自然中,我们获取了这两种思维,从而反过来,改造自然征服自然,成就了属于我们自己的一份伟大奇迹!

算法之问

我的第二季连载作品开始了。第二季的作品,是和算法训练有关的一个桌面 APP:刷题、刷算法题、刷数学题,看起来就是面对专业学生、专业老师才能做的事。但这一次,我用 AI 创造了一个虚拟的教练。那么在揭开这位教练的面纱之前,我们先来说一说我和算法的故事。虽然不是专业的计科出身,但是我与算法的情缘,正是源自于高中的那一堂数学课------这也成了我今天要开发这个 AI 助手的开端。

故事要从一道题讲起。2014 年,高中的数学课上,我第一次遇见它。那一年离现在,已经十二年了。

那节课讲的是牛顿二分法,求方程的近似解。老师引入这个方法,用的例子是找假币:一堆金币里混着一枚假的,比真币略轻,手里有一架没有砝码的天平,问怎样用最少的步骤把它找出来。老师给出的答案干净利落:把金币对半分,放上天平,假币一定在轻的那一堆,对那一堆继续对半分。结论是,二分法最优。

我把这个结论原样记住了。这一记,就是十二年。

再次见到这道题,是大二的冬天。一名南京航空航天大学的教授来学校讲计算思维。要说起来我对于算法的初次接触,其实也是从那时才算真正开始------开场没有定义,没有术语,教授直接抛出了同一道找假币的题。

全场给的答案和高中那节课一模一样:对半分,称,轻的继续。我也脱口而出:二分算法最简单。那个教授什么也没说,反过来抛了一个概念,叫解空间。我当时懵了,什么叫解空间?那时我连数据结构都没学过,只知道我们可以用算法解决问题。教授说,最优的解法是三分,公式是 log₃N 向上取整。

我不知道他是怎么推出来的。这个困惑我背了九年,现在先把答案补在这里。

天平每一次称量,能返回三种结果:左边轻,右边轻,或者平衡。而"假币是哪一枚"这个问题,有多少枚金币,就有多少种可能。每一次称量,都把这堆可能切成三份:假币在左盘,在右盘,或者在没上秤的那些里。称 k 次,全部可能的结果组合只有 3 的 k 次方种,每种组合至多指认一枚假币,所以 k 次称量至多分辨 3ᵏ 枚金币。要分辨一百枚,3⁴ 是八十一,不够;3⁵ 是二百四十三,够了------五次,这就是 log₃100 向上取整的来历。三分法恰好贴着这条线走,所以它称得上最优。最优两个字在这里有严格的含义:不是暂时没想到更好的办法,是更好的办法被证明不存在。

那么,高中老师错了吗?没有。他那道题里藏着一条从来没有人念出来的规则:所有金币必须上秤。全部上秤,假币必在其中一堆,天平必然倾斜,"平衡"这种结果根本不会出现------仪器只剩下两种回答,二分就是这两种回答下的最优。教授那一问的真正力量,不在于换了一个算法,在于废掉了那条没人宣布过的规则:允许留一堆金币不上秤,回答就从两个变成三个,答案从七次变成五次。

同一个故事,两个结论,都成立。真理没有打架------最优从来不是题目自带的属性,它是题目和规则共有的属性。规则变了,答案就变,这不叫矛盾,这叫看清了题目。而看清规则,恰恰是那堂课教给我的、比三分法重要得多的一课。

讲完这道题,教授继续讲计算思维。而院长把我拉到了台上:当场写一段 C 语言代码,把假币找出来。

我就是那样写出人生第一段算法程序的。代码我留到了今天:

c 复制代码
int found(int n)
{
static int i;
if(n==1)
printf("this is a false coin,\n",n);
else if(n==2)
	{
printf("the lighter one is the false coin\n");
	}
else if(n>=3)
	{
		n/=3;
found(n);
	}
	i++;
return i; 
}

用今天的眼光评审,这段代码的运气好得惊人。递归的骨架是对的:n 除以三,缩小问题,直到落进基线条件。但计数器 i 写在了递归调用之后,每一层函数都会执行一次------它数的不是称了多少次,而是函数走了多少层,连最后那声"找到了"也被登记成一次称量。输入三,正确答案是一次,程序报两次;输入九,报三次;凡是三的整数次幂,一律多报一次。另外,n/=3 是向下取整,一百枚金币分三堆,必然是三十三、三十三、三十四,最坏的情况落在三十四那堆,程序把这一情况悄悄低估了。奇妙的是,这两个错误在一百枚这个输入上互相抵消:函数走了五层,程序输出五次,恰好就是正确答案。两个错误互相抵消,产物是一个看起来完全正确的结果。这个模式,我在后来的开发生涯里见过许多次,每一次都想起这段代码。另外,代码的格式零零散散也是稚嫩初学的手笔,那或许是对于初学者一个空格一个空格敲出来的时光的怀念吧。所以我把这份凌乱的代码原样的堆在了这里。

但交代码的那一刻,我没有任何评审。那时候的状态,我后来对自己交代过:教授给的条件就是一百,它是个固定值。我只是说,你给我一个数字,我解特定语法的题,就好比给我一道奥数题,奥数题里的数字是固定的,它只有唯一正解。大三的算法分析课我还没上,根本不知道算法是解决问题的策略,我只是把它当做数学题来解。我也不会测试------不会测试,就永远发现不了自己的题和出题人的题,根本不是同一道题。

这里的分界值得多说一句。程序错误的标准定义,是与规格不符。而我心里没有规格:我以为题目是"一百枚金币要称几次",程序对这道题分毫不差。它错,是错在另一道我从未接到的题目上------"对任意的 n,最少称几次"。所以这段代码严格说不该叫有 bug 的程序,它该叫一块化石:它精确记录了一个"算法等于策略"的观念尚未安装的大脑,如何用奥数的方式认真地解一道编程题。答案思维和策略思维之间隔着一条线,我花了两年才跨过去------大三那节算法分析课,把线那头的一句话讲给了我:算法是解决问题的策略。

而在那节分析课到来之前,还有一段插曲。讲座结束之后,我发疯了,迷上了算法,动手去敲一道叫幻方的题:把 1 到 9 填进九宫格,让每一行、每一列、每一条对角线的和都相等。这道题我敲了三天,敲出来一段彻头彻尾不合工程规范的程序------从头到尾只有一个 main 函数,一百多行;四种阶数对应四种互不相同的构造方法,我一种一种地自学,一种一种地往里堆;程序写完,我不知道它对不对,又写了一段验证代码,把每一行、每一列、每一条对角线的和全部打印出来,这段验证在程序里重复了四遍。以工程的标准看,它烂得没有争议。可它也是我人生第一个自己造的验证器:在那之前,我无从判断一段程序的对错;从那以后我知道,对错可以让机器自己报出来。九年后,我的产品里最要紧的部件叫判定器,出的每一道题都先让程序实跑自校验------那个部件,就是幻方这三天里一点一点,东拼西凑出来的。

比这段代码更值得记下的,是这三天里没有发生的事:没有人指正我。C 语言的课只教到参数怎么填,教材上的示例和我写得一个水平,身边没有一个懂行的人。我用三天热情换来的这百余行代码,就这样原样放着,没有一个人告诉它烂在哪里。这个遗憾在当时看不见,它是这本手记后面所有教育话题的起点。

大二那一年,我和这门学科正式结盟。我在 QQ 空间给算法写了一封情书,隔了两天又写了一篇《数据结构------抽象的艺术美》,把敲了三天的幻方和九种排序算法全部写了一遍。也是在大二下学期,我学了字符串匹配的 KMP 算法,把三位发明者的名字音译成克鲁斯、莫瑞斯、帕罗特------我不知道第一个字母 K 代表的是高德纳,就是后来写下《计算机程序设计艺术》的那位。我看不懂这样的名字有什么含义,只不过是三个人名而已,不方便理解,自作主张改叫它首尾对称滑动算法。当毕业十多年后,我看见高中的数学老师在讲二分法的时候,引入的例子还是这道找假币,结论还是二分法最优。这一次我绷不住了。

现在我可以平心静气地拆这桩笑谈。老师没有错,在他的规则里他是对的;错的是那条规则本身没有被说出来。那道题在我这里已经换了三次身份:高中它是二分法的例题,大二它是一段有缺陷的 C 代码,大三它是一封情书里的比喻。一道题,十二年。每一次重新遇见,它都比上一次多讲了一层。而这个困惑的最终谜底,要等到十二年后,由一次提问来揭晓------这是后话。

第四种身份出现在 2025 年 11 月。我三十岁,在课上教学生,讲的就是这道找假币。这一次我写了第二和第三版,Java。

java 复制代码
package com.njts.array;
import java.util.Random;
import java.util.Scanner;
/**
* @author David GE
* Date 2025/11/13
天平找假币 计算思维
给定 n 枚外观相同的硬币,其中恰有 1 枚是假币且更轻(可用随机数生成)。你可以使用**无砝码的天平**进行称重:一次称重可将若干枚硬币分别放在天平左右两盘,天平会返回三种结果之一:
- 左重(左盘更重)
- 右重(右盘更重)
- 相等
- 解空间(状态)
- [log3N]
请设计一个算法,**最少数量级**地确定哪一枚是假币,并输出其下标。
输入
- 第一行:整数 n
输出
- 一行:`index=<假币下标> weighs=<称重次数>`
**/
public class CoinWeight {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 用数组模拟硬币重量,硬币编号就是数组的下标
        int n = scanner.nextInt();
        // 假设 真 重量 1 假币 重量为0
        int[] coins = new int[n];
        for (int i = 0; i < n; i++) {
            coins[i] = 1;
        }
        // 用随机数生成假币的位置
        Random random = new Random();
        int index = random.nextInt(n);
        coins[index] = 0;
        // 解题,假设index未知,找出index
        // 重量和硬币数 并不相等 区间长度不一定能被正好等分,某个区间会多一个硬币
        // 10 0-2 3-6 7-10
        // 即使在等分的情况下,重量也有可能比区间长度少
        // 不能简单对整个区间切分
        // 只有两枚硬币->忽略 n=n/3
        // 正解,动态记录区间端点,
        int targetIndex = 0;
        // [0,n/3) [n/3,2n/3),[2n/3,3n/3)
        int w1 = 0;
        int w2 = 0;
        int endIndex = n;//假硬币所在子区间的右端点 n 2 endIndex=2n/3 endindex=endindex*2/3
        // n==1时,当前下标就是假币所在
        if (endIndex - targetIndex == 1) {
            System.out.println(index); //对数器保留:打印真实假币下标
            System.out.printf("index=%d weighs=%d%n", targetIndex, 0);
            return;
        }
        int weights = 0;
        while (endIndex - targetIndex > 1) {
            int len = endIndex - targetIndex;
            //考虑只有两枚的情况
            if (len == 2) {
                weights++;
                if (coins[targetIndex] < coins[endIndex - 1]) {
                    System.out.println(index); //对数器保留
                    System.out.printf("index=%d weighs=%d%n", targetIndex, weights);
                } else {
                    System.out.println(index); //对数器保留
                    System.out.printf("index=%d weighs=%d%n", endIndex - 1, weights);
                }
                return;
            }
            // 计算每个区间的重量(分区)
            //重量不能和长度混淆(想当然)
            w1 = 0;
            w2 = 0;
            int sizeA = len / 3;
            int sizeB = len / 3;
            int aL = targetIndex;//[0,ar)
            int aR = targetIndex + sizeA;
            int bL = aR;//[ar,br)
            int bR = aR + sizeB;
            int cL = bR;
            int cR = endIndex;
            for (int i = aL; i < aR; i++) {
                w1 = w1 + coins[i];
            }
            for (int i = bL; i < bR; i++) {
                w2 = w2 + coins[i];
            }
            weights++;
            // 比较天平两端个重量大小
            if (w1 == w2) {
                targetIndex = cL;
                endIndex = cR;
            } else if (w1 < w2) {
                targetIndex = aL;
                endIndex = aR;
            } else if (w1 > w2) {
                targetIndex = bL;
                endIndex = bR;
            }
        }
        System.out.println(index); //对数器
        System.out.printf("index=%d weighs=%d%n", targetIndex, weights);
    }
}

Java版和 C 版的区别不在语言,在世界。C 版的程序里只有我和一个数字;Java 版里我造了一个完整的世界:数组是硬币,随机数是命运,求和是天平。程序里住着两个角色,一个是知道答案的上帝,一个是不许偷看的解题者,注释里写着他们之间的合同:假设 index 未知,找出 index。解题部分严格守约;找到之后还有一步------把解题者给出的下标,和随机生成的真实下标并排打印,当场对答案。这是经典的验证方法:已知答案与程序输出比对,正误立见,也是课堂演示的需要,学生要亲眼看见每一步的结果,才算真的看懂。要说瑕疵,是验证的输出和业务的输出混在同一段代码里,不知情的评审者会把它误会成作弊;测试与业务应当分家,这是它留给我的下一条教训。注释里还保留着我的第二版错误思路:重量和硬币数并不相等,区间长度不一定能被正好等分,只剩两枚时 n 除以三等于零。这三行注释是一条私人的历史翻车记录,每一行背后都是一次真实的卡壳。另外还有一笔账:余数没有摊匀,某些枚数下会多称一次,我只能在注释里把规格从"最少"降级成"最少数量级"。写的时候没有察觉,写完才认出来------这个动作我做过:九年前的 C 版,把"任意的 n"悄悄换成了"n 等于一百"。人解决旧问题的方式,常常是换一种新语言重犯旧错误,区别只在规模。

你知道方法的思想,和你能把这种思想用语言转换出来,根本不是一回事。这句话是我给学生讲课时的原话,它也解释了为什么同一道题我要写三遍:写一遍,是把思想翻译成一种语言;写第二遍是为了矫正自己对不对,换一种语言能不能复现出来,再写一遍,是让当年的自己和今天的自己比一比,看看到底进步了没有。

这份随堂代码的来历,还要从我的求职经历说起。

我是一个坐在轮椅上的程序员,也不是计算机科班出身。我读的专业叫教育技术学,师范课程和几门计算机基础课混在同一张培养方案里。选它的时候想法很简单:家里人告诉我,坐在轮椅上的我,出路无非两类,一类是文案相关的工作,一类是能远程完成的计算机工作;而我一直想当老师,两者结合,看起来正好。入学之后才知道,师范那条路其实早就堵死了------体检过不了,教师资格证拿不到,黑板板书更无从谈起。入学时还有老师当面向我表达过质疑:手指不灵活,打字恐怕都成问题。

打字慢是真的。但这门课教会我的第一件事,恰恰是打字不重要。后来我才明白。我们是程序员,又不是打字员;决定开发速度的,是调试和解决问题的那一段,不是敲键盘的那一段。这句话我记了很多年,它的好处在后之后被发现------AI 时代人人都慌机器写得比自己快,而我从入行第一天就知道,这一行考的从来不是手上的速度。

大四的寒假,疫情来了。校招没了,考研也失败,家里人笑着打趣劝我去社区残联找份文员的差事。我不服气:读了四年大学,不至于拿个本科文凭去当门卫。毕业前夕,我拨通了大三时给我们上 Python 课的一位企业项目经理的电话,进了他公司的培训班,开始一年的转码。结业后我投了一个星期简历,五百多份,大多数已读不回------自我介绍里写明身体状况的那部分,电话聊到一半就被人礼貌地挂断。后来我才明白,开发这一行并不都坐在电脑前,要跑客户,要出差,要现场调试。

收下我的是我的职业规划导师。他成了我的老板,也成了我的伯乐。试用期的第一课就够狠:我交出第一版数据库设计稿,带我的师傅问我,你是不是不是计科毕业的,连三范式这种最基本的要求都不守。他不厌其烦地替我改了三版,我拿着修改稿,用一个通宵把三范式的概念啃了下来。那半个月天天熬到凌晨,项目演示那天,我的两条腿坐在那里也在抖。可那一夜我也第一次体会到,被人指着错误不厌其烦地纠正,是一种什么滋味------它贵,但它值。我后来在产品里坚持让判定器当场指出学生的每一个错误,源头就是那三版设计稿。

在公司的两年里,Java、前端、数据库、Android、大数据,我一样一样地补。因为公司的业务是和院校合作培养人才,我还以企业工程师的身份被派回院校授课。当年那条被堵死的师范路,就这样以另一种方式走通了:我站上了讲台,教的恰好是编程。如今我自己也带实习生,看他们捧着代码跑来问我的时候,我总会想起九年前的自己------那种眼神里闪烁的光芒,是求知的渴望。师傅带徒弟,一代传一代。

前几天,我和 AI 共同做了一个软件,叫算法教练陪练。做它的初衷,是想让学生在苏格拉底追问式的教学下,获得更大的兴趣和动机。这个初衷在现实面前遇到的第一个人,是一位在软件公司做了两年开发的实习生。我问他要不要来测试产品,他回答:我不了,我对算法已经没有动力了,找学生玩吧。

没有动力这个短语,我记到了今天。没兴趣的人会说没意思,说用不上;没有动力是受过伤的人才用的词,而且伤得筋疲力尽,也许是被公司里面的杂活压得喘不过气,也许真的只是为了那一份养家糊口的钱,但是我觉得所有人学算法的目的,其实最初不过是为了那一份小小的欣喜,他没经历过欣喜,他知道该学,试过学,然后在某个环节被击败了------被天书的教材,被没人指正的练习,被"别人都会只有我不懂"的窒息感击败。而我在自己的数据库公开课上见过另一群人:我用诘问法引导学生提问,叫起来的常常是木头人。学生眼中没有问题,你让他怎么抛出问题?后来一位同事把话说透了:你设计得再精彩,讲得再透彻,不听的还是不会。

那这门课还有救吗?有的。我自己带实训的那段日子给出过证据:陪各小组在实训室把项目做到凌晨,那一期全员参与,没有一个木头人。原因不复杂------项目做完,他们才能找到工作。思路和困惑都接通解开了,学生自然就会提出问题。苏格拉底之所以成为苏格拉底,除了他自身的智慧,还因为他有柏拉图这样认真思考的学生;师生互相成就,缺一半都不成立。追问法没有错,错的是指望它对眼里没有问题的人生效。先让学生在意,再谈追问------顺序反了,课堂就只剩表演。

写下这些的时候,我把那位实习生的毫无动力的成因又想了一遍,想出一个之前没想透的判断:失去动力,不是感觉不到难------恰恰是因为感到越来越难,但是看不见自己变强。学习本该有两把刻度,一把量难度,一把量成长。正常的学习里两把刻度一起走:题还是难,但你昨天不会的今天会了。可在一个教材烂、没人指正、只能照着抄的环境里,成长的刻度先断了:你磨了一个月,难题依旧难,你却拿不到任何自己变强的凭证。于是只剩一把刻度在走,走的全是难度。只有难度刻度、没有成长刻度的努力,就是苦行。竞赛和刷题被体验成苦行,不是学生娇气,是他的世界里只剩难度在走。这个环还会自我加固:看不见进步,人就怀疑自己;怀疑自己,就更不敢往下走;越不敢走,越看不见进步。失去动力既是这个环的起点,也是它的结果,而环的第一圈,是环境推着转的。

我自己为什么没有掉进这个环里,今天也想明白了:不是我的意志比谁强,是我碰巧给自己造过用来校对的仪表。幻方写完不知道对不对,我就让程序把每一行、每一列、每一条对角线的和都打印出来------对错当场可见;那几天里我每改对一处,打印的数字就变一变,我的成长是被自己亲眼看着的。海隆那位实习生没有这样的仪表,他只有难度,没有刻度,于是剩下的全是苦行。这也是我的软件要做的第一件事:不做勇气教育,勇气教育是空话;做仪器------判定器让对错当场可见,动画让过程可见,错题本让积累可见。把进步做成看得见的刻度,那种由毫无动力产生的疲倦与消沉感,自然会烟消云散。

这里还有一层意思,借两百多年前的一本书说。有人问康德什么是启蒙,他的回答里点了两样东西做启蒙的敌人:懒惰,和怯懦------大多数人甘于被别人引导,不是因为缺乏理智,是因为缺乏运用自己理智的勇气。算法的修炼,恰好是一场针对怯懦的启蒙练习:题库可以让别人出,答案可以让 AI 给,唯独"我自己完整推导了一遍"这件事没法外包,而启蒙恰恰只发生在这件没法外包的事里。一个亲手解过假币、幻方、背包问题的人,和一个只会向 AI 要答案的人,拿到同一段代码时看到的不是同一个世界------前者看见的是步骤、代价和可能的裂缝,后者看见的只是一段能运行的文字。人道主义不歌颂苦难,所以它不会要求人们为了修炼去吃苦;它要求的只有一件事:别让自己的理解力长期寄存在别人手里。算法修炼之所以有意义,就是因为它每一次都在行使这项权利。

把这些经历全部讲出来,我的算法之问就完整了:AI 已经遍地开花,我们为什么还要开发一个训练人手写算法的软件?

这道题也有三种寿命的长度的答案。回答它之前,得先选一把尺子:答案好不好,取决于这段代码打算活多久。寿命五小时的代码------竞赛的题解、考场的程序------AI 已经写得又快又好;寿命十年的代码------套规范、进团队、被维护------AI 也写得越来越像样。跟它比这两样,人没有胜算,也不必去比。但代码还有第三种寿命,也是我最为崇拜的一种:以高德纳为代表的经典算法------我私下把它称作高门。高德纳写《计算机程序设计艺术》,从 1962 年写到今天没有写完,他甚至用一台虚构的机器来写书里的程序,因为他要的不是在任何一台真机上运行,是经得起任何人、任何时代的检验。这种要上五十年、一百年的代码,每一行都要配得上"最优"两个字,而最优需要证明,证明需要在头脑里完整地走完全程,走完全程的责任没有外放给任何人------这是高德纳用六十年作答的一道题。

所以手写算法练的从来不是打字的速度,是三种更慢的能力:把现象倒推成思路的预演能力,对任何一份送上门的答案保持核验的能力,以及在动手之前先看清规则与解空间的判断能力。AI 写得尽代码,写不尽责任------代码出事的时候,责任链的末端必须站着一个承担后果的人,而 AI 对错误的全部反应,只是下一次采样。代码的尽头是人。这句话不是在比较谁更强,它是在指出:这条链可以无限长,但每一环都必须能追到一个人。

写这篇稿子之前,我做了一件事:把那个背了十二年的困惑,原原本本抛给了 AI------为什么高中老师说找假币二分最优,大学教授却说三分最优,到底谁对?

它给出的答案,把谜底揭开了,也真的解答了我的疑惑。这道题的教辅原版,题干里其实印着一行关键限定:应用二分法的思想完成本题。也就是说,它从来就不是在问现实天平问题的最优策略,它是一道范式训练题------借天平和金币的外壳,练习"对半分割、依据反馈舍弃一半"的迭代。在这条限定之下,每次把金币对半、全部上秤,只使用左轻与右轻两种回答、放弃平衡态,二分就是该范式下的正确解法;但换个问法------允许把一部分金币搁置不上秤,天平就有三种回答,最优解变成三分法,次数是 log₃N 向上取整。同一道故事题,在两种题型下各有各的正确答案。误导有两层来源:第一层在题自身,它用现实故事做外壳,任务却是训练指定范式,读者看见天平和金币,本能地按"求最少称量次数"去理解,那行工具限定被自然而然地略过;第二层在传播,大量的课件和口头授课把"应用二分法的思想"这行限定删掉了,只留故事题干,参考答案却依旧输出二分------限定彻底隐身,次优解被当作最优,教了一届又一届。我高中听到的那一版,正是被删过限定的版本。

所以,老师没有推导错,他复述的本来就是一道被藏了条件的题;我也没错,我按题面本能地把它理解成了现实最优。真正出问题的是题目类型和传播链:一道范式训练题,冒充了十二年的现实最优题。揭晓的那一刻,我盯着屏幕坐了一会儿。人的认知和 AI 的标准,在这一刻发现了缝隙------缝隙那边,是教辅、课件和口口相传堆了十二年的标准说法;缝隙这边,是一个高中生背了十二年、从二分到三分始终没对上的困惑。AI 的标准早就摆在那里,可标准不会帮你解答困惑,把它们摆在一起接上的那个动作,是我做的:我得先记得这道题,先不信那句"二分最优",先在这么多年里把三分、解空间、称量的次数问了一遍,才谈得上把困惑原原本本问出来。这就是人和 AI 的距离,理解的距离:标准它都有,困惑只有我背着,而理解,发生在多年认知圆上的那一秒。

至于依赖。学手写算法不等于拒绝 AI,就像学算术从来不是为了拒绝计算器。恰恰相反:自己完整地推导过一遍,才有能力判断 AI 给出的答案对不对;没有亲手走完全程的人,连 AI 犯错都看不出来。手写算法是保持判断力的唯一途径,它训练的那个东西,恰好是依赖性最先夺走的那个东西。

回到我的软件。它要做的,不是把当年的苦难复述一遍,是把当年没有人给我补的课补上:一道题有人把解空间讲透,一次错误有人当场接住,一个疑问有人陪你追到底。让那个失去动力的实习生愿意再试一次,让眼里的木头人开始提出自己的问题,让每一个刚入门的人知道,你觉得枯燥的那门课,其实通向一种别的课程给不了的严谨与美。

至于我自己,十二年前把高德纳的名字弄错了一个字母,今天在读他的《计算机程序设计艺术》,把他书里的题收进了自己的题库。假币这道题,从我高中的数学课算起,我解了十二年,还没有解完:它做过二分法的例题,做过有缺陷的 C 代码,做过情书里的比喻,做过讲台上的随堂代码,今年又在一次与 AI 的对谈里交出了自己最后的秘密。它是这个 APP 的名字,也是这本手记的开篇。下一次它再出现的时候是什么样子,我不知道。我只知道每解一遍,都站上了更高的解空间。

算法之问没有终点答案,它只有一个又一个的阶段答案,而写下每一个阶段答案的人,都必须先亲手解完自己的那道题。

这,就是我的第一种回答。


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

相关推荐
LG.YDX1 小时前
基于中心的3D目标检测和探索追踪-笔记
笔记·算法·目标检测
民乐团扒谱机1 小时前
【读论文】音色在微分音程记忆中的作用
人工智能·深度学习·算法
Navigator_Z1 小时前
LeetCode //C - 1306. Jump Game III
c语言·算法·leetcode
小O的算法实验室1 小时前
IEEE TASE,面向无人机的不确定性感知信息路径规划:可解释贝叶斯强化学习方法
算法
L0208241 小时前
Kubernetes Service 与 kube‑proxy 深度详解
运维·算法·云原生·容器·贪心算法·kubernetes
傅科摆 _ py1 小时前
回溯力扣复习
算法·leetcode·职场和发展
wdfk_prog1 小时前
LWIP教程 40:从 `socket()` 到 `lwip_socket()`——DFS fd、SAL Socket 与 lwIP Backend
算法·深度优先
中伟视界2 小时前
边缘端行为识别部署实战:AI算法分析盒子落地皮带场景
人工智能·算法