算法Day03-算法研習指導之動態規劃算法詳解

動態規劃算法問題

  • 什麼叫作最優子結構? 和動態規劃有什麼關係?
  • 爲什麼動態規劃遍歷DP數組的方式有正着遍歷,有倒着遍歷,有斜着遍歷?

最優子結構

  • 最優子結構是某些問題的一種特定的性質,並不是動態規劃問題所特有的.
  • 很多問題都具有最優子結構,但是其中大部分不具有重疊子問題,所以不會歸爲動態規劃系列的問題
  • 最優子結構:
    • 可以從子問題的最優結果推導出更大規模問題的最優結果
    • 子問題之間必須相互獨立
  • 通過改造問題來優化由於子問題之間不獨立而導致的最優子結構失效的情況:
    • 問題: 假設學校有10個班,已知每個班的最高分與最低分差值的最大分數差,需要計算全校學生中的最大分數差
    • 分析: 這樣的問題就無法通過這10個班的最大分數差來推導出來,因爲這10個班的最大分數差不一定就包含全校學生的最大分數差.比如全校的最大分數差可能是由8班的最高分和6班的最低分的分數差而得.這樣就導致子問題之間不是互相獨立的
    • 改造問題: 直接進行問題改造
    int result = 0;
    for (Student a : school) {
    	for (Student b : school) {
    		if (a is b) {
    			continue;
    		} 
    		result = max(result, |a.score - b.score|)
    	}
    }
    return result;
    
  • 改造問題就是將問題等價轉化:
    • 最大分數差就等價於最高分數與最低分數的差
    • 那麼就是要求最高和最低分數
    • 求最高分數是具備最優子結構的,求最低分數也是具有最優子結構的
    • 這樣就樣一個不具備最優子結構的問題轉化爲具備最優子結構的子問題
    • 藉助最優子結構解決最值問題,再解決最大分數差問題
  • 題目: 求一棵二叉樹的最大值,假設節點中的值都爲非負數
int maxVal(TreeNode root) {
	if (root == null) {
		return -1;
	}
	int left = maxVal(root.left);
	int right = maxVal(root.right);
	return max(root.val, left, right);
}

這個問題符合最優子結構,以root爲根的樹的最大值可以通過兩邊子樹的子問題的最大值推導出來

最優子結構總結

  • 最優子結構並不是動態規劃獨有的一種性質,但是能求最值的問題大部分都會具備最優子結構的性質
  • 最優子結構性質作爲動態規劃問題的必要條件,一定是可以用來求最值的: 通常情況下,最值的問題,思路往動態規劃想就對了
  • 動態規劃就是從最簡單的base case往後推導, 類似一種鏈式反應.但是隻有具備最優子結構的問題,纔會有發生這種鏈式反應的性質
  • 最優子結構的尋找過程:
    • 就是證明狀態轉移方程正確性的過程
    • 方程符合最優子結構就可以直接遞歸求解
    • 寫出直接遞歸求解後可以根據遞歸樹看出有沒有重疊子問題,如果有則進行優化

DP數組的遍歷方向

  • 動態規劃中DP數組遍歷順序問題:
    • 示例: 二維DP數組
      • 正向遍歷:
      int[][] dp = new int[m][n];
      for (int i = 0; i < m; i++) {
      	for (int j = 0; j < n; j++) {
      		/*
      		 * 計算dp[i][j]
      		 */  
      		 ...
      	}	
      }	
      
      • 反向遍歷:
      int[][] dp = new int[m][n];
      for (int i = m-1; i >= 0; i--) {
      	for (int j = n - 1; j >= 0; j--) {
      		/*
      		 * 計算dp[i][j]
      		 */
      		 ...
      	}
      }
      
      • 斜向遍歷:
      int[][] dp = new int[m][n];
      for (int l = 2; l <= n; l++) {
      	for (int i = 0; i <= n - l; i++) {
      		int j = l + i - 1;
      		/*
      		 * 計算dp[i][j]
      		 */
      	}
      }
      
  • DP數組的遍歷的兩條原則:
    • 遍歷的過程中,所需的狀態必須是已經計算出來的
    • 遍歷的終點必須是存儲結果的位置

編輯距離問題

  • 通過對DP數組的定義,確定了:
    • base case : dp[n][0]dp[0][n]
    • 最終答案 : dp[m][n]
  • 通過狀態轉移方程知道:
    • dp[i][j] 需要從dp[i-1][j], dp[i][j-1], dp[i-1][j-1] 轉移而來
  • 再根據DP數組遍歷的兩條原則,確定使用正向遍歷:
for (int i = 1; i < m; i++) {
	for (int j = 1; j < n; j++) {
		/*
		 * 通過dp[i-1][j], dp[i][j-1], dp[i-1][j-1]計算dp[i][j]
		 */
	}
}
  • 這樣每一步迭代的左邊,上邊,左上邊的位置都是base case或者之前計算出來的值
  • 最終結束在結果的位置dp[m][n]

迴文子序列

  • 通過對DP數組的定義,確定了:
    • base case處在中間的對角線
    • 最終答案: dp[0][n-1]
  • 通過狀態轉移方程知道:
    • dp[i][j] 需要從dp[i+1][j], dp[i][j-1], dp[i+1][j-1] 轉移而來
  • 再根據DP數組遍歷的兩條原則,確定有兩種遍歷方式:
    • 從左至右斜着遍歷
    • 從下向上從左到右遍歷
  • 這樣每一步迭代的左邊,下邊,左下邊已經計算出來值
  • 最終結束在結果的位置 dp[0][n-1]

DP數組遍歷總結

  • DP數組遍歷主要就是看base case最終結果的存儲位置類決定遍歷順序
  • 選擇的遍歷順序要保證遍歷過程中使用的數據都是計算完畢的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章