
当面对一长串乘法运算时,例如在矩阵代数中,我们通常想当然地认为运算的分组方式无关紧要。虽然结合律保证了最终结果相同,但它并未说明得到该结果的成本。最优括号化难题所要解决的,正是寻找最高效的分组序列。这是一个可能性数量急剧爆炸以至于无法进行暴力枚举的问题。本文将介绍一种强大而优雅的方法来应对这一挑战,该方法能够巧妙地避开这种组合爆炸。
在接下来的章节中,您将发现这个优化问题背后的基本逻辑。“原理与机制”一章将剖析括号为何重要,并介绍基于最优性原理的动态规划方法,该方法能保证得到一个完美解。随后,“应用与跨学科联系”一章将揭示这个单一的抽象思想如何远远超越矩阵的范畴,为解决编译器设计、数据库系统、机器人学乃至计算生物学中的问题提供了一个统一的框架。
既然我们已经了解了最优括号化这个难题,现在让我们层层剥茧,探究其背后精妙的运作机制。计算机如何可能在数量惊人的选择中——这个数字呈指数级增长,被称为卡特兰数——找到唯一的最佳方案呢?答案并非暴力枚举,因为即使对于数量不多的矩阵,暴力枚举所需的时间也可能超过宇宙的年龄。答案在于一个极为简单而深刻的思想。
我们问题的核心是我们在小学就学过的一个性质:结合律。对于加法或乘法,如何对数字进行分组无关紧要: 与 是相同的。当我们计算一串矩阵的乘积时,比如 ,结合律保证了无论是计算 还是 ,最终的矩阵结果都是相同的。
那么,如果答案相同,我们为什么还要关心分组方式呢?因为得到该答案的成本可能会有天壤之别。想象一下,你是一家砂石厂的工头。你有三堆砂石: 堆重10吨, 堆重1000吨, 堆重20吨。你的工作是把它们合并起来。这家工厂有一条奇怪的规定,合并两堆砂石的精力(成本)是它们重量的乘积。假设你先合并 和 。成本是 。现在你得到了一堆1010吨的砂石,需要与 合并,这又需要花费 。总成本为 。
如果你先合并 和 呢?那将花费 。现在你得到了一堆1020吨的砂石,需要与 合并,这又需要花费 。总成本为 。嗯,在这个类比中,成本似乎是相同的。让我们修正一下这个类比,使其与矩阵乘法相匹配。
一个 矩阵乘以一个 矩阵的成本,并不仅仅是两个输入的函数;它是所涉及的三个维度的乘积:。这种三方交互是所有复杂性的根源。让我们考虑由三个矩阵组成的链,、 和 。
路径 1:
路径 2:
这些成本相同吗?仅仅是巧合而已!这两个表达式不同,正是这个问题之所以有趣的原因。“内部”维度 和 的值起着至关重要的作用。一条路径可能涉及创建一个巨大的中间矩阵,导致巨大的计算成本,而另一条路径则使中间产物保持较小规模。有趣的是,如果你知道哪条路径更便宜以及便宜多少,你实际上可以反向推导出维度之间的关系。
那么,我们如何在不检查所有路径的情况下找到最佳路径呢?诀窍在于认识到任何大问题都可以分解为更小的、相似的问题。这个思想被称为动态规划,其基础是最优性原理。
简单来说,它指的是:一个最优解是由其子问题的最优解构建而成的。
假设你想找到计算一长串矩阵(比如从 到 )乘积的最廉价方法。无论最后一次乘法是什么,它都必须将这个链条分成两个更小的部分: 和 。最优性原理告诉我们,为了使总成本达到绝对最小值,你计算左边部分 的方法也必须是计算那个特定部分的最廉价方法。对于右边部分也是如此。如果不是这样,你就可以用一个更廉价的方法来解决其中一个子问题,从而得到一个更好的整体解决方案,但这与你一开始就拥有最优解的假设相矛盾!
这为我们提供了一个强大的递推关系。设 为计算从 到 的矩阵链乘积的最小成本。为了找到它,我们只需检查每个可能的最后分割点 。对于每个 ,我们假设已经最优地解决了两个较小的子问题,然后计算总成本: “最后一次乘法的成本”取决于两个子乘积的维度,而这些维度由 和分割点 决定。通过从最小的链(长度为2)开始,逐步向上构建,我们可以建立一个包含所有子问题最优解的表,直到我们得到整个链的答案。
在这里,我们看到了这个思想的深刻普适性。最优性原理或递推关系的结构中,没有任何东西是专门针对矩阵乘法的!只要我们为每种组合定义了明确的成本,动态规划这个“引擎”就能找到涉及由任何满足结合律的二元运算符组合的任何对象序列的表达式的最优求值顺序。
这意味着我们可以用字符串拼接、集合并集或任何你能想象到的抽象运算来代替矩阵乘法。只要运算满足结合律并且我们有成本规则,同样的逻辑就成立。这就是将问题抽象到其核心组成部分的美妙之处:我们一次性解决了一整类问题。
括号化的选择可以被看作是在一个“成本景观”中导航,其中每个点代表一种不同的运算分组方式,其高度就是总成本。我们的目标是找到这片景观中最低的山谷。
这个景观是什么样的?这完全取决于成本函数。让我们考虑一个有趣的特例:一串大小完全相同的方阵,。任意两个中间矩阵相乘的成本是多少?由于任何子乘积也将是一个 的矩阵,因此每一步乘法的成本都是 。要将 个矩阵相乘,无论如何括号化,都恰好需要 次乘法。因此,任何括号化方案的总成本都只是 。
在这种情况下,成本景观是一片完全平坦的平原!每条路径都是最优路径。这带来一个美妙的推论:“最优”括号化方案的数量就是所有可能括号化方案的总数,即一个卡特兰数。这一洞见不仅让我们能找到成本,还能计算达到该成本的方法数量——通过对我们的动态规划引擎稍作修改,在多个分割点产生相同的最小成本时记录计数,我们就可以解决这个问题。
当然,在大多数现实情况下,维度并不统一。成本景观变成了由高耸山峰和深邃山谷组成的崎岖地形。分组方式的微小改变就可能让你从一个低成本的山谷走向一个高成本的山峰。动态规划算法是我们在这片复杂景观中找到绝对最低点的可靠向导。
同一个动态规划引擎不仅可以用来寻找最廉价的方法。如果出于某种原因,我们想找到执行计算的最昂贵方法呢?也许我们正在对一个系统进行压力测试,或者仅仅是探索最坏情况。逻辑完全相同!我们只需将递推关系中的 min 替换为 max:
完全相同的引擎,只需一个微小的调整,现在就能找到成本景观中的最高峰,而不是最低谷。这展示了其基本原理非凡的灵活性。
现在我们来看看这个方法最强大、最美妙的方面。“成本”不一定非得是算术运算次数。它可以是我们关心的任何事物,只要总成本是各个步骤成本的总和。只需插入一个不同的成本函数,我们的通用引擎就能解决一个全新的问题世界。
将可靠性作为成本: 想象每次标量乘法都有一个微小的概率 发生浮点错误。任何一次运算中的错误都会毁掉整个结果。单次运算正确的概率是 。如果一个括号化方案总共需要 次运算,那么整个链计算正确的概率是 。要最大化这个概率,我们必须最小化指数 。突然之间,一个关于最大化可靠性的问题就转化为了我们最初的最小化操作次数的问题!速度上的最优括号化方案同时也是正确性上的最优括号化方案。这是科学原理统一性的一个惊人例子。
将内存作为成本: 如果我们不受时间限制,而是受计算机内存限制呢?一次昂贵的乘法不是指耗时长的乘法,而是指产生一个巨大中间矩阵的乘法。我们可以将一步 的成本定义为所涉及的最大维度的大小,即 ,以此作为内存压力的代理指标。我们将这个新的成本函数代入我们的动态规划引擎,它就会找到对内存最友好的括号化方案。
将成本作为向量: 在现实世界中,我们常常面临多个相互竞争的目标。我们希望计算既快又节省内存。我们可以通过将成本定义为一个向量来处理这个问题,例如 。然后,我们寻求按字典序最小化这个向量:找到时间绝对最小的解,然后,在所有具有相同最小时间的解中,找到内存使用最低的那个。我们的动态规划引擎可以被调整以处理这种情况,只需在每一步比较成本向量而不是单个数字即可。
将成本作为一种不同的“物理学”: 标准的 成本来自经典的矩阵乘法算法。但如果我们使用更高级的、次立方的算法,如 Strassen 算法呢?计算的“物理学”就改变了。一个 矩阵乘以一个 矩阵的成本不再是 ,而是更复杂的形式,比如 ,其中 是“矩阵乘法指数”(对于Strassen算法约为2.81)。我们的整个框架会崩溃吗?完全不会!最优性原理依然成立。我们可以将这个新的、非线性的成本函数代入动态规划的递推关系中,为这种新型硬件找到最优的运算顺序。有趣的是,此时的最佳括号化方案可能与经典情况不同,这证明了最优策略与底层成本密切相关。
这个源于结合律的简单括号放置问题,已经发展成为一个强大的通用优化工具。通过理解其核心机制——在最优性原理指导下的递归分解——并认识到“成本”的抽象性,我们就能用同样优雅和统一的方法,应对各种复杂问题的广阔天地。
现在我们已经掌握了最优括号化原理,你可能会想把它当作一个矩阵乘法的巧妙技巧束之高阁。但这样做就只见树木,不见森林了!这不仅仅是一个数学上的奇趣问题;它是一种基本的模式,一种出现在最意想不到之处的“计算基因”,从我们数字世界的核心到生命本身的设计蓝图。它完美地诠释了,一个单一、优雅的思想如何能为一整类表面上看起来毫无关联的问题提供钥匙。让我们踏上一段旅程,看看这个“选择最佳线性分组方式”的简单思想将我们带向何方。
我们从离我们最近的计算机世界开始。当程序员写下一行像 a + b * c + d 这样的代码时,编译器必须决定如何对其求值。虽然数学上的优先级规则可能适用,但对于一长串同类型的运算,计算机是有选择的。我们已经看到,对于浮点数,这个选择并非无足轻重;它可能决定了结果是正确答案还是数值上的无稽之谈。但即使对于更简单的运算,编译器也可能为了速度而进行优化。最优括号化原理可以推广到为复杂的表达式树找到最高效的求值顺序,其中每个运算根据其输入的规模或复杂性而有不同的“成本”。这是编译器设计中的一项基本任务,确保我们编写的代码能以尽可能快的速度运行。
这种“运算流水线”的思想在软件中无处不在。想象一系列文本处理过滤器:一个查找所有电子邮件地址,另一个将所有专有名词大写,第三个用正式语言替换俚语。每个过滤器都接收文本并输出修改后的文本。以不同方式组合这些过滤器——((Filter1 Filter2) Filter3) 与 (Filter1 (Filter2 Filter3))——可能会产生截然不同的性能影响,这取决于每个过滤器如何改变它传递的数据的大小和结构。寻找链接这些过滤器的最廉价方式,再一次,是我们熟悉的括号化问题换上了一件新外衣。
在整个计算机科学中,经济意义最重大的应用可能是在数据库系统的核心。当你在网站上搜索某样东西时,它通常会触发一个数据库“查询”,从多个表中连接信息。例如,要查找“购买了特定产品的所有客户的送货地址”,数据库可能需要将 Customers 表与 Orders 表连接,然后将结果与 Products 表连接。“连接”(join)是一种根据公共字段组合两个表的操作。一系列的连接,如 ,是满足结合律的。你可以先计算 ,或者先计算 。连接的成本很大程度上取决于被连接表的大小。一个糟糕的连接顺序可能导致创建巨大的中间表,使查询从毫秒级慢到小时级。数据库查询优化器每天都面临这个问题,它们使用的正是我们学过的动态规划方法来找到连接的最优括号化方案,从而在全球范围内节省了无法估量的时间和计算资源。
同样的挑战在现代人工智能时代再次出现。许多机器学习系统被构建为转换的流水线。一个输入向量可能会经过一系列线性层,每个层都由一个矩阵表示。最终输出是按顺序应用所有这些矩阵变换的结果。为了减少模型做出预测所需的时间(即其“推理延迟”),工程师需要找到计算这个矩阵乘法链的最快方法。最优括号化再次提供了答案,帮助我们使前沿的人工智能模型更快、更高效。
这个原理不仅存在于软件的抽象领域。它向下延伸到物理硬件,向外扩展到工程世界。“操作”的“成本”并不总是一个抽象的计数。在真实的计算机中,两个矩阵相乘涉及将其数据从内存取到处理器小而高速的缓存中。如果一个中间矩阵足够小,能够完全放入这个缓存中,那么涉及它的下一次乘法将会快得多。因此,一个真正聪明的优化算法不应仅仅最小化乘法次数;它应该以一种能够尽可能创建小的、缓存友好的中间结果的方式来对链进行括号化。这需要调整我们的成本函数,但底层的动态规划结构保持不变,完美地弥合了理论算法与现实世界硬件架构之间的鸿沟。
让我们完全离开计算机。想象一条机器人装配线,任务是连接五个预制组件以制造最终产品。机器人一次只能连接两个相邻的子组件。执行一次连接所需的时间可能取决于所连接两个部件的复杂性和大小。机器人应该从连接前两个部件开始,还是最后两个?或者也许是中间的两个?这又是我们的老问题!组件序列就是矩阵链,“连接时间”就是乘法成本。找到最快的装配顺序等同于找到最优的括号化方案。
但速度并不是唯一重要的。准确性呢?当计算机对一串数字如 求和时,运算顺序对结果有深远的影响。如果你计算 ,由于精度有限,计算机可能直接将答案舍入回 。那个 1 就完全丢失了!但如果你先加小数字—— 得到 ——然后再将其加到 ,结果更有可能是准确的。对于一长串加法,找到最小化这种累积舍入误差的括号化方案在科学计算中至关重要。令人惊奇的是,这个最小化数值误差的问题也完美地映射到我们的动态规划框架上。在这里,一次分割的“成本”与所创建的中间和的大小有关,因为较大的和更容易吞没较小的和。这是一个微妙而深刻的例子,说明了相同的抽象结构如何能不仅为速度优化,也为正确性优化。
最引人注目的是,这个原理并非人类工程师的发明;大自然似乎也发现了它。在计算生物学中,科学家们试图通过比较物种的基因序列来重建其进化史。一种常用方法是构建系统发育树,其中叶节点是已知物种,内部节点代表假想的共同祖先。一种建模方法是从一个固定的序列顺序开始,决定首先“合并”哪些相邻的组,每次合并都根据基因差异关联一个成本。在这个模型下,寻找最 plausible(即成本最低)的进化树的问题,你猜对了,就是一个最优括号化问题。
也许最优雅的生物学类比是RNA折叠。单链RNA是一串核苷酸,它不会保持直线状态,而是会自我折叠,形成复杂的三维结构,这对其生物功能至关重要。这种结构主要由互补的核苷酸对(A与U,G与C)形成键来决定。然而,一个关键约束是结构必须是“非交叉”的——如果核苷酸 与 配对,而 与 配对,你不能有像 这样的顺序。这与数学表达式中括号必须满足的约束完全相同!预测最稳定的RNA结构的问题,就变成了寻找最大数量的非交叉配对的问题。虽然其递推关系比矩阵乘法的要复杂一些——除了在某个点 分割链之外,它还必须考虑端点 配对的情况——但基本策略是相同的:一个基于区间的动态规划,从更小的、最优的子解中构建出最优解。大自然在寻求稳定能量状态的过程中,似乎也在解决它自己版本的的最优括号化问题。
在所有这些不同领域中,回响着同一个模式:对于一串满足结合律的运算,最优的求值顺序可以通过递归地寻找最佳分割点来找到。动态规划算法的骨架保持不变。每个具体应用的“灵魂”在于其独特的成本函数。
对于标准矩阵乘法,成本是维度的简单乘积。但我们已经看到,它可以远不止于此。在处理稀疏矩阵时,它可以是浮点运算的真实数量,此时我们不仅要追踪最小成本,还要追踪中间结果不断变化的稀疏模式。它可以是延迟、数值误差、数据库连接时间的度量,甚至是一个分子热力学稳定性的代理指标。
这种算法范式的力量在于其优美的关注点分离:解的递归结构独立于我们定义组合两部分成本的具体方式。只要问题具有最优子结构且成本是可加的,这一个绝妙的思想就给了我们钥匙。它教会了我们一个深刻的道理:要解决一个新问题,有时我们所需要做的,只是识别一个旧模式,以及最重要地,正确定义“它的成本是什么”。