References

  1. 《算法导论》

最近在回顾算法的知识,特将一些动态规划的重点记录下来,好让以后自己不要忘记。

基本概念

动态规划(Dynamic Programming,简称为DP,下面都将使用DP来代替)是一种将大问题分解成小问题,然后将小问题的解决方案合并成大问题的解决方案的一种算法。他的思想有点类似于分而治之(Divide-and-Conquer,在递归解法里用的最多的一种思想)。

与分而治之所不同的是,DP使用的是一种自底向上、使用表来记录计算过程,每个子问题都只计算一次,从而避免了多余重复的计算子问题的复杂度。

DP一般被用来解决优化问题(Optimization Problem)。在优化问题里,一般对于问题都会有多个解法,而其中会有一些最优解,我们需要从这些解里面去找出最优解。

DP和其他的一些算法所不同的是,他并没有固定的复杂度,最核心的主要是问题的特征和对于问题的建模过程。对于不同的问题,DP解法会有不同的算法复杂度。

问题特征

能够使用DP解决的问题都具有下面的特征:

  1. 问题能够分解成子问题,而这些子问题具有和原问题相同的结构。通常来说,因为DP都是解决最优解问题,所以一般来说这个最优解都可以分解成一些更小的最优解。一直分解下去,直到最基本的结构,这些结构通常都是我们问题里面本身已经明确说明了的。
  2. 子问题之间有重叠。这个是我们利用DP来解决这种问题的最关键特征,因为DP相对于传统的穷举或者递归解法,对于每个子问题会只解一次,所以在子问题有重复的时候,利用DP可以极大的减少复杂度。

举个比较典型的例子(不过不是优化问题),就是求第n个Fibonacci数。用fib(n)来表示第n个Fibonacci数,那么如果用递归的方式求的话,就是下面的代码(C++):

int fib_rec(int n) { if (n == 1 || n == 0) return 1; else return fib_rec(n - 1) + fib_rec(n - 2); }

如果用自底向上的迭代思路解决,那就是下面的样子(C++):

int fib_iter(int n) { unsigned int f_2 = 1; unsigned int f_1 = 1; unsigned int tmp; if (n < 2) return 1; for (int i = 2; i <= n; i++) { tmp = f_1 + f_2; f_2 = f_1; f_1 = tmp; } return f_1; }

而如果使用DP的思路来解决,就是下面的样子(C++):

int fib_dp(int n) { #define FIB_SIZE 100 static int p = 2; static unsigned int fibnums[FIB_SIZE] = {1, 1}; // initialization if (n >= FIB_SIZE) return -1; if (n < p) { return fibnums[n]; } else { for ( ; p <= n; p++) fibnums[p] = fibnums[p - 2] + fibnums[p - 1]; return fibnums[n]; } }

可以看到这里DP具有一定的空间换时间的味道,不过当然我们可以通过动态内存分配解决这个问题。

当然Fibonacci数列这个例子本身不是很典型,不过他展现了一些自底向上的解决方案对于重叠的子问题有很大的优势。

经典问题

在《算法导论》里面,讲到了几个DP的经典问题:

  1. 生产线调度问题
  2. 矩阵链相乘问题
  3. 两个字符串的最长子串问题(Longest Common Subsequence, LCS)

下面主要说明一下各个问题的主要描述和DP的思路


生产线调度问题

生产一个部件需要N个步骤,而在车间里面共有两条生产线生产这个部件,每个部件都可以再两个条线上进行生产。

当部件进行第k道工序的时候,需要T(i, k)的时间,其中i取1或2,表示是第一还是第二条生产线。

同时,当进行完第k道工序之后,可以切换到另一条生产线进行下一道工序,不过需要切换时间,是C(i, k),其中当i等于2的时候,表示从1切换到2;当i等于1的时候,表示从2切换到1。

当工序开始的时候,放在1线或2线也需要时间,用e1 和e2 表示。

当工序完成的时候,从1线或2线取下来也是需要时间的,用x1 和x2 表示。

有了上面的问题定义,我们考虑一下怎么解这个优化问题。

首先定义f1 (j) ,他表示在1线上完成第j道工序的最快时间。

当j = 1的时候,f1 (j) 只有一个取值就是e1 + T(1, 1)。

当j = 2, 3, ..., n的时候,f1 (j) 什么的取值分为下面两种情况:

  1. f1 (j) = f1 (j - 1) + T(1, j - 1) ,这个时候完成第j道工序的时间就是在同一条生产线里面完成上一道工序的最快时间 加上当前这个工序的所需用时。
  2. f1 (j) = f2 (j - 1) + C(1, j - 1) + T(1, j - 1), 这个时候最快时间就是从第2道工序完成的最快时间 加上转换时间,再加上当前工序的所需用时。

注意上面的两个子问题,都是最快时间,因为如果f1 (j - 1) 不是最快用时的话,那么就存在一个更快的完成j - 1工序的时间,和f的定义矛盾。

这里可以看到最快时间问题可以分解成一个和自身结构相同的子问题,这样就满足了DP的第一个条件。

由此我们定义问题的解为f* = min(f1 (n), f2 (n))

f1 (j) =

  1. e1 + T(1, 1), 当j = 1时。
  2. min(f1 (j - 1) + T(1, j - 1), f2 (j - 1) + C(1, j - 1) + T(1, j - 1) ), 当j = 2, 3, ..., n时。

由此我们如果尝试用递归来写这个程序的话(自顶向下),可以看到如果用r1 (j)来表示f1 (j)的计算次数的话,那么我们从上面的分析得到下面的等式:

r1 (j) = r2 (j) = r1 (j + 1) + r2 (j + 1)

可以证明r1 (1) = 2n - 1 ,也就是所有f的计算时间需要O(2n )。这也同时表明子问题的重叠计算,也就满足了DP的第二个性质。

所以是用递归计算的话过程将会随着n的增大而变得非常慢。

如果我们利用DP的自底向上的思想的话,也就是从f1 (1), f2 (1) 开始计算,迭代到f*来完成的话,那么算法的复杂度则是O(n)。

算法的伪代码如下:

 1 FASTEST-WAY(T, C, e, x, n)
 2     f1[1] = e1 + T(1, 1)
 3     f2[1] = e2 + T(2, 1)
 4     for j in 2 to n
 5         do if f1[j - 1] + T(1, j) <= f2[j - 1] + C(1, j - 1) + T(1, j)
 6             then f1[j] = f1[j - 1] + T(1, j)
 7                  l1[j] = 1
 8             else f1[j] = f2[j - 1] + C(1, j - 1) + T(1, j)
 9                  l1[j] = 2
10            if f2[j - 1] + T(2, j) <= f1[j - 1] + C(2, j - 1) + T(2, j)
11             then f2[j] = f2[j - 1] + T(2, j)
12                  l2[j] = 2
13             else f2[j] = f1[j - 1] + C(2, j - 1) + T(2, j)
14                  l2[j] = 1
15     
16     if f1[n] + x1 <= f2[n] + x2
17       then f* = f1[n] + x1
18            l* = 1
19       else f* = f2[n] + x2
20            l* = 2

矩阵链相乘问题

假设有N个矩阵相乘,由矩阵的结合律我们知道A B C = (A B) C = A (B C),所以这一序列的矩阵相乘我们知道有很多种不同的相乘顺序。

如N1 N2 N3 N4 可以是((N1 N2 )(N3 N4 )),或者是((N1 (N2 N3 )) N4 )这样的顺序。

假设A和B是两个可以相乘的矩阵,那么A就是m x n的一个矩阵,而B就是n x p的一个矩阵,那么计算矩阵相乘就需要mnp那么多个乘法运算。

因为这个原因,如果矩阵运算的时候使用不同的顺序来进行矩阵相乘会有不同的乘法次数要求。

举例来说,现在假设有一个矩阵乘法序列A B C,其中A、B和C分别是10 x 100、100 x 5、5 x 50的矩阵。那么((A B) C)需要的乘法次数是(10 x 100 x 5) + (10 x 5 x 50) = 7500;(A (B C))需要的乘法次数则是(100 x 5 x 50) + (10 x 100 x 50) = 75000。所以是用第一个顺序会比是用第二个顺序快10倍。

有了上面,我们可以计算一下如果给定一个有n个矩阵相乘的矩阵序列,那么我们用P(n)来表示这n个矩阵的括号组合(也就是相乘的顺序)。

P(n)的值有下面两种可能:

  1. P(n) = 1,当n = 1的时候
  2. P(n) = Sum[1, n-1] P(k) P(n-k),当n >= 2时

由证明可以知道P(n)是以2n 作为下限的,所以如果是用枚举来计算组合的话,时间开销会非常大。

下面我们来考虑一下如果对这个优化问题进行DP解。

我们首先用A(i, j),其中i < j,来表示矩阵序列Ai Ai+1 ... Aj 相乘之后得到的矩阵。

计算A(i, j),我们需要首先将Ai Ai+1 ... Aj 从第k个矩阵处分开(k >= i && k < j)。所以求A(i, j)所需要的乘法次数就是求A(i, k)的次数 + A(k+1, j)的次数 + 这两个矩阵相乘的次数。

假设Ai 是一个pi-1 x pi 的矩阵,那么我们用m[i, j]来表示求A(i, j)需要的最少乘法次数。

有上面的分析可以知道,加入最少的乘法次数的实现是在我们将这个矩阵序列从第k个矩阵处分开的话,那么有

m[i, j] = m[i, k] + m[k+1, j] + pi-1 pk pj

这里可以看出原来的问题可以分解成一个和自身性质相同的子问题,所以这符合DP的第一个性质。

从而我们可以有下面的公式:

  1. m[i, j] = 0,如果i = j
  2. m[i, j] = min[i<= k < j] {m[i, k] + m[k+1, j] + pi-1 pk pj },如果i < j

根据这个公式,我们在计算的开始输入一个数组p,里面的元素表示矩阵的维数。那么DP解就有下面的伪代码:

 1 MATRIX-CHAIN-ORDER(p)
 2     n = length[p] - 1
 3     for i in 1 to n
 4         do m[i, i] = 0
 5     for l in 2 to n
 6         do for i in 1 to (n - l + 1)
 7                do j = i + l - 1
 8                   m[i, j] = INFINITY
 9                   for k in i to j - 1
10                       do q = m[i, k] + m[k+1, j] + p(i-1)p(k)p(j)
11                          if q < m[i, j]
12                             then m[i, j] = q
13                                  s[i, j] = k
14     return m and s

其中s记录的是矩阵划分的顺序。


LCS问题

这个问题在我之前的一篇研究diff算法的博文里面曾经提到过,所以在此就不重复了。

动态规划(Dynamic Programming)的一些事一些情相关推荐

  1. 动态规划 dynamic programming

    动态规划dynamic programming June,7, 2015 作者:swanGooseMan 出处:http://www.cnblogs.com/swanGooseMan/p/455658 ...

  2. 运筹学状态转移方程例子_动态规划 Dynamic Programming

    从运筹学和算法的角度综合介绍动态规划 规划论 Mathematical Programming / Mathematical Optimization In mathematics, computer ...

  3. 动态规划|Dynamic Programming

    由于最近课设要用动态规划,翻阅资料学习一下. 动态规划 解决复杂问题的方法,把它们分解成更简单的子问题. 一旦我们看到一些例子,这个定义就有意义了.实际上,我们今天只看解问题的例子 解决DP问题的步骤 ...

  4. 动态规划(Dynamic Programming)与贪心算法(Greedy Algorithm)

    文章目录 动态规划算法(Dynamic Programming) 动态规划问题的属性 应用实例:最长公共子序列问题(Longest Common Subsequence, LCS) 贪心算法(Gree ...

  5. [欠驱动机器人]4,动态规划(Dynamic Programming)

    目录 前言 控制问题变成优化问题 新增成本(Additive cost) 图搜索的最优控制 连续动力学方程 HJB 方程 求出最小控制 数值求解J 方程逼近与数值迭代 线性方程逼近 网格上的值迭代 连 ...

  6. 算法导论-动态规划(dynamic programming)

    动态规划:通过组合子问题的解来解决整个问题. 动态规划的四个步骤: 1)描述最优解的结构: 2)递归定义最优解的值: 3)按自低向上的方式计算最优解的值(首先找到子问题的最优解,解决子问题,最后找到问 ...

  7. 关于简单动态规划(Dynamic Programming)的总结

    Instructions 综上所述(好像没有上)我的DP真的垃圾的一批... 动态规划是用来避免重复计算状态导致效率低的情况,实现动规有记忆化搜索和填表两种方法,但记忆化搜索不能优化空间,所以常用的是 ...

  8. 算法初探-动态规划(Dynamic Programming)

    编程之中接触到很多关于算法的知识,想来整理一番,算是对自己记忆的一个提醒 1.Coin11Problem  问题为:使用最少的三种面值为1,3,5的硬币组合出11元. 重要的是状态以及状态转移方程 d ...

  9. 17.立体匹配——动态规划公式(Dynamic Programming Formulation),二维网格上的相干立体_4

    目录 动态规划公式(Dynamic Programming Formulation) 二维网格上的相干立体 动态规划公式(Dynamic Programming Formulation) 这就是所谓的 ...

最新文章

  1. OpenCV图像序列生成视频,MATLAB图像生成avi视频,image2video。
  2. 暑期集训1:C++STL 例3:UVA-12100
  3. laravel redis_如何将redis优化
  4. mysql日期序列填充_MySQL如何在范围内填充缺失的日期?
  5. Axure5.1不能输入中文问题.
  6. java listview用法_Java ListView.setMultiChoiceModeListener方法代码示例
  7. 不使用for完成一段有空格间隔的字符串,分辨长度大于等于4的单词(求各位高人修改,我表示我是菜鸟,这个算法实在太长了)...
  8. MongoDB CookBook读书笔记之导入导出
  9. pytorch ocr_使用PyTorch解决CAPTCHA(不使用OCR)
  10. 从团购的“占便宜”心态说起
  11. python blp模型 估计_BLP模型
  12. kindle mobi词典格式分析及代码实现
  13. j2ee是什么?(1)
  14. 汽车传感器:自动驾驶“第一步”
  15. verilog增量式编码器
  16. 通过香港招行一卡通收回PayPal资金
  17. mac 打字卡顿的解决方法
  18. 微信公众号服务器交接,微信收款服务商助手交接管理员方法
  19. 如何使用4EVERLAND-BUCKET获得免费云存储空间
  20. python中cls是什么意思_python中的cls到底指的是什么,与self有什么区别?

热门文章

  1. 我总结了70篇论文的方法,帮你透彻理解神经网络的剪枝算法
  2. java GUL编程
  3. python小程序短信发送助手
  4. BCD码-8421码、5421码、2421码、余3码
  5. 神级程序员教你用代码哄好生气的女朋友,网友:我要甜甜的恋爱!
  6. \t\t12时辰养生
  7. 使用百度云主机的GPU主机教程_第二部分
  8. cache 是什么意思 它包括的L1,L2,L3分别是什么东西?
  9. 中文大写数字转换为阿拉伯数字(java)
  10. createJs继承