動態規劃,我不再怕了。

什麼是動態規劃?

引用 leetcode 的一段話,我認爲它講很權威,我將結合實戰帶你學習動態規劃。java


看得很懵吧?懵就對了,我當初接觸動態規劃的時候,也懵了好久。可是,只有咱們搞清楚如下幾個問題,動態規劃其實也不是那麼的難。(三維四維DP難到懷疑人生QAQ)git

  1. 狀態的定義
  2. 狀態轉移方程(數學概括法)
  3. 初始條件和邊界

仍是有點懵?懵就對了,我詳細解釋一下。算法

狀態的定義

對於狀態的定義,其實就是找題目給定的條件,限制的條件。就拿經典的 爬樓梯 來舉例。typescript

首先咱們收集題目限制的條件,一個是每次只能夠爬1個臺階,或者2個臺階。另一個是須要n階到達樓頂;那麼咱們狀態的定義確定只和這兩個條件有關係。這不是廢話嘛,這真不是廢話,有時候解題的關鍵點之一就是在於找準狀態的定義數組

那這題的狀態的定義該怎麼定義呢?很明顯就是爬n個臺階到達樓頂的不一樣方法。bash

dp = 爬到樓頂的不一樣方法markdown

狀態轉移方程(數學概括法)

轉移方程,也是dp的難點之一,仍是繼續以爬樓梯爲例;其實dp最難也就是前兩點了;把狀態的定義狀態轉移方程肯定以後,dp也就迎刃而解了。oop

其實狀態轉移方程也就是數學概括法,聽着很高大上吧?其實它就是找規律而已。學習

關鍵是怎麼找規律呢?授之以魚不如授之以漁。編碼

直接根據求解的問題,拆分紅子問題,規模更小的問題來思考;好比爬樓梯,爬n臺階很懵吧?我相信沒有接觸過dp的同窗都會很懵,別怕,咱們從規模小的問題來思考,獲得結果,從而遞推出規律,也就是狀態轉移方程;

  • n=1,結果爲1
  • n=2,結果爲2
  • n=3,結果爲3
  • n=4,結果爲5
  • ........

仔細觀察上面的數據,由問題的規模不斷的變大,結果也會逐漸的變大,那它們有什麼規律呢?細心觀察的小夥伴確定會發現 f(n) = f(n-1) + f(n-2),這不就是最熟悉的斐波那契數列問題了嗎?到這裏,狀態轉移方程就是 f(n) = f(n-1) + f(n-2) { n > 2 }

初始條件和邊界

咱們把狀態的定義和狀態轉移方程肯定以後,初始條件就很簡單了,就直接觀察狀態轉移方程知足條件的起始值,其實就是 n > 2,當知足這個條件的時候,公式才成立,不知足公式的條件,就是初始條件或者邊界,也就是當 n <= 2 的結果,就是初始條件;咱們很容易就能夠想出來 f(1) = 1, f(2) = 2;有時候相對難一點的dp,須要利用狀態轉移方程來肯定,後面解釋。

代碼模版

得出dp三個須要肯定的條件以後,咱們就能夠根據這三個條件來寫代碼了

var climbStairs = function(n) {    
  // 狀態:dp = 爬到樓頂的不一樣方法 
  // 邊界: fn(1) = 1,fn(2) = 2 
  // 動態方程: fn(n) = fn(n-1) + fn(n-2) 
  if(n < 3) return n    
  let fn_1 = 1    
  let fn_2 = 2    
  let res = 0   
  for(let i = 3; i <= n; i++){ 
    res = fn_1 + fn_2  
    fn_1 = fn_2        
    fn_2 = res  
  }   
  return res
};複製代碼

經典的股票系列問題

當你徹底弄懂了股票系列問題,你纔算得上真正的入門動態規劃問題。在此以前,有個大神的文章寫得很是好,可是是java版本的,我在學習的過程當中也發現了一點小錯誤,掙扎了好久,弄明白以後寫下此文,對動態規劃學習作一個總結。我但願你能先認真的過一邊原文,原文很長,你必須耐心的看懂裏面講的問題,無需糾結裏面的細節,下面我將帶領你解決這些細枝末節的東西,真正的入門動態規劃。原文連接

股票問題狀態定義

我假設你已經看過原文,大概弄懂了做者講什麼,先來複習看這張狀態圖,有的小夥伴確定會問?他是怎麼獲得這張狀態定義圖的?上面爬樓梯我說過了,是根據題目的限制條件,抽取出來的。


  • 1 表明持有股票,只能選擇rest操做,或者sell賣掉股票
  • 0 表明未持有股票,只能選擇rest操做,或者buy買股票

以上兩點就是最關鍵的狀態定義,同時結合能夠交易的次數k,和第i天的股票價格,狀態的具體定義能夠這麼來:

  • dp[i][k][1]:今天是第 i 天,我如今手上持有着股票,至今最多進行 k 次交易。
  • dp[i][k][0]:今天是第 i 天,我如今手上未持有着股票,至今最多進行 k 次交易。

很顯然,咱們想求的最終答案是 dp[n - 1][K][0],即最後一天,最多容許 K 次交易,最多得到多少利潤。爲何不是 dp[n - 1][K][1]?由於 [1] 表明手上還持有股票,[0] 表示手上的股票已經賣出去了,很顯而後者獲得的利潤必定大於前者。 

狀態轉移方程(數學概括法)

最關鍵的步驟,也是難點之一,可是對於狀態轉移方程,咱們能夠根據狀態的定義,轉變得出,仔細觀察上面的狀態轉換圖,買賣股票的操做,咱們能夠得出持有股票,或未持有股票的兩個狀態轉移。若是還不明白,回頭看買賣的狀態圖。

  1. 未持有股票:以前就沒有,能夠rest;或者以前就持有,我如今賣了。
  2. 已持有股票,以前就持有,能夠rest,或者以前未持有,我如今買入。

根據這個狀態的轉換,咱們就能夠得出狀態轉移方程,也就是數學概括法得出通用公式,[ 這裏要注意賣出股票會獲取利潤,買入股票須要支付成本的問題 ]。爲何 k-1 呢?由於當咱們買入一次股票以後交易次數就要減 1

  1. dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
  2. dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

到這裏,狀態轉移方程就寫出來了,難點在於考慮買賣股票的狀態圖,根據狀態機得出轉移方程,不過,我認爲這都是熟練度的問題,你只要把dp的本質理清楚,剩下的就是多練了。

初始條件和邊界

股票問題的初始條件和邊界,會有點隱晦,不像爬樓梯那麼直白明瞭。我上面也說過,比較難的咱們能夠經過狀態轉移方程直接代入進去,而後得出,咱們來嘗試一下。

  • dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])

  1. dp[0][k][0] = max(dp[-1][k][0], dp[-1][k][1] + prices[i]) { i = 0 }
  2. 其中 dp[-1][k][0],無論k是多少次,股票第 i 天都是 -1 ,也就是還沒開始呀,0纔是開始,因此 dp[-1][k][0] = 0
  3. 再看 dp[-1][k][1],都沒有開始,你就持有股票了,咋可能呢?由於未持有是0,咱們就用負無窮表示未開始持有股票的值,即 -Infinity
  4. 因此 dp[i][k][0] = max(0, -Infinity + prices[i]) = 0
  • dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
  1. dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]) { i = 0 }
  2. 同理 dp[i-1][k][1] 爲 -Infinity,dp[i-1][k-1][0] 爲 0
  3. 因此 dp[i][k][1] = max(-Infinity, 0 - prices[i]) = -prices[i]

綜上所述,咱們就能夠得出初始條件和邊界值了,即當 i - 1 == -1的時候

  • dp[-1][k][0] = 0
  • dp[-1][k][1] = -prices[0]

當 k= 0 的時候,也就是還未交易,原理也是同樣的

  • dp[i][0][0] = 0
  • dp[i][0][1] = -prices[0]

代碼模版

  • 設三維數組 dp[n][k+1][2],n,k+1,2均爲當前維度數組元素個數,有的小夥伴會問爲何是k+1,n呢?而不是k,n-1。由於 dp 須要初始值遞推,因此要多取一個元素。
  • i 爲天數,m爲最大交易次數,0或1爲交易狀態;且 0 <= i < n ,1 <= m <= k
  • 爲何有的狀態枚舉是正着來?有的是反着來?其實兩個均可以,你只須要記住,遍歷的過程當中,所需的狀態必須是已經計算出來的;遍歷的終點必須是存儲結果的那個位置便可。

const maxProfit = function(k, prices) {
    // 交易天數
    let n = prices.length;
    // 最大交易次數,k不影響狀態轉移方程,此處去掉
    let maxTime = k;
    if(n == 0){
        return 0;
    }
    // 初始化三維數組
    // 若是當題 k 不影響狀態轉移方程,此處初始化去掉
    let dp = Array.from(new Array(n),() => new Array(maxTime+1));
    for(let i = 0;i < n;i++){
        for(let r = 0;r <= maxTime;r++){
            dp[i][r] = new Array(2);
        }
    }
    // 若是當題k不影響狀態轉移方程,則只需二維數組
    // let dp = Array.from(new Array(n),() => new Array(2));

    // 枚舉遞推
    for(let i = 0;i < n;i++){
        // 若是當題k不影響狀態轉移方程,內循環去掉
        for(let k = maxTime;k >= 1;k--){
            if(i == 0){
                // 邊界條件處理
                continue;
            }
            // 遞推公式,上面分析的狀態轉移方程
            dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])   
            dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
        }
    }
    // 返回結果
    return dp[n-1][maxTime][0];
    // 若是當題k不影響狀態轉移方程返回此結果
    // return dp[n-1][0];
};複製代碼

秒掉六道股票問題

121. 買賣股票的最佳時機122. 買賣股票的最佳時機 II123. 買賣股票的最佳時機 III188. 買賣股票的最佳時機 IV309. 最佳買賣股票時機含冷凍期714. 買賣股票的最佳時機含手續費

第一道

121. 買賣股票的最佳時機


第二道

122.買賣股票的最佳時機 II


第三道

123. 買賣股票的最佳時機 III


第四道

188.買賣股票的最佳時機 IV 

第五道

309. 最佳買賣股票時機含冷凍期


第六道

714. 買賣股票的最佳時機含手續費


總結

dp問題須要多練,簡單的一維二維還好,難度不大,其中三維思惟就很考驗熟練度和數學建模的抽象能力了,不得不認可有些是天賦型選手,咱們普通人,多練就好。

其實練算法最大的好處,就是提高編碼能力。我在半年以前一點都不懂算法的,寫業務也會偶爾卡殼,以前遇到一個排列組合的業務問題也徹底懵逼。在通過半年的思惟提高之後,業務代碼我徹底能夠切菜式的完成,並且代碼寫得也比以前要好不少。對於困難一點,複雜一點的組件封裝,也可以封裝得很好。甚至,能夠本身封裝一個UI庫。因此,算法真的頗有用,紮實編碼功底的最佳選擇。

另外,推薦閱讀大神的算法小抄,連接。

相關文章
相關標籤/搜索