LeetCode刷題 --基礎知識篇 --動態規划


  記錄一下《算法導論》里關於動態規划的一些知識點以及自己的想法。

動態規划

  動態規划是通過組合子問題來求解原問題的一種算法。動態規划應用於子問題重疊的情況,即不同的子問題具有公共的子子問題(子問題的求解是遞歸進行的,將其划分為更小的子子問題)。這種情況下,動態規划算法對每個子子問題只求解一次,將其解保存在一個表格中,從而無需每次求解一個子子問題時都重新計算,避免了不必要的計算工作。動態規划通常用來求解最優化問題。

  設計一個動態規划算法通常可以分為四步

  1. 刻畫一個最優解的特征值
  2. 遞歸定義最優解的值
  3. 計算最優解的值,通常采用自底向上的方法
  4. 利用計算出的信息構造出一個最優解

  步驟1~3是動態規划算法求解問題的基礎。如果我們僅僅需要一個最優解的值,而非解本身,可以忽略步驟4.如果確實需要步驟4,有時候需要在執行步驟3的過程中維護一些額外信息,以便用來構建一個最優解。

動態規划原理

  前面提到了動態規划通常是用來求解“最優化問題”,那么具體什么樣的問題適合使用動態規划來求解呢? 適合應用動態規划方法求解的最優化問題應該具備兩個要素:最優子結構子問題重疊

最優子結構:

  用動態規划方法求解最優化問題的第一步就是刻畫最優解的結構。如果一個問題的最優解包含其子問題的最優解,我們就稱此問題具有最優子結構性質。但也因此,我們必須小心確保考察了最優解中用到的所有子問題。在發掘最優子結構性質的過程中,實際上遵循了如下的通用模式:

  1. 證明問題最優解的第一個組成部分是做一個選擇,這次選擇會產生一個或多個待解決的子問題。
  2. 對於一個給定問題,在其可能的第一步選擇中,你假定已經知道哪種選擇才會得到最優解。你現在並不關心這種選擇具體是如何得到的,只是假定已經知道了這種選擇。
  3. 給定可獲得最優解的選擇后,你確定這次選擇會產生哪些子問題,以及如何最好地刻畫子問題空間。
  4. 作為構成原問題最優解的組成部分,每個子問題的解就是它本身的最優解。

重疊子問題:

  適用動態規划方法求解的最優化問題應該具備的第二個性質是子問題空間必須足夠“小”,即問題的遞歸算法會反復地求解相同的子問題,而不是一直生成新的子問題。一般來講,不同子問題的總數是輸入規模的多項式函數為好。如果遞歸算法反復求解相同的子問題,我們就稱最優化問題具有重疊子問題性質。

 

一些可以使用動態規划求解的題目:

鋼條切割:

  這道題應該是動態規划方面最經典的題目了,題目是這樣的:某公司購買了長鋼條,將其切割為短鋼條出售。切割工序本身沒有成本支出。公司管理層希望知道最佳的切割方案。假定我們知道出售一段長為i英寸的鋼條的價格為pi,鋼條的長度均為整英寸。給出一個價格表的樣例如下:

長度i 1 2 3  4 5 6 7 8 9 10
價格pi 1 5 8 9 10 17 17 20 24 30

 

  給定一段長度為n的鋼條和一個價格表,求切割方案使得銷售收益達到最大。注意,如果長度為n英寸的鋼條的價格pn足夠大,那么最優解可能就是完全不需要切割。

  我們可以使用這樣一種思路來思考這個問題:當完成首次切割后,將兩端鋼條看成兩個獨立的鋼條切割問題實例。我們通過組合這兩個相關子問題的最優解,並在所有可能的兩段切割方案中選擇收益最大者,來構成我們原問題-----對長度為n的鋼條切割的最優解。即長度為n的鋼條的最大收益為:

    Rn = Max(Pn, R1+R(n-1), R2+R(n-2) ...... , R(n-1) + R1)

  上述算式中Rn代表長度為n的鋼條的最大收益,如R2+R(n-2)即代表長度為2的鋼條的最大收益與長度為n-2的鋼條的最大收益的和。而我們獲得長度為n的鋼條的最大收益的方式就是在所有可能的兩切割方案中選擇收益最大者。即上述一系列最優解的組合的最大值。

  使用動態規划方法求解最優鋼條切割問題有兩種方法。

帶備忘的自頂向下法:

  此方法仍按自然的遞歸形式編寫過程,但過程會保存每個子問題的解。當需要一個子問題的解時,過程首先減產是否已經保存過此解,如果是則直接返回保存的值,從而節省了計算時間。下面給出這種方法的偽代碼:

 1 MEMOIZED-CUT-ROD(p, n)
 2 {
 3     let r[0...n] be a new array
 4     
 5     for(int i = 0; i <= n; i++)
 6     {
 7         r[i] = -∞;
 8     }
 9     
10     return MEMOIZED-CUT-ROD-AUX(p,n,r)
11 }
12 
13 MEMOIZED-CUT-ROD-AUX(p,n,r)
14 {
15     if(r[n] >= 0)
16     {
17         return r[n];
18     }
19     
20     if(n == 0)
21     {
22         q = 0;
23     }
24     else
25     {
26         q = -∞;
27         
28         for(int i = 1; i<= n; i++)
29         {
30             q = MAX(q, p[i] + MEMOIZED-CUT-ROD-AUX(p,n-i,r))
31         }
32     }
33     
34     r[n] = q;
35     
36     return q;
37 }

  這里首先把輔助數組r[0...n]的元素初始化為-∞。這里其實是為了將這個對應的值標記為“未知”,因為我們確定收益總是非負值,因此這樣一個值可以確定實際上它還沒有被計算過。在MEMOIZED-CUT-ROD-AUX(p,n,r)過程中先檢查所需要的值是否已知,如果已知則直接返回,否則計算所需的值,然后記錄下來。注意28~31行,其實就是我們之前提到的思路,在所有可能的兩段切割方案中選擇收益最大者。

 

自底向上法:

  這種方法一般需要恰當的定義子問題“規模”的概念,使得任何子問題的求解都只依賴於“更小的”子問題的求解。因此我們可以將子問題按規模排序,按由小到大的順序進行求解。當求解某個子問題的時候,它所依賴的那些更小的子問題都已求解完畢,結果已經保存。每個子問題只需求解一次,當我們求解它時,它的所有前提子問題都已求解完成。下面給出這種方式的偽代碼:

 1 BOTTOM-UP-CUT-ROD(p,n)
 2 {
 3     let r[0...n] be a new array
 4     r[0] = 0;
 5     
 6     for(int j = 1; j <= n; j ++)
 7     {
 8         q = -∞;
 9         
10         for(i = 1; i <= j; i++)
11         {
12             q = max(q, p[i] + r[j-i]);
13         }
14         
15         r[j] = q;
16     }
17     
18     return r[n];
19 }

  這種方法采用子問題的自然順序:若i<j,則規模為i的子問題比規模為j的子問題“更小”。因此依次求解。

  另:筆者第一次看到這里時有一個疑問,就是按上述的方式對應我們這道題目中給出的長度與價格的表格,如果長度大於10會怎樣?p[i]是會報error超出索引邊界的。實際上,要想將上面的自底向上的方法實際運用,還需要額外有一部分邏輯,就是更新p[]數組,以此來保證在遇到“更大”規模的問題時依然可以使用已求出的“更小”的規模的解。

 

下面附上兩個LeetCode的動態規划的題目:

  來源:力扣(LeetCode),傳送門

53. 最大子序和

給定一個整數數組 nums ,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。

示例:

輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,為 6。

 1 public class Solution {
 2         public int MaxSubArray(int[] nums)
 3         {
 4             int maxValue = nums[0];
 5 
 6             for (int i = 1; i < nums.Count(); i++)
 7             {
 8                 if (nums[i - 1] > 0)
 9                 {
10                     nums[i] += nums[i - 1];
11                 }
12 
13                 maxValue = Math.Max(maxValue, nums[i]);
14             }
15 
16             return maxValue;
17         }
18 }

  這道題其實需要想明白一個點,一旦想通了就可以理解上面的代碼了。即上面的代碼其實是求出了這個數組nums每個位置上所能獲得的最大子樹組的和。換句話說,比如index為0,那么這個位置上的最大和為nums[0]。又比如index為3,那么最大和就是Max(nums[0]+nums[1] + nums[2] + nums[3], nums[1] + nums[2] + nums[3], nums[2] + nums[3], nums[3])。

  用動態規划的思路來解釋是,每個位置可以取得的最大值實際上是和它本身的值以及它前一個位置所能取得的最大和的值密切相關的。

 

70.爬樓梯

假設你正在爬樓梯。需要 n 階你才能到達樓頂。

每次你可以爬 1 或 2 個台階。你有多少種不同的方法可以爬到樓頂呢?

注意:給定 n 是一個正整數。

示例 1:

輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
1. 1 階 + 1 階
2. 2 階
示例 2:

輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
1. 1 階 + 1 階 + 1 階
2. 1 階 + 2 階
3. 2 階 + 1 階

 1 public class Solution {
 2         public int ClimbStairs(int n)
 3         {
 4             if (n == 0)
 5             {
 6                 return 0;
 7             }
 8 
 9             if (n == 1)
10             {
11                 return 1;
12             }
13 
14             var n_Count = new int[n];
15 
16             n_Count[0] = 1;
17             n_Count[1] = 2;
18 
19             for (int i = 3; i <= n; i++)
20             {
21                 n_Count[i - 1] = n_Count[i - 2] + n_Count[i - 3];
22             }
23 
24             return n_Count[n_Count.Length - 1];
25         }
26 }

 

  這道題的思路是,要到達n這一層實際上有兩種方式,即從n-1爬1階,或是從n-2爬兩階。因此到達n這一層的所有方法的和就是到n-1層的所有方法的和 + 到達n-2層所有方法的和

 

  動態規划還是有很多有趣的題目,如最長公共子序列,矩陣乘法啊等等,篇幅受限就不記錄了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM