`
hao3100590
  • 浏览: 131483 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

动态规划

阅读更多

该文章转载自:http://www.cppblog.com/Fox/archive/2008/05/07/Dynamic_programming.html

非常感谢!

以前在学习非数值算法的时候,曾经了解过动态规划算法(Dynamic programming),以下是对Wikipedia动态规划的翻译,图也是Wikipedia上的,仓促行文,不到之处,请方家指正。

这篇文章的术语实在是太多了,所以我在文中加入了少量注释,一律以粗斜体注明。

本文的不足之处将随时修正,MIT的《Introduction to Algorithms》第15章是专门讲动态规划的。

_____________________________________________________________

动态规划

在数学与计算机科学领域,动态规划用于解决那些可分解为重复子问题(overlapping subproblems,想想递归求阶乘吧)并具有最优子结构(optimal substructure,想想最短路径算法)(如下所述)的问题,动态规划比通常算法花费更少时间。

上世纪40年代,Richard Bellman最早使用动态规划这一概念表述通过遍历寻找最优决策解问题的求解过程。1953年,Richard Bellman将动态规划赋予现代意义,该领域被IEEE纳入系统分析和工程中。为纪念Bellman的贡献,动态规划的核心方程被命名为贝尔曼方程,该方程以递归形式重申了一个优化问题。

在“动态规划”(dynamic programming)一词中,programming与“计算机编程”(computer programming)中的programming并无关联,而是来自“数学规划”(mathematical programming),也称优化。因此,规划是指对生成活动的优化策略。举个例子,编制一场展览的日程可称为规划。 在此意义上,规划意味着找到一个可行的活动计划。

  • 概述

 

图1 使用最优子结构寻找最短路径:直线表示边,波状线表示两顶点间的最短路径(路径中其他节点未显示);粗线表示从起点到终点的最短路径。

不难看出,start到goal的最短路径由start的相邻节点到goal的最短路径及start到其相邻节点的成本决定。

 

 

最优子结构即可用来寻找整个问题最优解的子问题的最优解。举例来说,寻找上某顶点到终点的最短路径,可先计算该顶点所有相邻顶点至终点的最短路径,然后以此来选择最佳整体路径,如图1所示。

一般而言,最优子结构通过如下三个步骤解决问题:

a) 将问题分解成较小的子问题;

b) 通过递归使用这三个步骤求出子问题的最优解;

c) 使用这些最优解构造初始问题的最优解。

子问题的求解是通过不断划分为更小的子问题实现的,直至我们可以在常数时间内求解。

 

 

图2 Fibonacci序列的子问题示意图:使用有向无环图(DAG, directed acyclic graph)而非表示重复子问题的分解。

为什么是DAG而不是树呢?答案就是,如果是树的话,会有很多重复计算,下面有相关的解释。

 

 

一个问题可划分为重复子问题是指通过相同的子问题可以解决不同的较大问题。例如,在Fibonacci序列中,F3 = F1 + F2和F4 = F2 + F3都包含计算F2。由于计算F5需要计算F3和F4,一个比较笨的计算F5的方法可能会重复计算F2两次甚至两次以上。这一点对所有重复子问题都适用:愚蠢的做法可能会为重复计算已经解决的最优子问题的解而浪费时间。

为避免重复计算,可将已经得到的子问题的解保存起来,当我们要解决相同的子问题时,重用即可。该方法即所谓的缓存(memoization,而不是存储memorization,虽然这个词亦适合,姑且这么叫吧,这个单词太难翻译了,简直就是可意会不可言传,其意义是没计算过则计算,计算过则保存)。当我们确信将不会再需要某一解时,可以将其抛弃,以节省空间。在某些情况下,我们甚至可以提前计算出那些将来会用到的子问题的解。

总括而言,动态规划利用:

1) 重复子问题

2) 最优子结构

3) 缓存

动态规划通常采用以下两种方式中的一种两个办法:

自顶向下:将问题划分为若干子问题,求解这些子问题并保存结果以免重复计算。该方法将递归和缓存结合在一起。

自下而上:先行求解所有可能用到的子问题,然后用其构造更大问题的解。该方法在节省堆栈空间和减少函数调用数量上略有优势,但有时想找出给定问题的所有子问题并不那么直观。

为了提高按名传递(call-by-name,这一机制与按需传递call-by-need相关,复习一下参数传递的各种规则吧,简单说一下,按名传递允许改变实参值)的效率,一些编程语言将函数的返回值“自动”缓存在函数的特定参数集合中。一些语言将这一特性尽可能简化(如SchemeCommon LispPerl),也有一些语言需要进行特殊扩展(如C++,C++中使用的是按值传递和按引用传递,因此C++中本无自动缓存机制,需自行实现,具体实现的一个例子是Automated Memoization in C++)。无论如何,只有指称透明(referentially transparent,指称透明是指在程序中使用表达式、函数本身或以其值替换对程序结果没有任何影响)函数才具有这一特性。

  • 例子

1. Fibonacci序列

寻找Fibonacci序列中第n个数,基于其数学定义的直接实现:

   function fib(n)
       if n = 0 
           return 0
       else if n = 1
           return 1
       return fib(n-1) + fib(n-2)

如果我们调用fib(5),将产生一棵对于同一值重复计算多次的调用树:

  1. fib(5)
  2. fib(4) + fib(3)
  3. (fib(3) + fib(2)) + (fib(2) + fib(1))
  4. ((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
  5. (((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

特别是,fib(2)计算了3次。在更大规模的例子中,还有更多fib的值被重复计算,将消耗指数级时间。

现在,假设我们有一个简单的映射(map)对象m,为每一个计算过的fib及其返回值建立映射,修改上面的函数fib,使用并不断更新m。新的函数将只需O(n)的时间,而非指数时间:

   var m := map(0 → 1, 1 → 1)
   function fib(n)
       if map m does not contain key n
           m[n] := fib(n-1) + fib(n-2)
       return m[n]

这一保存已计算出的数值的技术即被称为缓存,这儿使用的是自顶向下的方法:先将问题划分为若干子问题,然后计算和存储值。

自下而上的方法中,我们先计算较小的fib,然后基于其计算更大的fib。这种方法也只花费线性(O(n))时间,因为它包含一个n-1次的循环。然而,这一方法只需要常数(O(1))的空间,相反,自顶向下的方法则需要O(n)的空间来储存映射关系。

   function fib(n)
       var previousFib := 0, currentFib := 1
       if n = 0 
           return 0
       else if n = 1
           return 1
       repeat n-1 times
           var newFib := previousFib + currentFib
           previousFib := currentFib
           currentFib  := newFib
       return currentFib

在这两个例子,我们都只计算fib(2)一次,然后用它来计算fib(3)和fib(4),而不是每次都重新计算。

2. 一种平衡的0-1矩阵

考虑n*n矩阵的赋值问题:只能赋0和1,n为偶数,使每一行和列均含n/2个0及n/2个1。例如,当n=4时,两种可能的方案是:

+ - - - - +             + - - - - +
| 0 1 0 1 |             | 0 0 1 1 |
| 1 0 1 0 |             | 0 0 1 1 |
| 0 1 0 1 |             | 1 1 0 0 |
| 1 0 1 0 |             | 1 1 0 0 |
+ - - - - +             + - - - - +

问:对于给定n,共有多少种不同的赋值方案。

至少有三种可能的算法来解决这一问题:穷举法(brute force)、回溯法(backtracking)及动态规划(dynamic programming)。穷举法列举所有赋值方案,并逐一找出满足平衡条件的方案。由于共有C(nn/2)^n种方案(在一行中,含n/2个0及n/2个1的组合数为C(n,n/2),相当于从n个位置中选取n/2个位置置0,剩下的自然是1),当n=6时,穷举法就已经几乎不可行了。回溯法先将矩阵中部分元素置为0或1,然后检查每一行和列中未被赋值的元素并赋值,使其满足每一行和列中0和1的数量均为n/2。回溯法比穷举法更加巧妙一些,但仍需遍历所有解才能确定解的数目,可以看到,当n=8时,该题解的数目已经高达116963796250。动态规划则无需遍历所有解便可确定解的数目(意思是划分子问题后,可有效避免若干子问题的重复计算)。

通过动态规划求解该问题出乎意料的简单。考虑每一行恰含n/2个0和n/2个1的k*n(1<=k<=n)的子矩阵,函数f根据每一行的可能的赋值映射为一个向量,每个向量由n个整数对构成。向量每一列对应的一个整数对中的两个整数分别表示该列上该行以下已经放置的0和1的数量。该问题即转化为寻找f((n/2,n/2),(n/2,n/2),...,(n/2,n/2))(具有n个参数或者说是一个含n个元素的向量)的值。其子问题的构造过程如下:

1) 最上面一行(第k行)具有C(nn/2)种赋值;

2) 根据最上面一行中每一列的赋值情况(为0或1),将其对应整数对中相应的元素值减1;

3) 如果任一整数对中的任一元素为负,则该赋值非法,不能成为正确解;

4) 否则,完成对k*n的子矩阵中最上面一行的赋值,取k=k-1,计算剩余的(k-1)*n的子矩阵的赋值;

5) 基本情况是一个1*n的细小的子问题,此时,该子问题的解的数量为0或1,取决于其向量是否是n/2个(0, 1)和n/2个(1, 0)的排列。

例如,在上面给出的两种方案中,向量序列为:

((2, 2) (2, 2) (2, 2) (2, 2))       ((2, 2) (2, 2) (2, 2) (2, 2))     k = 4
  0      1      0      1              0       0       1      1

((1, 2) (2, 1) (1, 2) (2, 1))       ((1, 2) (1, 2) (2, 1) (2, 1))     k = 3
  1      0      1      0              0      0      1      1

((1, 1) (1, 1) (1, 1) (1, 1))       ((0, 2) (0, 2) (2, 0) (2, 0))     k = 2
  0      1      0      1              1      1      0      0

((0, 1) (1, 0) (0, 1) (1, 0))       ((0, 1) (0, 1) (1, 0) (1, 0))     k = 1
  1      0      1      0              1      1      0      0

((0, 0) (0, 0) (0, 0) (0, 0))       ((0, 0) (0, 0), (0, 0) (0, 0))

动态规划在此的意义在于避免了相同f的重复计算,更进一步的,上面着色的两个f,虽然对应向量不同,但f的值是相同的,想想为什么吧:D

该问题解的数量(序列a058527在OEIS)是1, 2, 90, 297200, 116963796250, 6736218287430460752, ...

下面的外部链接中包含回溯法的Perl源代码实现,以及动态规划法的MAPLE和C语言的实现。

3. 棋盘

考虑n*n的棋盘及成本函数C(i,j),该函数返回方格(i,j)相关的成本。以5*5的棋盘为例:

5 | 6 7 4 7 8
4 | 7 6 1 1 4
3 | 3 5 7 8 2
2 | 2 6 7 0 2
1 | 7 3 5 6 1
- + - - - - -
  | 1 2 3 4 5

可以看到:C(1,3)=5

从棋盘的任一方格的第一阶(即行)开始,寻找到达最后一阶的最短路径(使所有经过的方格的成本之和最小),假定只允许向左对角、右对角或垂直移动一格。

5 |
4 |
3 |
2 |   x x x
1 |     o
- + - - - - -
  | 1 2 3 4 5

该问题展示了最优子结构。即整个问题的全局解依赖于子问题的解。定义函数q(i,j),令:q(i,j)表示到达方格(i,j)的最低成本。

如果我们可以求出第n阶所有方格的q(i,j)值,取其最小值并逆向该路径即可得到最短路径。

记q(i,j)为方格(i,j)至其下三个方格((i-1,j-1)、(i-1,j)、(i-1,j+1))最低成本与c(i,j)之和,例如:

5 |
4 |     A
3 |   B C D
2 |
1 |
- + - - - - -
  | 1 2 3 4 5

q(A) = min(q(B),q(C),q(D)) + c(A)

定义q(i,j)的一般形式:

            |-  inf.                                                  j<1 or j>n
q(i,j) = -+-  c(i,j)                                                i=1
            |-  min(q(i-1,j-1),q(i-1,j),q(i-1,j+1))+c(i,j)   otherwise.

方程的第一行是为了保证递归可以退出(处理边界时只需调用一次递归函数)。第二行是第一阶的取值,作为计算的起点。第三行的递归是算法的重要组成部分,与例子ABCD类似。从该定义我们可以直接给出计算q(i,j)的简单的递归代码。在下面的伪代码中,n表示棋盘的维数,C(i,j)是成本函数,min()返回一组数的最小值:

function minCost(i, j)
    if j < 1 or j > n
        return infinity
    else if i = 1
        return c(i,j)
    else
        return min(minCost(i-1,j-1),minCost(i-1,j),minCost(i-1,j+1))+c(i,j)

需要指出的是,minCost只计算路径成本,并不是最终的实际路径,二者相去不远。与Fibonacci数相似,由于花费大量时间重复计算相同的最短路径,这一方式慢的恐怖。不过,如果采用自下而上法,使用二维数组q[i,j]代替函数minCost,将使计算过程快得多。我们为什么要这样做呢?选择保存值显然比使用函数重复计算相同路径要简单的多。

我们还需要知道实际路径。路径问题,我们可以通过另一个前任数组p[i,j]解决。这个数组用于描述路径,代码如下:

function computeShortestPathArrays()
     for x from 1 to n
         q[1, x] := c(1, x)
     for y from 1 to n
         q[y, 0]     := infinity
         q[y, n + 1] := infinity
     for y from 2 to n
         for x from 1 to n
             m := min(q[y-1, x-1], q[y-1, x], q[y-1, x+1])
             q[y, x] := m + c(y, x)
             if m = q[y-1, x-1]
                 p[y, x] := -1
             else if m = q[y-1, x]
                 p[y, x] :=  0
             else
                 p[y, x] :=  1

剩下的求最小值和输出就比较简单了:

function computeShortestPath()
     computeShortestPathArrays()
     minIndex := 1
     min := q[n, 1] 
     for i from 2 to n 
         if q[n, i] < min
             minIndex := i
             min := q[n, i]
     printPath(n, minIndex)

function printPath(y, x)
     print(x)
     print("<-")
     if y = 2
         print(x + p[y, x])
     else
         printPath(y-1, x + p[y, x])

4. 序列比对

序列比对是动态规划的一个重要应用。序列比对问题通常是使用编辑操作(替换、插入、删除一个要素等)进行序列转换。每次操作对应不同成本,目标是找到编辑序列的最低成本。

可以很自然地想到使用递归解决这个问题,序列AB的最优编辑通过以下措施之一实现:

插入B的第一个字符,对AB的剩余序列进行最优比对;

删去A的第一个字符,对AB进行最优比对;

B的第一个字符替换A的第一个字符,对A的剩余序列和B进行最优比对。

局部比对可在矩阵中列表表示,单元(i,j)表示A[1..i]到b[1..j]最优比对的成本。单元(i,j)的成本计算可通过累加相邻单元的操作成本并选择最优解实现。至于序列比对的不同实现算法,参见Smith-WatermanNeedleman-Wunsch

对序列比对的话题并不熟悉,更多的话也无从谈起,有熟悉的朋友倒是可以介绍一下。

  • 应用动态规划的算法

1) 许多字符串操作算法如最长公共子列最长递增子列最长公共字串

2) 将动态规划用于的树分解,可以有效解决有界树宽图生成树等许多与图相关的算法问题;

3) 决定是否及如何可以通过某一特定上下文无关文法产生给定字符串的Cocke-Younger-Kasami (CYK)算法;

4) 计算机国际象棋转换表驳斥表的使用;

5) Viterbi算法(用于隐式马尔可夫模型);

6) Earley算法(一类图表分析器);

7) Needleman-Wunsch及其他生物信息学中使用的算法,包括序列比对结构比对RNA结构预测;

8) Levenshtein距离(编辑距离);

9) 弗洛伊德最短路径算法;

10) 连锁矩阵乘法次序优化;

11) 子集求和背包问题分治问题的伪多项式时间算法;

12) 计算两个时间序列全局距离的动态时间规整算法;

13) 关系型数据库的查询优化的Selinger(又名System R)算法;

14) 评价B样条曲线的De Boor算法

15) 用于解决板球运动中断问题的Duckworth-Lewis方法;

16) 价值迭代法求解马尔可夫决策过程

17) 一些图形图像边缘以下的选择方法,如“磁铁”选择工具在Photoshop

18) 间隔调度

19) 自动换行

20) 巡回旅行商问题又称邮差问题或货担郎问题);

21) 分段最小二乘法

22) 音乐信息检索跟踪。

对于这些算法应用,大多未曾接触,甚至术语翻译的都有问题,鉴于本文主要在于介绍动态规划,所以仓促之中,未及查证。

  • 相关

1) 贝尔曼方程

2) 马尔可夫决策过程

3) 贪心算法

  • 参考
  • Adda, Jerome, and Cooper, Russell, 2003. Dynamic Economics. MIT Press. An accessible introduction to dynamic programming in economics. The link contains sample programs.
  • Richard Bellman, 1957, Dynamic Programming, Princeton University Press. Dover paperback edition (2003), ISBN 0486428095.
  • Bertsekas, D. P., 2000. Dynamic Programming and Optimal Control, Vols. 1 & 2, 2nd ed. Athena Scientific. ISBN 1-886529-09-4.
  • Thomas H. CormenCharles E. LeisersonRonald L. Rivest, and Clifford Stein, 2001. Introduction to Algorithms, 2nd ed. MIT Press & McGraw-Hill. ISBN 0-262-03293-7. Especially pp. 323–69.
  • Giegerich, R., Meyer, C., and Steffen, P., 2004, "A Discipline of Dynamic Programming over Sequence Data,Science of Computer Programming 51: 215-263.
  • Nancy Stokey, and Robert E. Lucas, with Edward Prescott, 1989. Recursive Methods in Economic Dynamics. Harvard Univ. Press.
  • S. P. Meyn, 2007. Control Techniques for Complex Networks, Cambridge University Press, 2007.
    • 外部链接

    _____________________________________________________________

    关于动态规划,这只是一篇译文,后面将根据实际问题具体写点动态规划的应用。

  • 分享到:
    评论

    相关推荐

      代码 随机动态规划的实例的matlab代码

      代码 随机动态规划的实例的matlab代码代码 随机动态规划的实例的matlab代码代码 随机动态规划的实例的matlab代码代码 随机动态规划的实例的matlab代码代码 随机动态规划的实例的matlab代码代码 随机动态规划的实例的...

      浅谈动态规划的几种优化方法

      动态规划是求解最优化问题的一种方法;动态规划虽然空间复杂度一般较大,但时间效率可观。但是,动态规划在求解中也会存在一些不必要、或者重复求解的子问题,这时就需要进行进一步优化。 在NOI及省选赛场上,一般...

      基于LINGO的优化问题动态规划法求解

      【基于LINGO的优化问题动态规划法求解】 在运筹优化领域,LINGO是一款强大的数学建模软件,尤其适用于解决各种线性、非线性以及动态规划问题。不同于传统方法,LINGO甚至可以在没有明确目标函数的情况下解决动态...

      动态规划实验报告

      本实验报告主要探讨了使用动态规划算法解决计算二项式系数的问题。实验者刘春云,专业为软件工程,通过这个实验学习并应用了动态规划这一核心的计算机科学概念。实验指导教师为赵晓平,实验时间为2012年2月21日。 *...

      动态规划算法经典题目

      动态规划算法经典题目分析 动态规划是一种非常经典的算法思想,解决的问题领域非常广泛。动态规划的基本思想是将一个复杂的问题分解成多个小问题,通过解决这些小问题来解决整个问题。今天,我们将要探讨动态规划的...

      动态规划的特点及其应用

      动态规划是信息学竞赛中的常见算法,本文的主要内容就是分析 它的特点。 文章的第一部分首先探究了动态规划的本质,因为动态规划的特 点是由它的本质所决定的。第二部分从动态规划的设计和实现这两个 角度分析了动态...

      ACM动态规划经典题

      动态规划是一种重要的算法思想,广泛应用于计算机科学,特别是在解决最优化问题时,如图论、组合优化、机器学习和自然语言处理等领域。在ACM(国际大学生程序设计竞赛)中,动态规划也是常考的题型,因为它能够帮助...

      会议安排(贪心算法和动态规划) 贪心算法和动态规划.pdf

      会议安排(贪心算法和动态规划) 会议安排问题是计算机科学中的一种经典问题,目的是在一系列活动中选择尽可能多的活动,使得每个活动的结束时间不早于下一个活动的开始时间。这个问题可以使用贪心算法和动态规划两...

      贪心算法、分治算法和动态规划的区别 贪心算法和动态规划.pdf

      贪心算法、分治算法和动态规划的区别 贪心算法、分治算法和动态规划是三种常用的算法设计策略,每种算法都有其特点和应用场景。下面我们将对这三种算法进行详细的比较和分析。 分治算法 分治算法是一种将原问题...

      贪心算法和动态规划以及分治法的区别? (1) 贪心算法和动态规划.pdf

      贪心算法、动态规划和分治法的区别 贪心算法是局部最优解的算法,它通过从上往下,从顶部一步一步最优,得到最后的结果。贪心算法顾名思义,就是做出在当前看来是最好的结果,它不从整体上加以考虑,也就是局部最优...

      动态规划解决TSP问题用动态规划算法求解TSP,数据为Solomon数据集的c101文件读取,可视化路径图,用图展示每次迭代的最

      动态规划是一种解决问题的有效方法,尤其在处理具有重叠子问题和最优子结构的复杂问题时。旅行商问题(Traveling Salesman Problem, TSP)是一个经典的组合优化问题,它的目标是找到一个城市的最短可能路线,使得...

      什么是动态规划(Dynamic Programming)?动态规划的意义是什么? - 知乎1

      动态规划(Dynamic Programming,简称DP)是一种用于解决最优化问题的算法思想,它通过将大问题分解为相互重叠的子问题,并存储子问题的解,以避免重复计算,从而达到高效求解的目的。DP的核心在于它能够识别并利用...

      算法动态规划总结(拓展篇)

      从给定的文件标题“算法动态规划总结(拓展篇)”和描述“算法动态规划拓展总结,内含详细解说和代码”中,我们可以提炼出一系列关于动态规划算法的知识点,这些知识点涵盖了动态规划的多种应用及其优化技术。...

      动态规划C语言矩阵连乘

      "动态规划C语言矩阵连乘" 动态规划是一种非常重要的算法思想,它可以解决很多 Optimization 问题。在这个资源中,我们将学习如何使用动态规划来解决矩阵连乘问题。 动态规划的基本思想是将待解决的问题分解成若干...

      dynprogs_MATLAB动态规划函数及测试程序_多阶段决策_多阶段规划_together1rz_

      标题中的“dynprogs_MATLAB动态规划函数及测试程序_多阶段决策_多阶段规划_together1rz_”表明这是一个关于使用MATLAB实现动态规划算法的资源包,主要用于解决涉及多阶段决策和规划的问题。动态规划是一种在数学、...

      动态规划 增量动态规划 水库优化调度 程序代码

      动态规划增量动态规划水库优化调度程序代码 动态规划是一种常用的优化算法,它可以解决很多复杂的优化问题。增量动态规划是动态规划的一种变体,它可以更好地解决一些特殊的优化问题。在水库优化调度中,动态规划和...

      动态规划动态规划.zip

      动态规划(Dynamic Programming,简称DP)是解决复杂问题的有效算法设计方法,尤其在计算机科学和IT领域中占有重要地位。它通过将一个大问题分解为若干个子问题,并利用子问题的解来构建原问题的解,从而避免了重复...

      运用LINGO解决某些动态规划的问题

      本篇文章将深入探讨如何运用LINGO来解决动态规划问题,通过具体的案例分析,我们将了解LINGO在动态规划中的应用方法和步骤。 ### 动态规划与LINGO 动态规划是一种用于解决多阶段决策过程中的最优化问题的方法,其...

    Global site tag (gtag.js) - Google Analytics