动态规划是软考中级软件设计师和高级系统架构设计师算法设计部分绕不开的核心考点,也是历年选择题与下午算法题的高频出题对象。不少考生对动态规划的理解停留在"背公式、套模板"的层面,一遇到状态转移方程的推导就发怵,遇到矩阵连乘这类经典问题的计算题更是频频丢分。这篇文章不堆砌概念,而是从动态规划的两大本质特征切入,把最优子结构、重叠子问题、状态转移方程、自底向上求解这些底层机制一次讲透,再落到矩阵连乘问题这个每年必考的具体算例上,配合历年真题的命题思路拆解,让读者看完不仅能算对题,更能真正理解动态规划为什么有效、什么时候该用它。
动态规划(Dynamic Programming)这一名称中的"Programming"并非指编写程序代码,而是指一种"规划"或"制表"的数学方法。它由美国数学家理查德·贝尔曼在二十世纪五十年代提出,最初用于求解多阶段决策过程的优化问题。在软考教材和算法设计的标准语境中,动态规划被定义为:一种通过把原问题分解为若干相互重叠的子问题,先求解子问题并保存其结果,再自底向上逐步构造原问题最优解的算法设计技术。
从形式化角度看,动态规划求解的问题必须满足两个条件:一是具有最优子结构性质,即问题的最优解能够由若干子问题的最优解组合而成;二是子问题具有重叠性质,即在递归求解过程中,同一个子问题会被多次重复计算。这两条性质是动态规划与分治法、贪心法的根本分界。教材通常把动态规划的基本步骤归纳为四步:第一步,刻画最优解的结构特征,也就是定义状态;第二步,递归地定义最优解的值,也就是写出状态转移方程;第三步,以自底向上的方式计算最优解的值,通常用表格或数组保存中间结果;第四步,根据计算得到的信息构造最优解本身。
分治法与动态规划都会把大问题分解为子问题,但二者的区别在于子问题之间是否相互独立。分治法要求子问题彼此独立,互不重叠,因此递归树上没有重复的节点,例如归并排序把数组一分为二,左右两半的排序互不干扰。动态规划所处理的子问题则是高度重叠的,递归树上会出现大量相同的节点,如果沿用分治法的朴素递归,就会把同一个子问题反复求解,导致指数级的时间复杂度。动态规划的智慧正在于"以空间换时间",把已经求解的子问题结果存起来,避免重复计算。
贪心法与动态规划都用于求解最优化问题,也都利用子问题的解来构造原问题的解,但二者的决策方式截然不同。贪心法在每一步都做出当前看起来最优的局部选择,并且一旦选定就不再回头,它依赖的是"贪心选择性质",即局部最优能够导出全局最优。动态规划则保留所有可能的选择,通过比较不同子问题的结果来选出全局最优,它依赖的是"最优子结构性质"。一个典型的判别例子是:在带权图的最短路径问题中,Dijkstra 算法采用的是贪心策略,每一步都选当前距离最小的顶点;而在矩阵连乘问题中,加括号的次序必须整体规划,任何局部贪心的加括号方式都不能保证得到全局最少乘法次数,这正是动态规划的用武之地。
理解动态规划可以从三个层次由浅入深地递进。第一层是朴素递归,它严格依照问题的递归定义直接编写函数,思路最直观,却会因重复计算导致指数级的时间复杂度,在 n 稍大时便无法在有限时间内得出结果。第二层是记忆化递归,它在递归函数入口处先查询一张哈希表或数组,若该子问题已经求解过就直接返回保存的结果,否则计算后再存入表中;这一层把指数级复杂度降到多项式级,是很多工程实现的首选。第三层是自底向上的递推,它彻底摆脱递归调用栈,按照子问题规模从小到大的顺序填表,先算小问题再算大问题。软考算法题重点考查的是第三层,因为递推的填表顺序与命题人设计的计算题直接对应,考生只有在纸上能熟练地按顺序填表,才能在考场上算对矩阵连乘这类题目。理解这三层之间的递进关系,也有助于在遇到陌生问题时快速判断应当从哪一层入手建模。
要真正掌握动态规划,必须深入到它得以成立的两个底层性质,以及由此衍生出的两种实现路径。这两个性质不是抽象的口号,而是直接决定了一道题能否用动态规划求解、状态如何定义、转移方程如何书写的技术判断依据。
最优子结构指的是:一个问题的最优解包含其子问题的最优解。换句话说,如果知道了子问题的最优解,就能通过某种组合方式得到原问题的最优解。以矩阵连乘为例,假设有 n 个矩阵从 A1 到 An 需要连乘,最终的最优加括号方案一定可以表示为在某一个位置 k 处把连乘式一分为二,即先计算 A1 到 Ak 的连乘积,再计算 Ak+1 到 An 的连乘积,最后把这两个结果相乘。如果整条连乘链的最优解在第 k 处断开,那么前半段 A1 到 Ak 的连乘次序、后半段 Ak+1 到 An 的连乘次序也必然分别是各自区间上的最优解,否则用一个更优的子区间方案替换,就能得到整条链上更少的乘法次数,与"最优"的前提矛盾。这种"整体最优蕴含局部最优"的结构,就是状态转移方程能够成立的逻辑根基。要强调的一点是,最优子结构并不要求原问题的最优解唯一,只要存在一种由子问题最优解组合而成的最优解即可。命题人有时会故意在题目中构造多个同为最优的加括号方案,来考察考生是否真正理解"存在性"而非"唯一性"。此外,判断一个优化问题是否具备最优子结构,一个实用的检验办法是反证法试错:假设子问题取非最优解仍能得到原问题最优解,看是否导出矛盾,若矛盾成立,则最优子结构性质成立。这一检验思路在应对"下列问题适合用动态规划求解的是"这类归类选择题时尤其管用。
重叠子问题指的是:在递归求解的过程中,同一规模的同一子问题会被多次访问。仍以矩阵连乘为例,朴素的递归做法是枚举断开位置 k,而枚举过程中,区间 [i, j] 的连乘最优值会被上层多个不同的父区间反复调用。随着 n 增大,这种重复计算的次数呈指数级增长,朴素递归的时间复杂度高达指数级,根本无法在合理时间内求解较大的 n。动态规划通过一张表来存储每个区间 [i, j] 的最优值,每个子问题只计算一次,后续直接查表,从而把时间复杂度从指数级降到多项式级。需要强调的是,重叠子问题与最优子结构是"且"的关系,二者缺一不可:只有最优子结构而没有重叠子问题的问题适合用分治法高效求解;只有重叠但没有最优结构的,则谈不上"最优解"的组合。
动态规划在具体实现上有两种主流写法。第一种是自底向上的递推法,也叫表格法,它按照子问题规模的从小到大依次计算,先算出最小的子问题,再用已算出的结果去算更大的子问题,最终得到原问题的解。矩阵连乘的标准求解就是自底向上的典型:先处理区间长度为 2 的情况,再逐步扩大区间长度。第二种是自顶向下的记忆化递归,它保留递归的自然结构,但在每次进入子问题前先查表,如果已经算过就直接返回,否则计算后存入表中。两种写法在时间复杂度和空间复杂度上等价,考试中的计算题大多要求掌握自底向上的表格填法,因为这种写法能直观地展示状态转移方程和填表顺序。两种写法各有优劣。自底向上的递推法空间上更可控,且便于在循环中直接构造最优解本身,是考试计算题的标准解法;记忆化递归则保留了递归代码的简洁结构,适合在问题规模不易按顺序枚举的场景中使用,但存在递归深度过大导致栈溢出的风险。软考对二者都有考查,选择题可能问自底向上与自顶向下的时间复杂度是否相同,答案是相同,均为多项式级,区别仅在于实现方式与常数因子。考生若能默写出矩阵连乘自底向上的双重循环框架,就能同时应对计算题和概念题。
矩阵连乘问题是动态规划最经典、也是软考出题最频繁的应用算例,理解它的完整建模过程,就等于掌握了动态规划解题的全套方法。这一节把问题形式化、转移方程推导、复杂度分析逐一讲清。
矩阵连乘问题的背景是这样的:给定 n 个矩阵 A1、A2、……、An,其中矩阵 Ai 的规模为 pi-1 行 pi 列,即相邻矩阵满足前一矩阵的列数等于后一矩阵的行数,从而保证连乘在维度上是合法的。矩阵乘法满足结合律,却不满足交换律,因此加括号的方式虽然不改变最终结果,却会极大地影响标量乘法的总次数。设 Ai 是 p×q 的矩阵,Aj 是 q×r 的矩阵,那么 Ai 与 Aj 相乘需要 p×q×r 次标量乘法。问题的目标就是确定一种加括号方案,使得整个连乘式所需的标量乘法总次数最少。
动态规划求解矩阵连乘问题的核心是定义状态并写出转移方程。定义 m[i][j] 为从矩阵 Ai 到矩阵 Aj 这一段连乘所需的最少标量乘法次数。当 i 等于 j 时,只有一个矩阵,无需乘法,因此 m[i][i] 等于 0。当 i 小于
本篇完!