算法基础知识——动态规划
目录:
- 基础知识
- 分治法和动态规划的区别
- 动态规划算法设计步骤
- 最优子结构性质定义
- 动态规划两种等价的实现方法(自顶向下带备忘、自底向上)
- 子问题图
- 经典问题
- 钢条切割
- 矩阵链乘法
- 最大连续子序列和
- 最长递增子序列
- 最长公共子序列
- 0-1背包问题
- 完全背包问题
- 应用实例
- N阶楼梯上楼问题【华中科技大学】
- 吃糖果【北京大学】
- 最大序列和【清华大学】
- 最大子矩阵【北京大学】
- 最大连续子序列【浙江大学】
- 拦截导弹【北京大学】
- 最大上升子序列和【北京大学】
- 方块涂色【暨南大学】
- 合唱队形【北京大学】
- Common Subsequence【Southeastern Europe 2003】
- Coincidence【上海交通大学】
- 点菜问题【北京大学】
- 采药【北京大学】
- 最小邮票数【清华大学】
- Piggy-Bank【Central Europe 1999】
- 悼念512汶川大地震遇难同胞——珍惜现在,感恩生活【2008-06-18《 ACM程序设计》期末考试——四川加油!中国加油!】
- The Triangle【POJ 1163】
- 放苹果【北京大学】
- 整数拆分【清华大学】
- Monkey Banana Problem【light oj 1004】
一、基础知识
1、分治法和动态规划的区别:
- 分治法(Divide and Conquer):
- 将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。
- 反复求解公共子子问题。
- 动态规划(Dynamic programming):
- 应用于子问题重叠的情况,即不同的子问题具有公共的子子问题。
- 对每个子子问题只求解一次,将其保存在一个表格中,从而无需每次求解一个子子问题时都重新计算。
- 通常用于求解最优化问题(Optimization problem),找到具有最优值的解,即找到问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都达到最优值。
2、动态规划算法设计步骤:
- 刻画一个最优解的结构特征;
- 递归地定义最优解的值;
- 计算最优解的值,通常采用自底向上的办法;
- 利用计算出的信息构造一个最优解。
3、最优子结构性质(optimal substructure):
- 问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
4、动态规划方法是付出额外的内存空间来节省计算时间,是时空权衡(time-memory trade-off)的例子,可以将一个指数时间的解转化为一个多项式时间的解。
5、动态规划两种等价的实现方法:
- 带备忘机制的自顶向下法(top-down with memoization):
- 按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。
- 当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值;否则,按通常方式计算这个子问题。
- 自底向上法(bottom-up method):
- 需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题的求解。因而可以将子问题按规模排序,按由小到大的顺序进行求解。
- 当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已求解完成。
- 区别:两种方法具有相同的渐近运行时间。在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。由于没有频繁的递归调用的开销,自底向上方法的时间复杂性函数通常具有更小的系数。
6、子问题图:
- 定义:一个有向图,每个顶点唯一地对应一个子问题,若求解子问题x的最优解时需要直接用到子问题y的最优解,那么在子问题图中就会有一条从子问题x的顶点到子问题y的顶点的有向边。
- 特点:
- 自底向上动态规划算法是按逆拓扑序(reverse topological sort)来处理子问题图中的顶点。
- 带备忘机制的自顶向下动态规划算法是按照深度优先搜索(depth-first search)来处理子问题图中的顶点。
- 关系:
- 子问题图G = (V, E),因为每个子问题只求解一次,所以动态规划算法运行时间等于每个子问题求解时间之和。
- 通常,一个子问题的求解时间与子问题图中对应顶点的出度成正比。
- 动态规划算法的运行时间与顶点和边的数量呈线性关系。
二、经典问题
1、钢条切割(将长钢条切割成短钢条,使得总价值最高)
(1)思路:
- 当完成初次切割后,将两段钢条看成两个独立的钢条切割问题实例。
- 通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
2、矩阵链乘法(用最少的标量乘法操作完成一个矩阵链相乘的运算)
(1)完全括号化:一个单一矩阵,或者是两个完全括号化的矩阵乘积链的积,且已外加括号。
- 如矩阵链为(A1,A2,A3,A4),则有五种完全括号化的矩阵乘积链:
- (A1(A2(A3A4)))
- ((A1(A2A3))A4)
- (A1((A2A3)A4))
- ((A1A2)(A3A4))
- (((A1A2)A3)A4)
- 若AB = C中,A为p * q的矩阵,B为q * r的矩阵,则C为p * r的矩阵,计算C的时间为pqr。因此不同的加括号方式可能会导致不同的计算代价。
(2)思路:
- 最优化括号方案的结构特征:
- 一个非平凡的矩阵链乘法问题实例的任何解都需要划分链,而任何最优解都是由子问题实例的最优解构成的。
- 因此为了构造一个矩阵链乘法问题实例的最优解,我们可以将问题划分为两个子问题(AiAi+1…Ak和Ak+1Ak+2…Aj的最优括号问题),求出子问题实例的最优解,然后将子问题的最优解组合起来。
- 一个递归求子问题最优解的方案:
- 找到最优分割点k:对于AiAi+1…Aj的最优括号化方案,k有j - i种可能取值,k = i,i + 1,…,j - 1。因此检查所有可能的情况,找到最优者。
- m[i, j] = min{m[i, k] + m[k + 1, j] + pi-1*pk*pj},其中i ≤ k < j,若i < j;m[i, j] = 0,若i = j。
- 用s[i, j]保存AiAi+1…Aj最优括号化方案的分割点位置k,即使得m[i, j] = m[i, k] + m[k + 1, j] + pi-1*pk*pj成立的k值。
- 计算最优代价:
- 过程用一个辅助表m[ 1…n, 1…n ]来保存代价m[ i, j],辅助表s[1..n-1, 2..n]记录最优值m[i, j]对应的分割点k。
(3)递归公式:
- P(n) = 1, n = 1;P(n) = ∑(从k = 1到k = n - 1)P(k)P(n - k),n ≥ 2。
3、最大连续子序列和
- 定义:在一个给定的序列{A1, A2, ..., An}中,找出一个连续的子序列{Ai,...,Aj},使得这个连续的子序列的和最大,输出这个最大的子序列和。
- 令dp[i]为以A[i]作为末尾的连续序列的最大和。
- dp[i] = max{A[i], dp[i - 1] + A[i]}
- 最终最大连续子序列和为数组dp中的最大值。
- 时间复杂度:O(n)
4、最长递增子序列(Longest Increasing Subsequence,LIS)
- 定义:在一个已知序列{A1,A2,...,An}中,取出若干元素(不必连续)组成一个新的序列{Ax,...,Ay},新序列的各个数之间依旧保持原序列中的先后顺序,此时称新序列{Ax,...,Ay}为原序列的一个子序列。若对子序列中的任意下标x < y有Ax < Ay,则称该子序列为原序列的一个递增子序列。最长递增子序列问题就是求给定序列的所有递增子序列中最长的那个子序列长度。
- 令dp[i]为以A[i]为末尾的最长递增子序列的长度。
- dp[i] = max{1, dp[j] + 1 | j < i && Aj < Ai}
- 最终最长递增子序列的长度即为数组dp中的最大值。
- 时间复杂度:O(n²)
5、最长公共子序列(Longest Common Subsequence,LCS)
- 定义:给定两个字符串S1和S2,求一个最长公共子串,即求字符串S3,它同时为S1和S2的子串,且要求它的长度最长,并确定这个长度。
- 对于长度为n的字符串S1和长度为m的字符串S2,令dp[i][j]表示以S1[i]作为末尾和以S2[j]作为末尾的最长公共子序列的长度,则dp[n][m]的值即为最长公共子序列的长度。
- dp[i][j] = dp[i - 1][j - 1] + 1,S1[i] = S2[j]
- dp[i][j] = max{ dp[i - 1][j], dp[i][j - 1] },S1[i] != S2[j]
- 对于边界,若两个字符串中的一个为空串,则:
- dp[i][0] = 0(0 ≤ i ≤ n)
- dp[0][j] = 0(0 ≤ j ≤ m)
- 最终dp[n][m]中保存的值即为两个原始字符串的最长公共子序列长度。
- 时间复杂度:O(nm)
6、0-1背包问题
- 定义:有n件物品,每件物品的重量为w[i],其价值为v[i],现在有个容量为m的背包,如何选择物品使得装入背包物品的价值最大。
- 令dp[i][j]表示前i个物品装进容量为j的背包能获得的最大价值。则dp[n][m]就是0-1背包问题的解。
- dp[i][j] = max{ dp[i - 1][j], dp[i - 1][ j - w[i] ] + v[i] }
- dp[i][j] = dp[i - 1][j],第i件物品不放入
- dp[i][j] = dp[i - 1][ j - w[i] ] + v[i],第i件物品放入,其中j - w[i] ≥ 0,表示背包的容量可以放入第i件物品
- 边界情况:
- dp[i][0] = 0,0 ≤ i ≤ n
- dp[0][j] = 0,0 ≤ j ≤ m
- dp[i][j] = max{ dp[i - 1][j], dp[i - 1][ j - w[i] ] + v[i] }
- 一维数组的形式:dp[j] = max{ dp[j], dp[ j - w[i] ] + v[i] },同时保证再每次更新中确定状态dp[j]时,dp[ j - w[i] ]未被修改,从而完成正确的状态转移。因此需要在每次更新中,倒序地遍历所有j的值。
- 最终dp[n][m]中保存的值即为0-1背包问题的解。
- 时间复杂度:O(nm)
7、完全背包问题
- 定义:有n种物品,每种物品的重量为w[i],其价值为v[i],每种物品的数量均为无限个,现在有容量为m的背包,如何选择物品使得装入背包的价值最大?
- 令dp[i][j]表示前i个物品装进容量为j的背包能获得的最大价值。则dp[n][m]就是完全背包问题的解。
- dp[i][j] = max{ dp[i - 1][j], dp[i][ j - w[i] ] + v[i] }
- dp[i][j] = dp[i - 1][j],第i件物品不放入
- dp[i][j] = dp[i][ j - w[i] ] + v[i],第i件物品放入,其中j - w[i] ≥ 0,表示背包的容量可以放入第i件物品
- 边界情况:
- dp[i][0] = 0,0 ≤ i ≤ n
- dp[0][j] = 0,0 ≤ j ≤ m
- 一维数组的形式:dp[j] = max{ dp[j], dp[ j - w[i] ] + v[i] },同时保证再每次更新中确定状态dp[j]时,dp[ j - w[i] ]已经被修改,从而完成正确的状态转移。因此需要在每次更新中,正序地遍历所有j的值。
- 最终dp[n][m]中保存的值即为完全背包问题的解。
- 时间复杂度:O(nm)
8、多重背包问题
- 定义:有n种物品,每种物品的重量为w[i],其价值为v[i],每种物品的数量均为k[i],现在有容量为m的背包,如何选择物品使得装入背包的价值最大?
- 将数量为k的物品拆分为若干组,将每组物品视为一件物品,