动态规划Dynamic Programming

1.动态规划理论

1.1动态规划基本思想

 

 

注意:斐波那契递归求解的时间复杂度为O(2n)。python

 

子问题不独立适合动态规划算法设计。git

分治:将原问题划分为互不相交的子问题,递归求解子问题,再将它们的解组合起来。算法

动态规划:子问题重叠的状况,不一样的子问题具备公共的子子问题数组

 

 

 

利用动态规划须要知足:框架

  1. 问题中的状态知足最优性原理。 =》最优子结构
  2. 问题中的状态必须知足无后效性。 =》之前出现的状态和之前状态的变化过程不会影响未来的变化

 

1.2动态规划的基本步骤

 

 2.动态规划例子-矩阵相乘

问题提出:

关键计算问题:

彻底加括号的矩阵连乘积:

 

 

3.举例

1.任务调度安排

                                              

 

 

 

。。。学习

 

 

 

2. 不相邻数字和最大

求一堆不相邻数字和最大,没有要求选两个,能够选多个。优化

 

OPT(n)指的是数据到下标为n的数字选取的最好的方案。网站

 

 

#递归方程实现
def rec_opt(arr, i):
    if i == 0:
        return arr[0]
    elif i == 1:
        return max(arr[0], arr[1])
    else:
        A = rec_opt(arr, i - 2) + arr[i]#选择当前值
        B = rec_opt(arr, i - 1)#不选当前值
        return max(A, B)

#非递归实现:dp动态规划
def dp_opt(arr):
    opt = [0 for i in range(len(arr))]
    opt[0] = arr[0]
    opt[1] = max(arr[0], arr[1])
    for i in range(2,len(arr)):
        A = opt[i-2] + arr[i]
        B = opt[i-1]
        opt[i] = max(A,B)
    return opt[-1]

arr1 = [4, 1, 1, 9, 1]
arr2 = [1, 2, 4, 1, 7, 8, 3]
print(rec_opt(arr1, len(arr1)-1))#13
print(dp_opt(arr1))#13
print(rec_opt(arr2, len(arr2)-1))#15
print(dp_opt(arr2))#15

 

3.给出一个数字和一个数组,判断是否数组中存在一些数的和为给出的数字

 

#递归实现
def rec_subset(arr, i, s):
    if s == 0:
        return True
    elif i == 0:
        return arr[0] == s
    elif arr[i] > s:
        return rec_subset(arr, i-1, s)
    else:
        A = rec_subset(arr, i-1, s-arr[i])
        B = rec_subset(arr, i-1, s)
        return A or B

arr = [3, 34, 4, 12, 5, 2]
print(rec_subset(arr, len(arr)-1, 9))#True
print(rec_subset(arr, len(arr)-1, 10))#True
print(rec_subset(arr, len(arr)-1, 13))#False

 

 

#非递归实现:dq动态规划
def dp_subset(arr, S):
    import numpy as np
    subset = np.zeros((len(arr), S+1), dtype=bool)
    subset[:, 0] = True
    subset[0, :] = False
    subset[0, arr[0]] = True
    for i in range(1, len(arr)):
        for s in range(1, S+1):
            if arr[i] > s:
                subset[i, s] = subset[i-1, s]
            else:
                A = subset[i-1, s-arr[i]]
                B = subset[i-1, s]
                subset[i, s] = A or B
    r, c = subset.shape
    return subset[r-1, c-1]
arr = [3, 34, 4, 12, 5, 2]
print(dp_subset(arr, 9))#True
print(dp_subset(arr, 10))#True
print(dp_subset(arr, 13))#False

 

系统学习

1 背包问题

  假设你是个小偷,背着一个可装4磅东西的背包。你可盗窃的商品有以下3件。spa

  为了让盗窃的商品价值最高,你该选择哪些商品?设计

1.1 简单算法

  最简单的算法以下:尝试各类可能的商品组合,并找出价值最高的组合。

  这样可行,但速度很是慢。在有3件商品的状况下,你须要计算8个不一样的集合;有4件商品时,你须要计算16个集合。每增长一件商品,须要计算的集合数都将翻倍!这种算法的运行时间为O(2n),真的是慢如蜗牛。

  只要商品数量多到必定程度,这种算法就行不通。在第8章,你学习了如何找到近似解,这接近最优解,但可能不是最优解。那么如何找到最优解呢?

1.2 动态规划

  答案是使用动态规划!下面来看看动态规划算法的工做原理。动态规划先解决子问题,再逐步解决大问题。

  对于背包问题,你先解决小背包(子背包)问题,再逐步解决原来的问题。

  动态规划是一个难以理解的概念,若是你没有当即搞懂,也不用担忧,咱们将研究不少示例。

  先来演示这种算法的执行过程。看过执行过程后,你内心将有一大堆问题!我将竭尽所能解答这些问题。

  每一个动态规划算法都从一个网格开始,背包问题的网格以下。

  网格的各行为商品,各列为不一样容量(1~4磅)的背包。全部这些列你都须要,由于它们将帮助你计算子背包的价值。

  网格最初是空的。你将填充其中的每一个单元格,网格填满后,就找到了问题的答案!你必定要跟着作。请你建立网格,咱们一块儿来填满它。

  1. 吉他行
  后面将列出计算这个网格中单元格值的公式。咱们先来一步一步作。首先来看第一行。

  这是吉他行,意味着你将尝试将吉他装入背包。在每一个单元格,都须要作一个简单的决定:偷不偷吉他?别忘了,你要找出一个价值最高的商品集合。

  第一个单元格表示背包的容量为1磅。吉他的重量也是1磅,这意味着它能装入背包!所以这个单元格包含吉他,价值为1500美圆。

  下面来开始填充网格。

  与这个单元格同样,每一个单元格都将包含当前可装入背包的全部商品。

  来看下一个单元格。这个单元格表示背包的容量为2磅,彻底可以装下吉他!

  这行的其余单元格也同样。别忘了,这是第一行,只有吉他可供你选择。换言之,你伪装现在还无法盗窃其余两件商品。

  此时你极可能心存疑惑:原来的问题说的是4磅的背包,咱们为什么要考虑容量为1磅、2磅等的背包呢?前面说过,动态规划从小问题着手,逐步解决大问题。这里解决的子问题将帮助你解决大问题。请接着往下读,稍后你就会明白的。

  此时网格应相似于下面这样。

  别忘了,你要作的是让背包中商品的价值最大。这行表示的是当前的最大价值。它指出,若是你有一个容量4磅的背包,可在其中装入的商品的最大价值为1500美圆。

  2. 音响行
  咱们来填充下一行——音响行。你如今出于第二行,可偷的商品有吉他和音响。在每一行,可偷的商品都为当前行的商品以及以前各行的商品。所以,当前你还不能偷笔记本电脑,而只能偷音响和吉他。咱们先来看第一个单元格,它表示容量为1磅的背包。在此以前,可装入1磅背包的商品的最大价值为1500美圆。

  该不应偷音响呢?
  背包的容量为1磅,能装下音响吗?音响过重了,装不下!因为容量1磅的背包装不下音响,所以最大价值依然是1500美圆。

  接下来的两个单元格的状况与此相同。在这些单元格中,背包的容量分别为2磅和3磅,而之前的最大价值为1500美圆。

  因为这些背包装不下音响,所以最大价值保持不变。
  背包容量为4磅呢?终于可以装下音响了!原来的最大价值为1500美圆,但若是在背包中装入音响而不是吉他,价值将为3000美圆!所以仍是偷音响吧。

  你更新了最大价值!若是背包的容量为4磅,就能装入价值至少3000美圆的商品。在这个网格中,你逐步地更新最大价值。

  3. 笔记本电脑行
  下面以一样的方式处理笔记本电脑。笔记本电脑重3磅,无法将其装入容量为1磅或2磅的背包,所以前两个单元格的最大价值仍是1500美圆。

  对于容量为3磅的背包,原来的最大价值为1500美圆,但如今你可选择盗窃价值2000美圆的笔记本电脑而不是吉他,这样新的最大价值将为2000美圆!

  对于容量为4磅的背包,状况颇有趣。这是很是重要的部分。当前的最大价值为3000美圆,你可不偷音响,而偷笔记本电脑,但它只值2000美圆。

  价值没有原来高。但等一等,笔记本电脑的重量只有3磅,背包还有1磅的容量没用!

  在1磅的容量中,可装入的商品的最大价值是多少呢?你以前计算过。

  根据以前计算的最大价值可知,在1磅的容量中可装入吉他,价值1500美圆。所以,你须要作以下比较。

  你可能始终心存疑惑:为什么计算小背包可装入的商品的最大价值呢?希望你如今明白了其中的缘由!余下了空间时,你可根据这些子问题的答案来肯定余下的空间可装入哪些商品。笔记本电脑和吉他的总价值为3500美圆,所以偷它们是更好的选择。最终的网格相似于下面这样。

  答案以下:将吉他和笔记本电脑装入背包时价值最高,为3500美圆。
  你可能认为,计算最后一个单元格的价值时,我使用了不一样的公式。那是由于填充以前的单元格时,我故意避开了一些复杂的因素。其实,计算每一个单元格的价值时,使用的公式都相同。这个公式以下。

  你可使用这个公式来计算每一个单元格的价值,最终的网格将与前一个网格相同。如今你明白了为什么要求解子问题吧?你能够合并两个子问题的解来获得更大问题的解。

 2 背包问题 FAQ

  你可能仍是以为这像是变魔术。

2.1 再增长一件商品将如何呢

  假设你发现还有第四件商品可偷——一个iPhone!

  此时须要从新执行前面所作的计算吗?不须要。别忘了,动态规划逐步计算最大价值。到目前为止,计算出的最大价值以下。

  这意味着背包容量为4磅时,你最多可偷价值3500美圆的商品。但这是之前的状况,下面再添加表示iPhone的行。  最大价值可能发生变化!请尝试填充这个新增的行,再接着往下读。
  咱们从第一个单元格开始。iPhone可装入容量为1磅的背包。以前的最大价值为1500美圆,但iPhone价值2000美圆,所以该偷iPhone而不是吉他。

  在下一个单元格中,你可装入iPhone和吉他。

  对于第三个单元格,也没有比装入iPhone和吉他更好的选择了。

  对于最后一个单元格,状况比较有趣。当前的最大价值为3500美圆,但你可偷iPhone,这将
余下3磅的容量。
  3磅容量的最大价值为2000美圆!再加上iPhone价值2000美圆,总价值为4000美圆。新的最
大价值诞生了!
  最终的网格以下。

  问题:沿着一列往下走时,最大价值有可能下降吗?

  答案:不可能。每次迭代时,你都存储当前的最大价值。最大价值不可能比之前低!

 

2.2 行的排列顺序发生变化时结果将如何

  答案会随之变化吗?假设你按以下顺序填充各行:音响、笔记本电脑、吉他。网格将会是什么样的?请本身动手填充这个网格,再接着往下读。
  网格将相似于下面这样。

  答案没有变化。也就是说,各行的排列顺序可有可无。

 

2.3 能够逐列而不是逐行填充网格吗

  本身动手试试吧!就这个问题而言,这没有任何影响,但对于其余问题,可能有影响。

 

2.4 增长一件更小的商品将如何呢

  假设你还能够偷一条项链,它重0.5磅,价值1000美圆。前面的网格都假设全部商品的重量为整数,但如今你决定把项链给偷了,所以余下的容量为3.5磅。在3.5磅的容量中,可装入的商品的最大价值是多少呢?不知道!由于你只计算了容量为1磅、2磅、3磅和4磅的背包可装下的商品的最大价值。如今,你须要知道容量为3.5磅的背包可装下的商品的最大价值。
  因为项链的加入,你须要考虑的粒度更细,所以必须调整网格。


2.5 能够偷商品的一部分吗

  假设你在杂货店行窃,可偷成袋的扁豆和大米,但若是整袋装不下,可打开包装,再将背包倒满。在这种状况下,再也不是要么偷要么不偷,而是可偷商品的一部分。如何使用动态规划来处理这种情形呢?
  答案是无法处理。使用动态规划时,要么考虑拿走整件商品,要么考虑不拿,而无法判断该不应拿走商品的一部分。
  但使用贪婪算法可轻松地处理这种状况!首先,尽量多地拿价值最高的商品;若是拿光了,再尽量多地拿价值次高的商品,以此类推。
  例如,假设有以下商品可供选择。

  藜麦比其余商品都值钱,所以要尽可能往背包中装藜麦!若是可以在背包中装满藜麦,结果就是最佳的。
  若是藜麦装完后背包还没满,就接着装入下一种最值钱的商品,以此类推。

 

 2.6 旅游行程最优化

  假设你要去伦敦度假,假期两天,但你想去游览的地方不少。你无法前往每一个地方游览,因此你列个单子。
  对于想去游览的每一个名胜,都列出所需的时间以及你有多想去看看。根据这个清单,你能确定该去游览哪些名胜吗?
  这也是一个背包问题!但约束条件不是背包的容量,而是有限的时间;不是决定该装入哪些商品,而是决定该去游览哪些名胜。请根据这个清单绘制动态规划网格,再接着往下读。
  网格相似于下面这样。
  你画对了吗?请填充这个网格,决定该游览哪些名胜。答案以下。

 

2.7 处理相互依赖的状况

  假设你还想去巴黎,所以在前述清单中又添加了几项。
  去这些地方游览须要很长时间,由于你先得从伦敦前往巴黎,这须要半天时间。若是这3个地方都去玩,是否是要4.5天呢?
  不是的,由于不是去每一个地方都得先从伦敦到巴黎。到达巴黎后,每一个地方都只需1天时间。所以玩这3个地方须要的总时间为3.5天(半天从伦敦到巴黎,每一个地方1天),而不是4.5天。
  将埃菲尔铁塔加入“背包”后,卢浮宫将更“便宜”:只要1天时间,而不是1.5天。如何使用动态规划对这种状况建模呢?
  没办法建模。动态规划功能强大,它可以解决子问题并使用这些答案来解决大问题。但仅当每一个子问题都是离散的,即不依赖于其余子问题时,动态规划才管用。这意味着使用动态规划算法解决不了去巴黎玩的问题。

 2.8 计算最终的解时会涉及两个以上的子背包吗

  为得到前述背包问题的最优解,可能须要偷两件以上的商品。但根据动态规划算法的设计,最多只需合并两个子背包,即根本不会涉及两个以上的子背包。不过这些子背包可能又包含子背包。

 

2.9 最优解可能致使背包没装满吗

  彻底可能。假设你还能够偷一颗钻石。

  这颗钻石很是大,重达3.5磅,价值100万美圆,比其余商品都值钱得多。你绝对应该把它给偷了!但当你这样作时,余下的容量只有0.5磅,别的什么都装不下。

 

 3 最长公共子串

  经过前面的动态规划问题,你获得了哪些启示呢?

  • 动态规划可帮助你在给定约束条件下找到最优解。在背包问题中,你必须在背包容量给定的状况下,偷到价值最高的商品。
  • 在问题可分解为彼此独立且离散的子问题时,就可以使用动态规划来解决。

  要设计出动态规划解决方案可能很难,这正是本节要介绍的。下面是一些通用的小贴士。

  • 每种动态规划解决方案都涉及网格。
  • 单元格中的值一般就是你要优化的值。在前面的背包问题中,单元格的值为商品的价值。
  • 每一个单元格都是一个子问题,所以你应考虑如何将问题分红子问题,这有助于你找出网格的坐标轴  下面再来看一个例子。假设你管理着网站dictionary.com。用户在该网站输入单词时,你须要给出其定义。
      但若是用户拼错了,你必须猜想他本来要输入的是什么单词。例如,Alex想查单词fish,但不当心输入了hish。在你的字典中,根本就没有这样的单词,但有几个相似的单词。

  在这个例子中,只有两个相似的单词,真是过小儿科了。实际上,相似的单词极可能有数千个。
  Alex输入了hish,那他本来要输入的是fish仍是vista呢?

 

 3.1 绘制网格

  用于解决这个问题的网格是什么样的呢?要肯定这一点,你得回答以下问题。

  • 单元格中的值是什么?
  • 如何将这个问题划分为子问题?
  • 网格的坐标轴是什么?

  在动态规划中,你要将某个指标最大化。在这个例子中,你要找出两个单词的最长公共子串。hish和fish都包含的最长子串是什么呢?hish和vista呢?这就是你要计算的值。
  别忘了,单元格中的值一般就是你要优化的值。在这个例子中,这极可能是一个数字:两个字符串都包含的最长子串的长度。
  如何将这个问题划分为子问题呢?你可能须要比较子串:不是比较hish和fish,而是先比较his和fis。每一个单元格都将包含这两个子串的最长公共子串的长度。这也给你提供了线索,让你以为坐标轴极可能是这两个单词。所以,网格可能相似于下面这样。

  若是这在你看来犹如巫术,也不用担忧。这些内容很难懂,但这也正是我到如今才介绍它们的缘由!

 

3.2 填充网格

  如今,你很清楚网格应是什么样的。填充该网格的每一个单元格时,该使用什么样的公式呢?因为你已经知道答案——hish和fish的最长公共子串为ish,所以能够做点弊。
  即使如此,你仍是不能肯定该使用什么样的公式。计算机科学家有时会开玩笑说,那就使用费曼算法(Feynman algorithm)。这个算法是以著名物理学家理查德·费曼命名的,其步骤以下。
  (1) 将问题写下来。
  (2) 好好思考。
  (3) 将答案写下来。
  计算机科学家真是一群不按常理出牌的人啊!
  实际上,根本没有找出计算公式的简单办法,你必须经过尝试才能找出管用的公式。有些算法并不是精确的解决步骤,而只是帮助你理清思路的框架。请尝试为这个问题找到计算单元格值的公式。给你一点提示吧:下面是这个单元格的一部分。

  其余单元格的值呢?别忘了,每一个单元格都是一个子问题的值。为什么单元格(3, 3)的值为2呢?又为什么单元格(3, 4)的值为0呢?
  请找出计算公式,再接着往下读。这样即使你没能找出正确的公式,后面的解释也将容易理解得多。

 

3.3 揭晓答案

  最终的网格以下。


  我使用下面的公式来计算每一个单元格的值。
  实现这个公式的伪代码相似于下面这样。

if word_a[i] == word_b[j]:#两个字母相同
    cell[i][j] = cell[i-1][j-1] + 1
else:#两个字母不一样
    cell[i][j] = 0 

 

  查找单词hish和vista的最长公共子串时,网格以下。

  须要注意的一点是,这个问题的最终答案并不在最后一个单元格中!对于前面的背包问题,最终答案老是在最后的单元格中。但对于最长公共子串问题,答案为网格中最大的数字——它可能并不位于最后的单元格中。
  咱们回到最初的问题:哪一个单词与hish更像?hish和fish的最长公共子串包含三个字母,而hish和vista的最长公共子串包含两个字母。
所以Alex极可能本来要输入的是fish。

 

3.4 最长公共子序列

  假设Alex不当心输入了fosh,他本来想输入的是fish仍是fort呢?
  咱们使用最长公共子串公式来比较它们。
  最长公共子串的长度相同,都包含两个字母!但fosh与fish更像。

  这里比较的是最长公共子串,但其实应比较最长公共子序列:两个单词中都有的序列包含的字母数。如何计算最长公共子序列呢?
  下面是用于计算fish和fosh的最长公共子序列的网格的一部分。

  你能找出填充这个网格时使用的公式吗?最长公共子序列与最长公共子串很像,计算公式也很像。请试着找出这个公式——答案稍后揭晓。

 

3.5 最长公共子序列之解决方案

  最终的网格以下。

 

  下面是填写各个单元格时使用的公式。

  伪代码以下。

if word_a[i] == word_b[j]:#两个字母相同
    cell[i][j] = cell[i-1][j-1] + 1 
else: #两个字母不一样
    cell[i][j] = max(cell[i-1][j], cell[i][j-1]) 

  本章到这里就结束了!它绝对是本书最难理解的一章。动态规划都有哪些实际应用呢?

  • 生物学家根据最长公共序列来肯定DNA链的类似性,进而判断度两种动物或疾病有多相似。最长公共序列还被用来寻找多发性硬化症治疗方案。
  • 你使用过诸如git diff等命令吗?它们指出两个文件的差别,也是使用动态规划实现的。
  • 前面讨论了字符串的类似程度。编辑距离(levenshtein distance)指出了两个字符串的相似程度,也是使用动态规划计算获得的。编辑距离算法的用途不少,从拼写检查到判断用户上传的资料是不是盗版,都在其中。
  • 你使用过诸如Microsoft Word等具备断字功能的应用程序吗?它们如何肯定在什么地方断字以确保行长一致呢?使用动态规划!

 

练习
  请绘制并填充用来计算blue和clues最长公共子串的网格。 

 

4 小结

  • 须要在给定约束条件下优化某种指标时,动态规划颇有用。
  • 问题可分解为离散子问题时,可以使用动态规划来解决。
  • 每种动态规划解决方案都涉及网格。
  • 单元格中的值一般就是你要优化的值。
  • 每一个单元格都是一个子问题,所以你须要考虑如何将问题分解为子问题。
  • 没有放之四海皆准的计算动态规划解决方案的公式。
相关文章
相关标签/搜索