作为一个非科班出身的码农,对计算机的一些基础知识有很多不太熟悉的地方。考虑到万丈高楼平地起,只是一味地在工作中对业务问题构建空中楼阁确实不利于以后的发展,决定对计算机的基础知识做一些学习和整理,一方面检查自己是否真正掌握该内容,另一方面望各位同行不吝赐教。
废话少说,直入正题,今天主要整理一下动态规划,以下内容主要来源于对《算法导论》和《麻省理工大学算法导论公开课》的学习整理。
1. 动态规划简介
动态规划(Dynamic Programming)中的programming指的是一种表格法,直译过来就是用动态表格去解决问题的一种方法,通常用来解决最优化问题,问题的解并非唯一解,而是一个解。没有实例讲解算法就是耍流氓,接下来以最长公共子序列的问题来展开介绍动态规划是个啥,以及如何设计动态规划来解决实际问题。
2. 最长公共子序列问题
子序列:给定一个序列
X=<x1,x2,...,xn>
,另一个序列
Z=<z1,z2,...,zk>
满足如下条件时成为
X
的子序列,即存在一个严格递增的
公共子序列:给定两个序列
X
和
最长公共子序列(Longest common subsequence, LCS)问题即给定两个序列
X(x1,x2,...,xn)
,
Y(y1,y2,...,ym)
,求
X
和
问题实例
设给定两个序列,
X=(A,B,C,B,D,A,B)
,
Y=(B,D,C,A,B,A)
,求
X
和
直观地,我们可以考虑用穷举法解决该问题。
2.1 穷举法
因为要查找
X
和
时间复杂度:
2.2 带备忘的自顶向下方法
首先,引入一组定义:
从而可以得到 c[i,j] 的递归表达式:
从而引出动态规划的 第一个重要性质——最优子结构:如果一个问题的最优解包含其子问题的最优解,那么我们称此问题具有最优子结构的性质。上面递归表达式中, c[i,j] 的最优解必然包含 c[i,j−1] 和 c[i−1,j] 的最优解。
基于以上表达式,我们可以构建求解 c[i,j] 的递归算法,其伪代码表示为:
求解的最差情况是 X[i]≠Y[j],∀(i≠j)
根据上面的递归表达式,我们以 n=7,m=6 为例,画出求解问题的迭代树。(不知道用markdown如何生成这种图片,就手画了。。。)
可以发现,会有很多递归求解是重复的,这就引出了动态规划的 第二个重要性质——重叠子问题:如果递归算法反复求解相同的子问题,则称该问题具有重叠子问题。LCS的问题包含了 m∗n 个独立子问题。为了减少重复计算,通常会将子问题的结果存入表中,每次查表获取子问题的解,或许因为查表,所以动态规划(programming),这个表也被成为 备忘表。此时,递归伪代码变为:
此时求解LCS的时间复杂度变为 Θ(mn) ,空间复杂度为 Θ(mn)
以上即为带备忘的自顶向下求解LCS的方法,通过增加空间复杂度,大大降低了时间复杂度。
2.3 自底向上动态规划
自顶向下的方法中,每次会对自身进行迭代,获取当前需要的子问题,相当于一个从大到小的转变。在某些情况下,自顶向下对方法并未考虑所有的子问题(比如某些子问题并为必须求解的),而且,频繁地递归调用会有一定的开销,因此考虑自底向上,求解全部的子问题的最优解,进而求解最终的最优解,简单的伪代码如下:
通过该代码,即可求出LCS的长度,即最优解的值。为了求出LCS的具体元素,可以额外使用一个数组储存每次寻找的路径,从而获取最优解。
3. 动态规划基本策略
综上,可以总结一下动态规划的基本策略:
1. 刻画一个最优解的结构特征
2. 递归地定义最优解的值
3. 计算最优解的值,通常采用自底向上的方法
4. 利用计算出的信息构造一个最优解