動態(tài)規(guī)劃

動態(tài)規(guī)劃(Dynamic Programming)

本文包括:

  1. 動態(tài)規(guī)劃定義
  2. 狀態(tài)轉(zhuǎn)移方程
  3. 動態(tài)規(guī)劃算法步驟
  4. 最長非降子序列(LIS)
  5. 最大乘積子串
  6. Unique Paths
  7. Unique Paths II
  8. Minimum Path Sum
  9. Triangle
  10. 最長公共自序列(LCS)
  11. 編輯距離
  12. 交替字符串
  13. 矩陣鏈乘積

前文引自:http://www.hawstein.com/posts/dp-novice-to-advanced.html

1、 什么是動態(tài)規(guī)劃,我們要如何描述它?

  • 動態(tài)規(guī)劃算法通常基于一個遞推公式及一個或多個初始狀態(tài)。 當(dāng)前子問題的解將由上一次子問題的解推出。使用動態(tài)規(guī)劃來解題只需要多項式時間復(fù)雜度, 因此它比回溯法、暴力法等要快許多。

  • 首先,我們要找到某個狀態(tài)的最優(yōu)解,然后在它的幫助下,找到下一個狀態(tài)的最優(yōu)解。

2、“狀態(tài)”代表什么及如何找到它?

  • “狀態(tài)”用來描述該問題的子問題的解。原文中有兩段作者闡述得不太清楚,跳過直接上例子。

  • 如果我們有面值為1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元? (表面上這道題可以用貪心算法,但貪心算法無法保證可以求出解,比如1元換成2元的時候)

  • 首先我們思考一個問題,如何用最少的硬幣湊夠i元(i<11)?為什么要這么問呢? 兩個原因:1.當(dāng)我們遇到一個大問題時,總是習(xí)慣把問題的規(guī)模變小,這樣便于分析討論。 2.這個規(guī)模變小后的問題和原來的問題是同質(zhì)的,除了規(guī)模變小,其它的都是一樣的, 本質(zhì)上它還是同一個問題(規(guī)模變小后的問題其實(shí)是原問題的子問題)。

  • 好了,讓我們從最小的i開始吧。當(dāng)i=0,即我們需要多少個硬幣來湊夠0元。 由于1,3,5都大于0,即沒有比0小的幣值,因此湊夠0元我們最少需要0個硬幣。 (這個分析很傻是不是?別著急,這個思路有利于我們理清動態(tài)規(guī)劃究竟在做些什么。) 這時候我們發(fā)現(xiàn)用一個標(biāo)記來表示這句“湊夠0元我們最少需要0個硬幣。”會比較方便, 如果一直用純文字來表述,不出一會兒你就會覺得很繞了。

  • 那么, 我們用d(i)=j來表示湊夠i元最少需要j個硬幣。于是我們已經(jīng)得到了d(0)=0, 表示湊夠0元最小需要0個硬幣。當(dāng)i=1時,只有面值為1元的硬幣可用, 因此我們拿起一個面值為1的硬幣,接下來只需要湊夠0元即可,而這個是已經(jīng)知道答案的, 即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。當(dāng)i=2時, 仍然只有面值為1的硬幣可用,于是我拿起一個面值為1的硬幣, 接下來我只需要再湊夠2-1=1元即可(記得要用最小的硬幣數(shù)量),而這個答案也已經(jīng)知道了。 所以d(2)=d(2-1)+1=d(1)+1=1+1=2。

  • 一直到這里,你都可能會覺得,好無聊, 感覺像做小學(xué)生的題目似的。因?yàn)槲覀円恢倍贾荒懿僮髅嬷禐?的硬幣!耐心點(diǎn), 讓我們看看i=3時的情況。當(dāng)i=3時,我們能用的硬幣就有兩種了:1元的和3元的( 5元的仍然沒用,因?yàn)槟阈枰獪惖臄?shù)目是3元!5元太多了親)。 既然能用的硬幣有兩種,我就有兩種方案。如果我拿了一個1元的硬幣,我的目標(biāo)就變?yōu)榱耍?湊夠3-1=2元需要的最少硬幣數(shù)量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。 這個方案說的是,我拿3個1元的硬幣;第二種方案是我拿起一個3元的硬幣, 我的目標(biāo)就變成:湊夠3-3=0元需要的最少硬幣數(shù)量。即d(3)=d(3-3)+1=d(0)+1=0+1=1. 這個方案說的是,我拿1個3元的硬幣。

  • 好了,這兩種方案哪種更優(yōu)呢? 記得我們可是要用最少的硬幣數(shù)量來湊夠3元的。所以, 選擇d(3)=1,怎么來的呢?具體是這樣得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。

  • 從以上的文字中, 我們要抽出動態(tài)規(guī)劃里非常重要的兩個概念:狀態(tài)和狀態(tài)轉(zhuǎn)移方程。

  • 上文中d(i)表示湊夠i元需要的最少硬幣數(shù)量,我們將它定義為該問題的”狀態(tài)”, 這個狀態(tài)是怎么找出來的呢?我在另一篇文章 動態(tài)規(guī)劃之背包問題(一)中寫過: 根據(jù)子問題定義狀態(tài)。你找到子問題,狀態(tài)也就浮出水面了。 最終我們要求解的問題,可以用這個狀態(tài)來表示:d(11),即湊夠11元最少需要多少個硬幣。

  • 那狀態(tài)轉(zhuǎn)移方程是什么呢?既然我們用d(i)表示狀態(tài),那么狀態(tài)轉(zhuǎn)移方程自然包含d(i), 上文中包含狀態(tài)d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。沒錯, 它就是狀態(tài)轉(zhuǎn)移方程,描述狀態(tài)之間是如何轉(zhuǎn)移的。當(dāng)然,我們要對它抽象一下,

      d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j個硬幣的面值;
    
  • 偽代碼:

  • 依次計算,可以計算出11元至少要3枚硬幣。

3、DP 算法步驟

  • 動態(tài)規(guī)劃算法一般用來求解最優(yōu)化問題,當(dāng)問題有很多可行解,而題目要求尋找這些解當(dāng)中的“最大值”/“最小值”時,通常可以采用DP。

  • 動態(tài)規(guī)劃算法與分治法相似,都是通過組合子問題的解來求解原問題。所不同的是,動態(tài)規(guī)劃應(yīng)用于子問題重疊的情況,在遞歸求解子問題的時候,一些子子問題可能是相同的,這種情況下,分治法會反復(fù)地計算同樣的子問題,而動態(tài)規(guī)劃對于相同的子問題只計算一次。

  • 動態(tài)規(guī)劃算法的設(shè)計步驟:

      1、刻畫最優(yōu)解的結(jié)構(gòu)特征(尋找最優(yōu)子結(jié)構(gòu))
      2、遞歸地定義最優(yōu)解的值(確定遞歸公式,動態(tài)規(guī)劃法的重點(diǎn)就是這個)
      3、計算最優(yōu)解的值(有兩種方法:帶備忘錄自頂向下法、自底向上法)
      4、利用計算出的信息構(gòu)造一個最優(yōu)解(通常是將具體的最優(yōu)解輸出)
    

最優(yōu)子結(jié)構(gòu):問題的最優(yōu)解包含的子問題的解相對于子問題而言也是最優(yōu)的。
并非所有組合優(yōu)化問題都具有最優(yōu)子結(jié)構(gòu)特性。

4、最長非降子序列(LIS)

  • 問題描述:

    一個序列有N個數(shù):A[1],A[2],…,A[N],求出最長非降子序列的長度。
    (講DP基本都會講到的一個問題LIS:longest increasing subsequence)

  • 思路:

    假如我們考慮求A[1],A[2],…,A[i]的最長非降子序列的長度,其中i<N, 那么上面的問題變成了原問題的一個子問題(問題規(guī)模變小了,你可以讓i=1,2,3等來分析)

    然后我們定義d(i),表示前i個數(shù)中以A[i]結(jié)尾的最長非降子序列的長度。

    如果我們把d(1)到d(N)都計算出來,那么最終我們要找的答案就是這里面最大的那個。

    OK,狀態(tài)找到了,下一步找出狀態(tài)轉(zhuǎn)移方程。

  • 可以舉個例子,然后求出前面幾項,找出遞推規(guī)律

    d(i)可以用下面的狀態(tài)轉(zhuǎn)移方程得到:

      d(i) = max{1, d(j)+1},其中j<i,A[j]<=A[i]
    
  • Python 代碼:

      def LIS(A):
          d = [0 for i in range(len(A))]
          for i in range(len(A)):
              d[i] = 1   # 就算前面所有元素都比當(dāng)前大,那么至少可以包含自身,所以長度默認(rèn)值為1
              for j in range(i):
                  if A[i] >= A[j] and d[j]+1 > d[i]:
                      d[i] = d[j] + 1
          return max(d)
    
      A = [5, 3, 4, 8, 6, 7]
      print(LIS(A))
    

另一個解法:利用LCS思想(第9點(diǎn))

  • 構(gòu)造一個新序列,B,它是對A進(jìn)行降序排列而得。

  • 此時求最長非增子序列,調(diào)用LCS-LENGTH即可求出A與B的最長公共子序列,這個序列就是最后的解。

5、最大乘積子串

  • 題目描述:

    給一個浮點(diǎn)數(shù)序列,取最大乘積連續(xù)子串的值,例如 -2.5,4,0,3,0.5,8,-1,則取出的最大乘積連續(xù)子串為3,0.5,8。
    也就是說,上述數(shù)組中,3 0.5 8這3個數(shù)的乘積30.58=12是最大的,而且是連續(xù)的。

  • 思路:

    因?yàn)橛姓胸?fù),下次要處理的 a[i] 可能是負(fù),所以每次都要存儲最小的子串乘積值,負(fù)負(fù)得正,得出的結(jié)果有可能還更大。

  • 遞推方程:

      maxend = max(max(maxend * a[i], minend * a[i]), a[i]);
      minend = min(min(maxend * a[i], minend * a[i]), a[i]);
    
  • Python 代碼:

      def maxProductSubstring(a):
          maxend, minend, maxresult = a[0], a[0], a[0]
          for i in range(1,len(A)):
              maxend = max(max(maxend * a[i], minend * a[i]), a[i])
              minend = min(min(maxend * a[i], minend * a[i]), a[i])
              maxresult = max(maxend, maxresult)
          return maxresult
    
      A = [-2.5, 4, 0, 3, 0.5, 8, -1]
      print(maxProductSubstring(A))
    

6、Unique Paths

  • Leetcode:62. Unique Paths

    A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

    The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

    How many possible unique paths are there?

  • Solution:

      class Solution(object):
          def uniquePaths(self, m, n):
              """
              :type m: int
              :type n: int
              :rtype: int
              """
              c = [[0 for i in range(n+1)] for i in range(m+1)]
              for i in range(n+1):
                  c[0][i] = 0
              for i in range(m+1):
                  c[i][0] = 0
              for i in range(1, m+1):
                  for j in range(1, n+1):
                      if i == 1 and j == 1:
                          c[i][j] = 1
                      else:
                          c[i][j] = c[i-1][j] + c[i][j-1]
              return c[i][j]
    
      sol = Solution()
      print(sol.uniquePaths(1, 1))
    

7、Unique Paths II

  • Leetcode:63. Unique Paths II

    Follow up for "Unique Paths":

    Now consider if some obstacles are added to the grids. How many unique paths would there be?

    An obstacle and empty space is marked as 1 and 0 respectively in the grid.

    For example,
    There is one obstacle in the middle of a 3x3 grid as illustrated below.

    [
    [0,0,0],
    [0,1,0],
    [0,0,0]
    ]

    The total number of unique paths is 2.

    Note: m and n will be at most 100.

  • solution:

      class Solution(object):
          def uniquePathsWithObstacles(self, obstacleGrid):
              """
              :type obstacleGrid: List[List[int]]
              :rtype: int
              """
    
              m = len(obstacleGrid)
              n = len(obstacleGrid[0])
              dp = [[0 for i in range(n+1)] for i in range(m+1)]
              dp[1][1] = 1
              for i in range(1, m+1):
                  for j in range(1, n+1):
                      if obstacleGrid[i-1][j-1] == 1:
                          dp[i][j] = 0
                      elif i == 1 and j == 1:
                          dp[1][1] = 1
                      else:
                          dp[i][j] = dp[i-1][j] + dp[i][j-1]
    
              return dp[m][n]
    
      sol = Solution
      obstacleGrid = [
          [0, 0, 0],
          [0, 1, 0],
          [0, 0, 0]
      ]
      print(sol.uniquePathsWithObstacles(sol, obstacleGrid))
    

8、Minimum Path Sum

  • Leetcode:64. Minimum Path Sum

    Given a m x n grid filled with non-negative numbers,

    find a path from top left to bottom right which minimizes the sum of all numbers along its path.

    Note: You can only move either down or right at any point in time.

  • solution:

      class Solution(object):
          def minPathSum(self, grid):
              """
              :type grid: List[List[int]]
              :rtype: int
              """
              m = len(grid)
              if m > 0 and len(grid[0]) > 0:
                  n = len(grid[0])
              else:
                  return 0
    
              dp = [[0 for i in range(n+1)] for i in range(m+1)]
              for i in range(m+1):
                  dp[i][0] = float('inf')
              for i in range(n+1):
                  dp[0][i] = float('inf')
              dp[1][1] = grid[0][0]
              for i in range(1, m+1):
                  for j in range(1, n+1):
                      if i == 1 and j == 1:
                          dp[i][j] = grid[i-1][j-1]
                      elif dp[i-1][j] < dp[i][j-1]:
                          dp[i][j] = dp[i-1][j] + grid[i-1][j-1]
                      else:
                          dp[i][j] = dp[i][j-1] + grid[i-1][j-1]
              return dp[m][n]
    

9、Triangle

  • Leetcode:120. Triangle

    Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.

    For example, given the following triangle

      [
             [2],
            [3,4],
           [6,5,7],
          [4,1,8,3]
      ]
    

    The minimum path sum from top to bottom is 11 (i.e., 2 + 3 + 5 + 1 = 11).

    Note:
    Bonus point if you are able to do this using only O(n) extra space, where n is the total number of rows in the triangle.

  • 第一次嘗試:

      class Solution(object):
          def minimumTotal(self, triangle):
              """
              :type triangle: List[List[int]]
              :rtype: int
              """
              length = len(triangle)
              minSum = triangle[0][0]
              index = 0
              for i in range(1, length):
                  if triangle[i][index] < triangle[i][index+1]:
                      minSum += triangle[i][index]
                  else:
                      minSum += triangle[i][index+1]
                      index += 1
              return minSum
    

    解釋:
    有點(diǎn)類似于貪心,每一步都尋找最優(yōu)的,但是卻沒想到,全局最優(yōu)可能是另外一種情況,比如下面的測試用例:

      Submission Result: Wrong Answer More Details 
    
      Input:
      [[-1],[2,3],[1,-1,-3]]
      Output:
      0
      Expected:
      -1
    

    很明顯,按照我的程序,走法是:-1 2 -1 = 0。
    但是其實(shí)最優(yōu)解是 -1 3 -3 = -1。

  • 需要輔助空間 O(n^2) 的 DP 解法:

      '''
          extra space: O(n^2)
          using 'top to bottom' thought
      '''
      def minimumTotal(self, triangle):
          """
          :type triangle: List[List[int]]
          :rtype: int
          """
          length = len(triangle)
          dp = [[0 for i in range(j+1)] for j in range(length)] # dp[i][j] means element of row i column j is the last one of the min_path
          dp[0][0] = triangle[0][0]
          for i in range(1, length):
              for j in range(i+1):
                  if j == 0:
                      dp[i][j] = dp[i-1][j] + triangle[i][j]
                  elif j == i:
                      dp[i][j] = dp[i-1][j-1] + triangle[i][j]
                  elif dp[i-1][j] < dp[i-1][j-1]:
                      dp[i][j] = dp[i-1][j] + triangle[i][j]
                  else:
                      dp[i][j] = dp[i-1][j-1] + triangle[i][j]
          return min(dp[length-1])
    

    理解不難,同樣是構(gòu)建二維數(shù)組,dp[i][j] 的意思是以 triangle[i][j] 結(jié)尾的最小路徑和。這其實(shí)就是帶備忘錄的自頂向下辦法。

  • 需要輔助空間 O(n) 的 DP 解法(推薦!):

      '''
          extra space: O(n)
          using 'bottom to top' thought
      '''
      def minimumTotal_bottomtotop(self, triangle):
          """
          :type triangle: List[List[int]]
          :rtype: int
          """
          length = len(triangle)
          dp = triangle[length-1]
          for i in range(length-2, -1, -1):
              for j in range(i+1):
                  dp[j] = min(dp[j], dp[j+1]) + triangle[i][j]
          return dp[0]
    

    這種辦法是從底往上,dp 只是一維數(shù)組,在每層操作時,僅記錄該層的各元素結(jié)尾的最小路徑和,并且路徑是從底往上。在處理不同層時,dp 根據(jù)下一層的計算結(jié)果算出當(dāng)前層的值,所以一直在變。輔助空間只有 O(n)。

10、最長公共子序列(LCS)

  • 最長公共子序列(Longest Common Subsequence)是一個在一個序列集合中(通常為兩個序列)用來查找所有序列中最長子序列的問題。這與查找最長公共子串的問題不同的地方是:子序列不需要在原序列中占用連續(xù)的位置 。最長公共子序列問題是一個經(jīng)典的計算機(jī)科學(xué)問題,也是數(shù)據(jù)比較程序,比如Diff工具,和生物信息學(xué)應(yīng)用的基礎(chǔ)。它也被廣泛地應(yīng)用在版本控制,比如Git用來調(diào)和文件之間的改變。

  • LCS 問題可以用動態(tài)規(guī)劃思想完美解決:

    • 設(shè)二維數(shù)組 f[i][j] 表示:數(shù)組 X 的前 i 位與數(shù)組 Y 的前 j 位的最長公共子序列的長度

    • 則有:

      f[i][j] = same(1,1)
      f[i][j] = max{f[i-1][j-1] + same(i,j), f[i-1][j], f[i][j-1]}
      
    • 其中,same(a,b) 表示:當(dāng) X 的第 a 位與 Y的第 b 位完全相同時為“1”,否則為“0”。

  • 綜上,可以總結(jié)出如下的算法,用B[i,j]來作標(biāo)記,

      用"↖"表示序列 X 和 Y 的當(dāng)前最后兩項 x[i] 和  y[j] 相等;
      用“↑”表示選擇時不考慮 x[i],即 f[i][j] = f[i-1][j];
      用“←”表示選擇時不考慮 y[j],即 f[i][j] = f[i][j-1]
    
      LCS_LENGTH(X, Y):
      for i := 1 to m
          C[i,0] := 0
      for j := 1 to n
          C[0,j] := 0
      for i := 1 to m
          for j := 1 to n
              if X[i] = Y[j]
                  C[i,j] := C[i-1,j-1] + 1
                  B[i,j] := "↖"
              else if C[i-1,j] >= C[i, j-1]
                  C[i,j] := C[i-1,j]
                  B[i,j] := "↑"
              else
                  C[i,j] := C[i,j-1]
                  B[i,j] : "←"
    
  • 為了更加直觀,這里用圖表形象給出:

  • Python 代碼(A表示↖,T表示↑,L表示←):

      def LCS_LENGTH(X, Y):
          m = len(X)
          n = len(Y)
          c = [[0 for i in range(n+1)] for i in range(m+1)]
          b = [[0 for i in range(n)] for i in range(m)]
          for i in range(1, m + 1):
              for j in range(1, n + 1):
                  if X[i-1] == Y[j-1]:
                      c[i][j] = c[i-1][j-1] + 1
                      b[i-1][j-1] = "A"
                  elif c[i-1][j] >= c[i][j-1]:
                      c[i][j] = c[i-1][j]
                      b[i-1][j-1] = "T"
                  else:
                      c[i][j] = c[i][j-1]
                      b[i-1][j-1] = "L"
          return c, b
    
    
      def print_lcs(b, X, i, j):
          if i == -1 or j == -1:
              return
          if b[i][j] == "A":
              print_lcs(b, X, i-1, j-1)
              print(X[i])
          elif b[i][j] == "T":
              print_lcs(b, X, i-1, j)
          else:
              print_lcs(b, X, i, j-1)
          return
    
      Y = "BDCABA"
      X = "ABCBDAB"
      c, b = LCS_LENGTH(X, Y)
      print_lcs(b, X, len(X)-1, len(Y)-1)
    
  • 代碼細(xì)節(jié):

    • 創(chuàng)建二維數(shù)組,并且不會出現(xiàn)引用的問題(引用問題是一改就改了多處)

      c = [[0 for i in range(n+1)] for i in range(m+1)]
      
    • b 是輔助的一個二維數(shù)組(表),用來指示當(dāng)前的路徑選擇(走對角A,還是走上T,還是走左L),實(shí)際上,《算法導(dǎo)論》中也明確指出這里可以省略,進(jìn)而在 print_lcs 過程中做比較,這樣可以節(jié)省輔助空間,但是這樣比較淺顯易懂

      2017924-LCSb
    • 構(gòu)造完表后,就可以調(diào)用 print_lcs 過程來得到路徑了,在過程中使用了遞歸的思想,并且最后輸出的順序是從字符串的左邊到右邊的

      "C:\Program Files\Python36\python.exe" D:/PythonProject/DataStructure-Algorithm/DynamicProgramming/LCS.py
      B
      C
      B
      A
      
      Process finished with exit code 0
      

11、編輯距離

  • 題目描述:

    給定一個源串和目標(biāo)串,能夠?qū)υ创M(jìn)行如下操作:

    1. 在給定位置上插入一個字符
    2. 替換任意字符
    3. 刪除任意字符

    寫一個程序,返回最小操作數(shù),使得對源串進(jìn)行這些操作后等于目標(biāo)串,源串和目標(biāo)串的長度都小于2000。

  • 思路:

    動態(tài)規(guī)劃,構(gòu)建二維數(shù)組,注意二維數(shù)組的第0行和第0列不是全0的。

    可以想象,如果source 為空,想要轉(zhuǎn)換為 target,則肯定要執(zhí)行 len(target) = n 次操作,所以dp[i][j]賦初值時要注意這點(diǎn)。

  • 遞推方程:

      //dp[i,j]表示表示源串S[0…i] 和目標(biāo)串T[0…j] 的最短編輯距離
      dp[i,j] = min {dp[i-1,j]+1, dp[i,j-1]+1, dp[i-1,j-1] + (s[i] == t[j] ? 0 : 1) }
      //分別表示:刪除1個,添加1個,替換1個(相同就不用替換)。
    
  • 解釋:

    • 插入是A在和B的前j-1個比,然后再在A的基礎(chǔ)上進(jìn)行插入一個字符,插入的字符是B的第j位,所以插入的代價是dp[i][j-1]+1

    • 刪除是A的前i-1個和B的j個比,因?yàn)榘袮刪除了一個字符,所以刪除的代價是dp[i-1][j]+1

    • 替換是A的前i-1個和B的j-1個比,然后把A的第i位變成B的第j位。所以編輯的代價是dp[i-1][j-1]+1

  • python 代碼:

      def editDistance(source, target):
          m = len(source)
          n = len(target)
    
          dp = [[0 for i in range(n+1)] for i in range(m+1)]
          for i in range(n+1):
              dp[0][i] = i
          for i in range(m+1):
              dp[i][0] = i
    
          for i in range(1, m+1):
              for j in range(1, n+1):
                  if source[i-1] == target[j-1]:
                      dp[i][j] = dp[i-1][j-1]
                  else:
                      dp[i][j] = min(min(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1
          return dp[m][n]
    
      source = "abc"
      target = "axxxbxxxc"
      print(editDistance(source, target))
    

12、交替字符串

  • Leetcode: 97. Interleaving String

  • 題目描述:

    輸入三個字符串s1、s2和s3,判斷第三個字符串s3是否由前兩個字符串s1和s2交錯而成,即不改變s1和s2中各個字符原有的相對順序,

    例如當(dāng)s1 = “aabcc”,s2 = “dbbca”,s3 = “aadbbcbcac”時,則輸出true,但如果s3=“accabdbbca”,則輸出false。

  • 思路:

    多個字符串做“比較”的問題,大多都可以用DP求解。

    構(gòu)建二維數(shù)組,一般其規(guī)模為:(m+1)*(n+1)。

    令dp[i][j]代表s3[0...i+j-1]是否由s1[0...i-1]和s2[0...j-1]的字符組成。

    自然,我們的想法是遍歷s3中的每個元素,然而要如何找到遞推關(guān)系呢?

    因?yàn)橹恍枰敵鰐rue或false,那么我們可以只計算true的情形,其余情況全是false。

    假設(shè)dp[i-1][j]為true,那么dp[i][j]為true的條件就是s1[i-1]是否等于s3[i+j-1]。
    假設(shè)dp[i][j-1]為true,那么dp[i][j]為true的條件就是s2[j-1]是否等于s3[i+j-1]。

  • 由此遞推關(guān)系就可以求出:

      dp[i][j]= (dp[i][j-1]  && str2[j-1]==str3[i+j-1])  || (dp[i-1][j]   && str1[i-1]==str3[i+j-1])
    
  • Python 代碼:

      def isInterleave(s1, s2, s3):
          m = len(s1)
          n = len(s2)
          k = len(s3)
          if k != m + n:
              return False
          dp = [[False for i in range(n + 1)] for i in range(m + 1)]
          dp[0][0] = True
          # if s1[0] == s3[0]:
          #     dp[1][0] = True
          # if s2[0] == s3[0]:
          #     dp[0][1] = True
          for i in range(m+1):
              for j in range(n+1):
                  if i != 0 or j != 0:
                      if dp[i-1][j] is True and s1[i-1] == s3[i+j-1]:
                          dp[i][j] = True
                      elif dp[i][j-1] is True and s2[j-1] == s3[i+j-1]:
                          dp[i][j] = True
                      else:
                          dp[i][j] = False
          return dp[i][j]
    
      s1 = "xyz"
      s2 = "abc"
      s3 = "xyzabc"
      print(isInterleave(s1, s2, s3))
    

13、矩陣鏈乘積

  • 矩陣鏈乘積(英語:Matrix chain multiplication,或Matrix Chain Ordering Problem,MOCP)是可用動態(tài)規(guī)劃解決的最佳化問題。給定一序列矩陣,期望求出相乘這些矩陣的最有效方法。此問題并不是真的去執(zhí)行其乘法,而只是決定執(zhí)行乘法的順序而已。

  • 因?yàn)榫仃嚦朔ň哂薪Y(jié)合律,所有其運(yùn)算順序有很多種選擇。換句話說,不論如何括號其乘積,最后結(jié)果都會是一樣的。例如,若有四個矩陣ABCD,將可以有:

      ABCD = (AB)(CD) = A(BC)D = AB(CD) ...
    
  • 但括號其乘積的順序是會影響到需計算乘積所需簡單算術(shù)的數(shù)目,假定各矩陣維數(shù)分別為 10x100 100x5 5x50,如果按 ((AB)C) 的加括號方式,要執(zhí)行7500次標(biāo)量乘法,而按(A(BC))的加括號方式,要執(zhí)行75000次標(biāo)量乘法。

  • 那要如何決定n個矩陣相乘的最佳順序呢?可以比較每一順序的運(yùn)算量(使用蠻力),但這將需要時間O(2^n),是一種非常慢且對大n不實(shí)在的方法。那解決方法,如我們將看到的,是將問題分成一套相關(guān)的子問題。以解答子問題一次而再使用其解答數(shù)次,即可以徹底地得出其所需時間。此一方法稱為動態(tài)規(guī)劃。

  • 動態(tài)規(guī)劃算法步驟:

    • 首先思考:若只有兩個矩陣相乘,則只會有一種方法去乘它們,所有其最小成本為乘積的成本,那么接下來可以按照如下方法計算。
    • 取得矩陣的序列且將其分成兩個子序列。
    • 找出乘完每一子序列的最小成本。
    • 將成本加起來,并加上兩個結(jié)果矩陣相乘的成本。
    • 在每一矩陣序列可分開的位置運(yùn)作,并取其最小值。
  • 總結(jié)成狀態(tài)轉(zhuǎn)移方程即為:

      m[i,j] = 0                             i=j
      m[i,j] = min{m[i,k]+m[k+1,j]+Pi-1PkPj} i<j
    

m[i,j] 為 A1A2...Aj 的最優(yōu)完全加括號所需的最少標(biāo)量乘法次數(shù),對于整個問題來說,計算 A1...n 的最節(jié)省方式的代價自然應(yīng)為 m[1,n]

Pi-1PkPj 是計算 Ai...k 和 Ak+1...j 的積所消耗的時間

  • 偽代碼:

      Matrix-Chain-Order(int p[])
      {
          n = p.length - 1;
          for (i = 1; i <= n; i++) 
              m[i,i] = 0;
    
          for (l=2; l<=n; l++) { // l is chain length
              for (i=1; i<=n-l+1; i++) {
                  j = i+l-1;
                  m[i,j] = MAXINT;
                  for (k=i; k<=j-1; k++) {
                      q = m[i,k] + m[k+1,j] + p[i-1]*p[k]*p[j];//Matrix Ai has the dimension  p[i-1] x p[i].
                      if (q < m[i,j]) {
                          m[i,j] = q;
                          s[i,j] = k;
                      }
                  }
              }
          }
      }
    
    • 稍加考察,可以知道運(yùn)行時間為 Θ(n^3),輔助空間為 Θ(n^2)
  • 上述過程除了確定最少標(biāo)量乘法數(shù),還計算了用于構(gòu)造最優(yōu)解的表格 s[1..n,1..n] 。每一項s[i,j]記錄了AiAi+1...Aj的最佳加括號的在Ak和Ak+1之間分裂的值k,于是我們知道最終矩陣積A1..n的最佳計算是A1..s[1,n] As[1,n]+1..n。

  • 于是可以有打印過程:

      PRINT-OPTIMAL-PARENS(s,i,j)
      if i = j
          then print "A"i
          else print "("
              PRINT-OPTIMAL-PARENS(s,i,s[i,j])
              PRINT-OPTIMAL-PARENS(s,s[i,j]+1,j)
              print ")"
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內(nèi)容