一、概述
1.设计思想
动态规划法将待求解问题分解成若干个相互重叠的子问题,每个子问题对应决策过程的一个阶段,通过组合子问题而解决整个问题的解。
2.基本要素
(1)最优子结构
最优性原理体现为问题的最优子结构特性。当一个问题的最优解中包含了子问题的最优解时,则称该问题具有最优子结构特性。
(2)重叠子问题
通常,子问题的重叠关系表现在对给定问题求解的递推关系(称为动态规划函数)中,将子问题的解求解一次并填入表中,当需要再次求解该子问题时,通过查表获得,从而避免了大量重复计算。
(3)问题状态须满足无后效性。
所谓无后效性是指:“下一时刻的状态只与当前状态有关,而和当前状态之前的状态无关,当前状态是对以往决策的总结”。
3.求解过程
一般来说,动态规划算法的求解过程由以下三个阶段组成:
(1)分析最优解结构:将原问题分解为若干个子问题,每个子问题对应一个决策阶段,根据子问题找出最优解的性质,并刻画其结构特征;
(2)建立递归关系:根据子问题之间的重叠关系找到子问题满足的递推关系式(即动态规划函数),递归地定义最优值,这是算法关键;
(3)计算最优值:设计表格,以自底向上的方式计算各个子问题的解并填表,得到最优值(目标函数极值),实现动态规划过程。
如果要求出具体的最优解,通常在动态规划过程中记录必要的信息,再根据最优决策序列构造最优解。
4.算法比较
名称 | 描述 | 是否填表 | 递归方式 | 适用范围 |
动态规划算法 | 略 | 填表 | 自底向上 | 当该问题的所有子问题都需要求解一次时使用 |
备忘录方法 | 动态规划算法的变形 | 填表 | 自顶向下 | 当子问题空间的部分子问题不必求解时使用 |
直接递归方法 | 直接或间接调用自身 | 不填表 | 自顶向下 |
二、例题总结
1.矩阵连乘问题
【问题】给定n个矩阵{A1,A2, ... ,An},其中Ai与Ai+1是可乘的,考察这n个矩阵的连乘积A1A2...An。
【算法设计】
(1)分析最优解结构
考察A[1:n]的最优计算次序:设这个计算次序在Ak和Ak+1之间将矩阵连断开,依此次序,总计算量 = A[1:k]的计算量 + A[k+1:n]的计算量 + A[1:k]和A[k+1:n]相乘的计算量。
该问题的一个关键特征是:计算A[1:n]的最优次序所包含的计算子矩阵链A[a:k]和A[k+1:n]的次序也是最优的,这种性质称为最优子结构性质。
(2)建立递归关系
设计算A[i:j]的最少数乘次数为m[i][j],即原问题的最优值为m[1][n],递归定义如下:
将对应m[i][j]的断开位置k记为s[i][j],在计算出最优值m[i][j]后,可递归地由s[i][j]构造出相应的最优解。
(3)计算最优值
动态规划算法解此问题,可依据其递归式以自底向上的方式进行计算,在计算过程中保留已解决的子问题答案,方便后边查找,避免大量重复计算。
【算法实现】
下面给出动态规划算法MatrixChain,输入参数{p0,p1,...,pn}存储于数组p中,除了输出最优值数组m,还给出记录最优断开位置的数组s。
算法分析:
2.最长公共子序列
【问题】给定两个序列X和Y,当序列Z既是X的子序列又是Y的子序列时,称z是X和Y的公共子序列。最长公共子序列问题:给定两个序列X={x1,x2,...,xm}和Y={y1,y2,...,yn},找出X和Y的最长公共子序列。
【算法设计】
(1)分析最优解结构
两个序列的最长公共子序列包含了这两个序列前缀的最长公共子序列,因此,该问题具有最优子结构性质。
(2)建立递归关系
(3)计算最优值
由于所考虑的子问题空间中,共θ(mn)个不同的子问题,因此用动态规划算法自底向上计算最优值能提高算法效率。
【算法实现】设序列X存储在数组x[m]中,序列Y存储在数组y[n]中,最长公共子序列存储在数组z[k]中,二维数组L[m+1][n+1]存储最长公共子序列的长度,S[m+1][n+1]存储相应的状态,算法如下:
【算法分析】
时间复杂度O(mn)
3.最大字段和
【问题描述】给定由n个整数(可正可负)组成的序列a1,a2,...,an,求该序列子段和的最大值。当所有整数均为负整数时定义其最大字段和为0。
【蛮力算法】
【分治算法】
(1)划分:按照平衡子问题原则,将序列划分为长度相同的两个子序列,则会出现以下三种情况:
①a[1:n]的最大字段和与a[1:n/2]的最大字段和相同;
②a[1:n]的最大字段和与a[n/2+1:n]的最大字段和相同;
③a[1:n]的最大字段和既包含左半段又包含右半段。
(2)求解子问题:对于划分后的情况1和2可递归求解,情况3需要分别计算左右两边的最大字段和s1和s2,则s1+s2为情况3的最大字段和。
(3)合并:比较三种情况的最大字段和,选择较大者作为原问题的解。
【动态规划算法】
(1)分析最优子结构
用f(i)表示以第i个数结尾的子数组的最大和,则
当以第i-1个数结尾的所有数字和为负时,f(i)的结果为a[i]本身;
当以第i-1个数结尾的所有数字和为正时,f(i)的结果为f(i-1) + a[i]。
(2)建立递归关系
(3)计算最优值,算法实现
4.流水作业调度
【问题描述】n个作业在两台机器M1和M2组成的流水线上完成加工,先在M1上加工,然后在M2上加工,M1和M2加工作业i的时间分别是ai和bi。流水作业调度问题要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工所需的时间最少。
5.0-1背包问题(*)
【问题描述】
【算法设计】
(1)分析最优解结构
设V(n,C)表示将n个物品装入容量为C的背包获得的最大价值,定义一个子问题:
①初始子问题:V(i,0) = V(0,j) = 0;
②原问题的一部分:设V(i,j)表示将前i个物品装入容量为j的背包获得的最大价值,在决策xi时,已确定了前i-1个物品的状态,则问题可分为以下两种情况;
情况一:如果装不下当前物品,那么前n个物品的最大价值(最佳组合)与前n-1个物品的是一样的;(不用给当前物品预留空间)
情况二:如果装的下当前物品,则有两种选择:
选择1:装当前物品(先默认给当前物品预留空间,容量只剩下j-wi),背包中物品价值等于把前i-1个物品装入容量为j-wi的背包的价值加上当前物品价值vi;
选择2:不装当前物品,如情况一,然后选取选择1和选择2中较大价值,作为当前最大价值(最佳组合)。
(2)建立递归关系
(3)计算最优值
如何获取在最佳组合的情况下,选取了哪几个物品?
从表的右下角开始回溯,如果发现前n个物品的最佳组合的价值和前n-1个物品的最佳组合价值一样,则没装入,否则装入了。
由此得到以下函数:
【算法实现】设n个物品的重量存储在数组w[n]中,价值存储在数组v[n]中,背包容量为C,数组V[n+1][C+1]存放迭代结果,其中V[i][j]表示前i个物品装入容量为i的背包中获得的最大价值,数组x[n]存储装入背包的物品,动态规划算法求解0/1背包问题的算法如下:
【算法分析】
第一个for循环:O(n),第二个for循环:O(C),第三个for循环:O(nC),第四个for循环:O(n) ,
因此,KnapSack算法的时间复杂度为O(nC)。
6.最优二叉查找树
【问题描述】最优二叉查找树是以n个记录构成的二叉查找树中具有最少平均比较次数的二叉查找树。具有以下性质:存储在每个结点中的元素x大于其左子树中任一结点所存储的元素,小于其右子树中任一结点所存储的元素。
【算法设计】
(1)该算法具有最优子结构特征;
(2)定义子问题:设C(1,n)是最优二叉查找树T(1,n)的平均比较次数,显然初始子问题是只有一个记录,则对应的二叉查找树只有根结点。考虑原问题的部分解,记录rk为T(i,j)的根结点,则可得到如下动态规划函数:
【算法实现】
设n个字符的查找概率存储在数组p[n]中,为避免在函数之间传递参数,将矩阵C[n+1][n+1]和R[n+1][n+1]设为全局变量,动态规划算法求解最优二叉查找树算法如下:
【算法分析】
算法OptimalBST的基本语句显然是三层for循环中最内层的条件语句,因此该算法的时间复杂度是O(n3)。