知乎热榜 ( ) • 2024-05-14 17:40
锦恢的回答

首先赞赏一下提问者 @Vic亮 的提问精神,我认为每一个学习计算机技术与算法的小兔崽子都应该问一下这个问题,没有问出这个问题却心安理得地进行学习的人和附庸风雅之辈恐怕没有差别。

这位晚辈,你在看我的回答之前需要做好心理准备,因为你提出的这个问题涉及到了计算机这个学科几十年来的核心与痼疾。

阁下提出的这个问题我曾经也愤怒地质询过很多人,可惜无人给出令我满意的答案,但是如今,我想,我能回答若干年前的自己抛出的问题了。所以虽然只是一次简单的谢邀,但是权当对穿越时空尚且余热的少年愤怒的回应了。

那么,请允许我抛出一家之言。

我最近找到了一个不错的 citypop collection,推荐和本篇文章一起食用:【Playlist/Lofi/蔚蓝档案】回到家也只是玩手机而已 | 纯音乐歌单-锦恢-我的音乐-哔哩哔哩视频


【很重要】什么是算法

这个问题非常重要!!!因为短短的10年内,计算机领域的“算法”二字的定义发生了很大的变化。

2019年之前

如果是2019年之前,算法(algorithm)指的是诸如,排序、动态规划、倍增、指纹等等这样的算法,基本上你都能在下面这本书或者长得像这本书的书上找到:

为了方便阐述,我将这种算法定义为“传统算法”

如果你身边有朋友从小学开始打计算机竞赛的,那么这个小混蛋会的所谓的“算法”,指的就是上面提到的传统算法。

而2019年之前大厂招聘的“算法工程师”,指的就是能够熟练使用上面这些算法进行编程的人。那么你可能想问,大厂招这些人干啥呢?我平时在开发软件的过程中根本用不到这些算法呀!

好问题,因为教你技术的老师有两件很重要的事情没有教你,不是因为他能力不足,而是因为这些老师与业界脱轨太久了:

  • 第一件事:这些算法脱离抽象语境后在哪里被使用。
  • 第二件事:目前互联网界的发展进度。

2019年,这个节点上,互联网大厂的格局基本已经形成了,各自的大厦(打底的业务和对应的软件平台)都已经搭建完成,但是5G并没有如预期般普及给行业带来新的技术红利,非视觉类AI算法也难以落地形成创收,剩下来要做的事情就是存量竞争。存量竞争时代,人们就开始对已有的软件、网络等等进行优化,如何优化?大抵就是使用这些算法进行优化。比如原本上有 N 个人一起访问一个网站上的视频,但是彼此的网络状态啥的都是未知或者半未知状态,为了更好的用户体验,我们就可以给这个问题进行建模,设置效用函数,然后使用类型于动态规划的问题进行优化。这个过程其实和数学建模很像,数据建模的C题一般也喜欢搞一些计算机算法,因此,早年间不少大厂都喜欢招有数学建模背景的同学入职算法岗,比如阿里巴巴数不清的子业务部附属的算法部门。

当然,上面这些话只适用于流量主导的简单业务软件,比如80%+的互联网平台。它并不适用于部分游戏和B端软件的生态,这两个玩意儿我会在后面讲。

2019年之后

2019年后,算法指的更多是和深度学习,人工智能相关的软件开发。如果你有一个在大厂上班的朋友,他说他是搞算法的,那么大概率他是做 AI 相关的开发。

大部分的深度学习开发都是用不到传统算法的,这个行业目前鱼龙混杂,大部分从业者做的事情就是调包,调参,炼丹,其中关于搭建模型的过程更像是搭积木,基本上用不到传统算法。更多的是靠经验和一点点的数学基础。当然大部分的从业者的那一丁点微不足道的数学基础是根本帮不上忙的。

有一些涉及到规则匹配或者强先验的 trick 会用上类似于匈牙利算法这样的东西。

当然,如果你是做深度学习框架、编译器、分布式训练框架等开发的,那就是另外一回事了。但是国内的AI生态留给这些AI基础软件的利润空间并不多。

这三个方向鄙人都有所涉猎,倘若阁下感兴趣,欢迎私信我。


从ACM竞赛到高校教材

好啦好啦,我们来说点实际的吧。为什么作为计算机专业的学生,你的培养方案上会出现算法设计,以及为什么这些算法设计的题目会如此脱离实际。

在互联网早期,大概30年前的样子,那个时候有大量的基础软件的开发需求,GNU还为大部分互联网从业者熟知。而基础软件的开发过程中往往会涉及到一些“看起来不太好解决的问题”,比如服务器的流量分配、芯片设计EDA的自动布线算法,这些问题的数学模型都是 NP 难,作为解决NP难问题的妥协,我们往往会设计各种解决方法,比如一些强先验的动态规划,甚至有一些早期的基于概率学模型的优化解法。

总之就是,这些早期问题很难得到一个完美的解决方案,然后彼时刚好计算机教育在西方普及,一些公司就想着不如把这些问题分摊给在家里玩着世嘉游戏机的小屁孩解决,他们从小接受过编程教育(特斯拉的老总 Mask 和 openai的首席执行官 Altman 都是在 10岁之前开始进行编程),而且很多优化的过程并不需要很高深的数学知识。只要对这些工程问题进行适当包装,去除会让这些小屁孩看着害怕的复杂工程学名词和数学用语,我们就可以把这些工程问题抽象成一一个个简单的小游戏,就像《三体》里面云天明把光速飞船计划的抽象成了小故事一样。

然后我们把这些问题打包成一个XXX竞赛,ACM应运而生。

只要这样,这些聪慧的孩子们或许能提供一些新的思路,他们能拿到奖金,公司可以节省人月,可以说是双赢。

随着时光的推移,这些高度抽象的题目被传承了下来,ACM这个赛制也被传承了下来,但是这些题目是如何被抽象成小孩子都能读懂的题目,比如 Vic亮 你提到的:

什么小A小B玩游戏,小C给朋友买糖果

这些题目背后的问题原型和被埋藏的技术背景是什么?随着时光的推移,越来越多的人慢慢地忘却了,曾经的工程师们还未将过去的真相对这些聪慧的孩子们悉数告之,就和这些孩子手里的世嘉游戏机一样,退出历史舞台了。只留下来了空有“标准答案”的ACM赛题。

任何行业的人员涌入都会伴随着“劣币驱逐良币”的社会学现象,在多重因素的加持下,现在的一些赛题和算法设计的课本题目从过去的“以工程问题为导向的出题模式”慢慢变成了“以理论讲解为导向的出题模式”。算法设计的题目不再服务于某个具体的工程问题,而服务于“排序”、“动态规划”这一个个抽象的算法。

或许有一些高校的老家伙们还知道这些题目的工程学背景,但是大部分应该都不太清楚那段历史,清楚的少部分也已经没有心力把这些工程问题是如何被抽象的,抽象后的问题是如何演化为我们看到的算法的,这些算法如何建模设计开发的。因为这些很花时间,老师们也要休息的,而且其中涉及到了大量的工程学的背景,比如计算机网络的底层架构呀,数字图像的信号处理呀,讲了大部分学生也不懂,还不如不讲。特别是现在部分地区那杰出的双减政策,培养的小孩子一个比一个笨。

以前的题目是小明小红,糖果分配。后面的题目可能就是小帅小美,英雄联盟。

来,给你看看这些算法背后的工程问题都是什么

不过,我知道,我的观众,个个不是聪慧就是富有耐心,否则你们也不可能会看到这里。而聪慧(具有足够的联想能力)和耐心(你的奖励函数的 gamma 值比较大)正是成为一名合格工程师必备的两个特性。

我看到你的在提出“什么小A小B玩游戏,小C给朋友买糖果”这个问题时的不解,纳闷,愤怒,很好!请保持你的愤怒。

我接下来就把一些简单的小朋友过家家游戏背后的工程学问题抽丝剥茧书写出来。不过受限于时间,一些例子已经从我大脑的cache中被剔除了,想起它们需要时间,所以我此处只会给出两三个例子。我相信你想要的不是答案,而是追求答案的阶梯。愿这两个例子能成为你追寻答案的阶梯,解铃还须系铃人。

例子1:二分查找

题目:

小K 的学校为了响应减负政策,决定给体育长跑设置定级给分。整个跑道一共设置 N 个时间点,记作 times,
每个同学跑完后的用时记作 cost。如果同学的 cost 刚好落在了第 i 个时间点上,那么就当前跑步的评级
就记作 i;如果同学的 cost 刚好落在第 i 和 第 i + 1 个时间点之间,那么跑步的评级也记作 i。请设计
一个体育的评分系统,根据体育老师设置的 times 和同学的耗时 cost 自动计算出评级。

输入:times=[0, 30, 40, 60], cost=10
输出:0

输入:times=[0, 30, 40, 60], cost=50
输出:2

怎么样,这个题目我出得还不错吧!贴合同学们的日常生活,同时没有暴露任何的工程背景。很显然,这是一个二分查找的问题。

这个问题模型在大部分的与时间移动相关的软件中都被用到。比如我之前开发的基于 webgl2 的 vcd 波形渲染器。当用户把对轴移动到下图的 39ns 的位置,然后按下鼠标左键后,我的渲染器要在第一时间把所有信号在 39ns 这个位置的值(16进制)传入左侧sidebar上并转换为10进制。

而 VCD 标准下传入的并不是每一个 ns 上的值,这样做太浪费内存了。VCD传入的是形如下面的二维数组,第一个位置是信号当前值的起点ns,第二位置是当前位置的值:

const wave = [
    [0, 0],
    [200, 0x10],
    [300, 0x20]
]

上面这个数组代表的含义是:

  • 0 到 200 ns,信号的值为 0
  • 200 ns 到 300 ns,信号的值为16进制的 10
  • 300 ns 到 末尾,信号的值为16进制的 20

因此,当用户的光标点击下,DOM配合参数化的层叠表和坐标系转换公式计算出了39ns这个数字后,我们发现 39ns 停留在 0 和 200 之间,因此,我们选择 0 这个位置的值,也就是 0。怎么样,这个问题是不是和刚才的体育跑步分级问题模型完全一样?但是里面涉及到了大量的数字芯片设计中的专业术语和名词,直接给学生做有可能让他们直接退学,从而影响我们的绩效评定。

我当时写的很简单的二分查找:

/**
 * @param {'bit' | 'vec'} kind
 * @param {Array<Array<string | number>>} wave 
 * @param {number} time 
 */
function findCurrentSignalValue(kind, wave, time) {
    const times = wave.map(p => p[0]);

    // 二分查找,并将结果存入 i
    let i = 0, j = wave.length - 1;
    while (i < j) {
        if (times[i] === time) {
            break;
        }
        if (times[j] <= time) {
            i = j;
            break;
        }
        if (j - i === 1) {
            break;
        }
        const mid = (i + j) >> 1;
        if (times[mid] > time) {
            j = mid;
        } else {
            i = mid;
        }
    }

    const value = getWireValueCaption(kind, wave[i][1], wave[i][2]);
    return value;
}


例子2:倍增扩容

题目:

从前有一片一望无边的大草原,草原上有着一栋恢弘壮丽的城堡。有一天城堡的主人在翻阅古籍时无意发现
了城堡的周围有宝藏,具体在哪里并不知道,但是只知道宝藏在城堡的南北方向上。城堡的主人决定派遣
小K 去搜寻宝物。请你为小K 设计一个搜寻算法。
注:小K 从城堡出发后,只能向南或者向北走。

接受过正规算法教育的科班朋友们一眼就能看出来,这个问题是在描述倍增算法。一个可行的解是:(记向南走是负数,向北走是正数)

  • 小K 走 N
  • 小K 走 - 2N
  • 小K 走 4N
  • 小K 走 -8N

也就是每个回合,小K走的步数的表达式为:

(-2)^{n-1} N (n\ge 1)\\

当然,你把上面的 2 换成 3 也是可以的。毕竟,3 距离自然对数 e 最接近嘛。(三进制计算机消耗的元器件最少)

其中 N 是一个你随便指定多少都可以的正数,它只与问题规模有关。

这个算法在实际工程问题中用得也很多。我举一个例子,大家都知道 python 吧?也都学过 C 语言和操作系统吧?

在 C 语言中,无论在栈上还是在堆上,一个数组的创建是需要事先指定数组长度的:

// 栈上创建
int stackArray[50];

// 堆上创建
int *heapArray = (int *)malloc(50 * sizeof(int));

问题是,我们无法事先知道用户到底要给这个数组里面塞多少东西进去,就好像小K一开始并不知道宝藏的确切距离。因此,我们创建动态数组往往只能在堆上操作。

一个比较朴素的思想就是一开始只申请1个单位的空间,每次需要往里面塞东西的时候,就再次申请一个单位的空间。这就好比小K每次只往一个方向走 N 的距离。系统申请内存是需要进行系统调用的,而系统调用往往很耗时,这么做会让效率非常低下。

一个通用的做法就是倍增扩容:一开始申请10个单位的空间,每次往里面加数据后,如果空间不足了,那么就重新创建一个长度为目前大小2倍的数组,然后把现有的数据搬到上面,再把原本的数组销毁。而小K 的策略和这个倍增算法如出一辙:每次走的距离都是上次距离的反方向的两倍。

事实上,这也是C++的std::vector的实现方法。当然有的观众不服气了,为啥一定只能是之前大小的两倍,三倍行不行?三倍再减一行不行?当然行,如果你的倍增策略是上次大小的三倍减一,那么这就是 python 的 list (准确说是 CPython 的 list 扩容实现,别的 runtime 的 Python解释器 的源代码我没时间看) 。


例子3:最短路问题

最短路问题我就不举例了,题目非常多,而且也有着 Dijkstra,A star 这样的成熟算法。

在导航软件,游戏,机器人中,这些算法用得非常多。但是!这些场景中,A star 算法都是不能直接使用,我们学到的写最短路算法,都是已经被抽象为完美数学模型的形态下,进行的“纸上谈兵”。遇到实际问题,最难的不是使用最短路算法,毕竟,都是现成的算法,也不需要你改进什么,最难的是把问题抽象为最短路问题的输入。打数学建模的同学应该会很擅长。

  • 导航软件依靠交通局提交的数据得最短路输入模型
  • 机器人导航通过SLAM算法获得最短路输入模型
  • 游戏中依靠什么呢?

作为从12岁就开始打星际2的老玩家,我有必要拿星际举例,讲解一下这些游戏中是如何应用最短路算法的。

在星际中,最短路算法用于做单位的寻路算法,啥是寻路算法呢?它指的是当我们用鼠标框选了一个小兵,然后点击地图上的某个地点,小兵会自动走向目的地,并且走看起来最短的路径。

为啥不能直接使用最短路算法呢?因为游戏中的每一个单位的坐标它不是离散的,而是连续的,这种情况下直接使用最短路算法,复杂度爆炸。所以一个最简单的做法就是使用有限元中的 Delaunay 三角剖分减少可行点的个数:

当然,如果是多个单位的集群移动,那个情况就更加复杂了,星际里面使用了一些仿生学算法进行多智能体协同。那就不在本篇文章的讨论中了。感兴趣的同学可以看这个视频,可以说是全网(包括国外)关于星际内算法最详细生动的视频了:

解密星际2寻路算法思路,风暴之门能超越吗?_哔哩哔哩bilibili_星际争霸2

总之,阁下对这些算法的效用建立起一个感性的认知便是极好的。


Q & A

最后,关于邀请我的这位少年可能感兴趣的几个话题,我简单回答一下。

深度学习里面会用到算法吗?

这个问题之前回答了一半,我把剩下的一半补充完整。

首先,如果你是做深度学习算法设计,去卷 NIPS,CVPR,AAAI,ICLR 这些顶会论文的,那么大概率用不到“传统算法”,大部分做AI的工作就是读论文,加模块,诚然,其中的一些 idea 你深挖下去存在很多传统算法的影子,但是就目前我读过的顶会论文而言,这些传统算法的影子似乎并不受这些天之骄子们的欢迎。

但是如果你做的是深度学习偏底层的开发,比如深度学习框架,里面会用到不少算法,比如图遍历,递归等等,感兴趣可以看看我之前写的关于原理级深度学习框架的教程。

锦恢:纯Python实现原理级深度学习框架(一)计算图的原理,节点类的实现和计算图的可视化

除此之外,如果你听过目前大模型的分布式训练框架,比如 DeepSpeed,CollossalAI,其中也用到了大量的动态规划,图算法。因为在分布式系统中,所有进程节点天然构成一张图,自然存在诸多新问题。然后 ChatGPT 本身的显存占用超过了任何一张商用级显卡大小,为了在多张卡上部署ChatGPT,需要对模型参数进行切分,这里面又涉及到CPU设计中的流水线并行算法。

如果正确切分网络,使得每一个 shader 在都可以让GPU利用率尽可能高是一个 NP难 问题,目前都是基于先验的动态规划做的,就和30年前一样。感兴趣的话,可以阅读 DeepSpeed,CollossalAI 中关于 PP 切分的处理。

什么类型的开发中会用上算法?

在大部分同学就职的互联网部分,特别是那种偏向业务部分的地方,不用说算法重不重要,甚至连技术本身都是不重要的,业务理解才是那群人的壁垒。

但是在一些偏底层的领域,还有少数的游戏开发,B端软件,其中涉及到的技术难点非常多,而且大多没有现成的库让你调用,都需要使用足量的算法开发工程师。此处会用到很多算法,但是B端软件的开发和大部分的程序员其实关系不大了,因为它的门槛很高。


暂时不写了,去打 galgame 了。