告別動態規劃,連刷 40 道題,我總結了這些套路,看不懂你打我(萬字長文)

動態規劃難嗎?說實話,我以爲很難,特別是對於初學者來講,我當時入門動態規劃的時候,是看 0-1 揹包問題,當時真的是一臉懵逼。後來,我遇到動態規劃的題,看的懂答案,但就是本身不會作,不知道怎麼下手。就像作遞歸的題,看的懂答案,但下不了手,關於遞歸的,我以前也寫過一篇套路的文章,若是對遞歸不大懂的,強烈建議看一看:爲何你學不會遞歸,告別遞歸,談談個人經驗java

對於動態規劃,春招秋招時好多題都會用到動態規劃,一氣之下,再 leetcode 連續刷了幾十道題面試

在這裏插入圖片描述

以後,豁然開朗 ,感受動態規劃也不是很難,今天,我就來跟你們講一講,我是怎麼作動態規劃的題的,以及從中學到的一些套路。相信你看完必定有所收穫算法

若是你對動態規劃感興趣,或者你看的懂動態規劃,但殊不知道怎麼下手,那麼我建議你好好看如下,這篇文章的寫法,和以前那篇講遞歸的寫法,是差很少同樣的,將會舉大量的例子。若是一次性看不完,建議收藏,同時別忘了素質三連數組

爲了兼顧初學者,我會從最簡單的題講起,後面會愈來愈難,最後面還會講解,該如何優化。由於 80% 的動規都是能夠進行優化的。不過我得說,若是你連動態規劃是什麼都沒聽過,可能這篇文章你也會壓力山大。微信

1、動態規劃的三大步驟

動態規劃,無非就是利用歷史記錄,來避免咱們的重複計算。而這些歷史記錄,咱們得須要一些變量來保存,通常是用一維數組或者二維數組來保存。下面咱們先來說下作動態規劃題很重要的三個步驟,數據結構

若是你聽不懂,也不要緊,下面會有不少例題講解,估計你就懂了。之因此不配合例題來說這些步驟,也是爲了怕大家腦殼亂了學習

第一步驟:定義數組元素的含義,上面說了,咱們會用一個數組,來保存歷史數組,假設用一維數組 dp[] 吧。這個時候有一個很是很是重要的點,就是規定你這個數組元素的含義,例如你的 dp[i] 是表明什麼意思?優化

第二步驟:找出數組元素之間的關係式,我以爲動態規劃,仍是有一點相似於咱們高中學習時的概括法的,當咱們要計算 dp[n] 時,是能夠利用 dp[n-1],dp[n-2].....dp[1],來推出 dp[n] 的,也就是能夠利用歷史數據來推出新的元素值,因此咱們要找出數組元素之間的關係式,例如 dp[n] = dp[n-1] + dp[n-2],這個就是他們的關係式了。而這一步,也是最難的一步,後面我會講幾種類型的題來講。3d

學過動態規劃的可能都常常聽到最優子結構,把大的問題拆分紅小的問題,說時候,最開始的時候,我是對最優子結構一夢懵逼的。估計大家也聽多了,因此這一次,我將換一種形式來說,再也不是各類子問題,各類最優子結構。因此大佬可別噴我再亂講,由於我說了,這是我本身平時作題的套路。code

第三步驟:找出初始值。學過數學概括法的都知道,雖然咱們知道了數組元素之間的關係式,例如 dp[n] = dp[n-1] + dp[n-2],咱們能夠經過 dp[n-1] 和 dp[n-2] 來計算 dp[n],可是,咱們得知道初始值啊,例如一直推下去的話,會由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,因此咱們必需要可以直接得到 dp[2] 和 dp[1] 的值,而這,就是所謂的初始值

由了初始值,而且有了數組元素之間的關係式,那麼咱們就能夠獲得 dp[n] 的值了,而 dp[n] 的含義是由你來定義的,你想求什麼,就定義它是什麼,這樣,這道題也就解出來了。

不懂?沒事,咱們來看三四道例題,我講嚴格按這個步驟來給你們講解。

2、案例詳解

案例1、簡單的一維 DP

問題描述:一隻青蛙一次能夠跳上1級臺階,也能夠跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

(1)、定義數組元素的含義

按我上面的步驟說的,首先咱們來定義 dp[i] 的含義,咱們的問題是要求青蛙跳上 n 級的臺階總共由多少種跳法,那咱們就定義 dp[i] 的含義爲:跳上一個 i 級的臺階總共有 dp[i] 種跳法。這樣,若是咱們可以算出 dp[n],不就是咱們要求的答案嗎?因此第一步定義完成。

(2)、找出數組元素間的關係式

咱們的目的是要求 dp[n],動態規劃的題,如大家常常據說的那樣,就是把一個規模比較大的問題分紅幾個規模比較小的問題,而後由小的問題推導出大的問題。也就是說,dp[n] 的規模爲 n,比它規模小的是 n-1, n-2, n-3.... 也就是說,dp[n] 必定會和 dp[n-1], dp[n-2]....存在某種關係的。咱們要找出他們的關係。

那麼問題來了,怎麼找?

這個怎麼找,是最核心最難的一個,咱們必須回到問題自己來了,來尋找他們的關係式,dp[n] 究竟會等於什麼呢?

對於這道題,因爲狀況能夠選擇跳一級,也能夠選擇跳兩級,因此青蛙到達第 n 級的臺階有兩種方式

一種是從第 n-1 級跳上來

一種是從第 n-2 級跳上來

因爲咱們是要算全部可能的跳法的,因此有 dp[n] = dp[n-1] + dp[n-2]。

(3)、找出初始條件

當 n = 1 時,dp[1] = dp[0] + dp[-1],而咱們是數組是不容許下標爲負數的,因此對於 dp[1],咱們必需要直接給出它的數值,至關於初始值,顯然,dp[1] = 1。同樣,dp[0] = 0.(由於 0 個臺階,那確定是 0 種跳法了)。因而得出初始值:

dp[0] = 0.
dp[1] = 1.
即 n <= 1 時,dp[n] = n.

三個步驟都作出來了,那麼咱們就來寫代碼吧,代碼會詳細註釋滴。

int f( int n ){
    if(n <= 1)
    return n;
    // 先建立一個數組來保存歷史數據
    int[] dp = new int[n+1];
    // 給出初始值
    dp[0] = 0;
    dp[1] = 1;
    // 經過關係式來計算出 dp[n]
    for(int i = 2; i <= n; i++){
        dp[i] = dp[i-1] + dp[-2];
    }
    // 把最終結果返回
    return dp[n];
}
(4)、再說初始化

你們先想如下,你以爲,上面的代碼有沒有問題?

答是有問題的,仍是錯的,錯在對初始值的尋找不夠嚴謹,這也是我故意這樣弄的,意在告訴大家,關於初始值的嚴謹性。例如對於上面的題,當 n = 2 時,dp[2] = dp[1] + dp[0] = 1。這顯然是錯誤的,你能夠模擬一下,應該是 dp[2] = 2。

也就是說,在尋找初始值的時候,必定要注意不要找漏了,dp[2] 也算是一個初始值,不能經過公式計算得出。有人可能會說,我想不到怎麼辦?這個很好辦,多作幾道題就能夠了。

下面我再列舉三道不一樣的例題,而且,再在將來的文章中,我也會持續按照這個步驟,給你們找幾道有難度且類型不一樣的題。下面這幾道例題,不會講的特性詳細哈。實際上 ,上面的一維數組是能夠把空間優化成更小的,不過咱們如今先不講優化的事,下面的題也是,不講優化版本。

案例二:二維數組的 DP

我作了幾十道 DP 的算法題,能夠說,80% 的題,都是要用二維數組的,因此下面的題主要以二維數組爲主,固然有人可能會說,要用一維仍是二維,我怎麼知道?這個問題不大,接着往下看。

問題描述

一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲「Start」 )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲「Finish」)。

問總共有多少條不一樣的路徑?

在這裏插入圖片描述

這是 leetcode 的 62 號題:https://leetcode-cn.com/problems/unique-paths/

仍是老樣子,三個步驟來解決。

步驟1、定義數組元素的含義

因爲咱們的目的是從左上角到右下角一共有多少種路徑,那咱們就定義 dp[i] [j]的含義爲:當機器人從左上角走到(i, j) 這個位置時,一共有 dp[i] [j] 種路徑。那麼,dp[m-1] [n-1] 就是咱們要的答案了。

注意,這個網格至關於一個二維數組,數組是從下標爲 0 開始算起的,因此 右下角的位置是 (m-1, n - 1),因此 dp[m-1] [n-1] 就是咱們要找的答案。

步驟二:找出關係數組元素間的關係式

想象如下,機器人要怎麼樣才能到達 (i, j) 這個位置?因爲機器人能夠向下走或者向右走,因此有兩種方式到達

一種是從 (i-1, j) 這個位置走一步到達

一種是從(i, j - 1) 這個位置走一步到達

由於是計算全部可能的步驟,因此是把全部可能走的路徑都加起來,因此關係式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]。

步驟3、找出初始值

顯然,當 dp[i] [j] 中,若是 i 或者 j 有一個爲 0,那麼還能使用關係式嗎?答是不能的,由於這個時候把 i - 1 或者 j - 1,就變成負數了,數組就會出問題了,因此咱們的初始值是計算出全部的 dp[0] [0….n-1] 和全部的 dp[0….m-1] [0]。這個仍是很是容易計算的,至關於計算機圖中的最上面一行和左邊一列。所以初始值以下:

dp[0] [0….n-1] = 1; // 至關於最上面一行,機器人只能一直往左走

dp[0…m-1] [0] = 1; // 至關於最左面一列,機器人只能一直往下走

擼代碼

三個步驟都寫出來了,直接看代碼

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    for(int i = 0; i < m; i++){
      dp[i][0] = 1;
    }
    for(int i = 0; i < n; i++){
      dp[0][i] = 1;
    }
        // 推導出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
}

O(n*m) 的空間複雜度能夠優化成 O(min(n, m)) 的空間複雜度的,不過這裏先不講

案例3、二維數組 DP

寫到這裏,有點累了,,但仍是得寫下去,因此看的小夥伴,大家可得繼續看呀。下面這道題也不難,比上面的難一丟丟,不過也是很是相似

問題描述

給定一個包含非負整數的 m x n 網格,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和爲最小。

說明:每次只能向下或者向右移動一步。

舉例:
輸入:
arr = [
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
輸出: 7
解釋: 由於路徑 1→3→1→1→1 的總和最小。

和上面的差很少,不過是算最優路徑和,這是 leetcode 的第64題:https://leetcode-cn.com/problems/minimum-path-sum/

仍是老樣子,可能有些人都看煩了,哈哈,但我仍是要按照步驟來寫,讓那些不大懂的加深理解。有人可能以爲,這些題太簡單了吧,別慌,小白先入門,這些屬於 medium 級別的,後面在給幾道 hard 級別的。

步驟1、定義數組元素的含義

因爲咱們的目的是從左上角到右下角,最小路徑和是多少,那咱們就定義 dp[i] [j]的含義爲:當機器人從左上角走到(i, j) 這個位置時,最下的路徑和是 dp[i] [j]。那麼,dp[m-1] [n-1] 就是咱們要的答案了。

注意,這個網格至關於一個二維數組,數組是從下標爲 0 開始算起的,因此 由下角的位置是 (m-1, n - 1),因此 dp[m-1] [n-1] 就是咱們要走的答案。

步驟二:找出關係數組元素間的關係式

想象如下,機器人要怎麼樣才能到達 (i, j) 這個位置?因爲機器人能夠向下走或者向右走,因此有兩種方式到達

一種是從 (i-1, j) 這個位置走一步到達

一種是從(i, j - 1) 這個位置走一步到達

不過此次不是計算全部可能路徑,而是計算哪個路徑和是最小的,那麼咱們要從這兩種方式中,選擇一種,使得dp[i] [j] 的值是最小的,顯然有

dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + arr[i][j];// arr[i][j] 表示網格種的值
步驟3、找出初始值

顯然,當 dp[i] [j] 中,若是 i 或者 j 有一個爲 0,那麼還能使用關係式嗎?答是不能的,由於這個時候把 i - 1 或者 j - 1,就變成負數了,數組就會出問題了,因此咱們的初始值是計算出全部的 dp[0] [0….n-1] 和全部的 dp[0….m-1] [0]。這個仍是很是容易計算的,至關於計算機圖中的最上面一行和左邊一列。所以初始值以下:

dp[0] [j] = arr[0] [j] + dp[0] [j-1]; // 至關於最上面一行,機器人只能一直往左走

dp[i] [0] = arr[i] [0] + dp[i] [0]; // 至關於最左面一列,機器人只能一直往下走

代碼以下
public static int uniquePaths(int[][] arr) {
    int m = arr.length;
    int n = arr[0].length;
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    dp[0][0] = arr[0][0];
    // 初始化最左邊的列
    for(int i = 1; i < m; i++){
      dp[i][0] = dp[i-1][0] + arr[i][0];
    }
    // 初始化最上邊的行
    for(int i = 1; i < n; i++){
      dp[0][i] = dp[0][i-1] + arr[0][i];
    }
        // 推導出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + arr[i][j];
        }
    }
    return dp[m-1][n-1];
}

O(n*m) 的空間複雜度能夠優化成 O(min(n, m)) 的空間複雜度的,不過這裏先不講

案例 4:編輯距離

此次給的這道題比上面的難一些,在 leetcdoe 的定位是 hard 級別。好像是 leetcode 的第 72 號題。

問題描述

給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使用的最少操做數 。

你能夠對一個單詞進行以下三種操做:

插入一個字符
刪除一個字符
替換一個字符

示例:
輸入: word1 = "horse", word2 = "ros"
輸出: 3
解釋: 
horse -> rorse (將 'h' 替換爲 'r')
rorse -> rose (刪除 'r')
rose -> ros (刪除 'e')

解答

仍是老樣子,按照上面三個步驟來,而且我這裏能夠告訴你,90% 的字符串問題均可以用動態規劃解決,而且90%是採用二維數組。

步驟1、定義數組元素的含義

因爲咱們的目的求將 word1 轉換成 word2 所使用的最少操做數 。那咱們就定義 dp[i] [j]的含義爲:當字符串 word1 的長度爲 i,字符串 word2 的長度爲 j 時,將 word1 轉化爲 word2 所使用的最少操做次數爲 dp[i] [j]

有時候,數組的含義並不容易找,因此仍是那句話,我給大家一個套路,剩下的還得看大家去領悟。

步驟二:找出關係數組元素間的關係式

接下來咱們就要找 dp[i] [j] 元素之間的關係了,比起其餘題,這道題相對比較難找一點,可是,無論多難找,大部分狀況下,dp[i] [j] 和 dp[i-1] [j]、dp[i] [j-1]、dp[i-1] [j-1] 確定存在某種關係。由於咱們的目標就是,**從規模小的,經過一些操做,推導出規模大的。對於這道題,咱們能夠對 word1 進行三種操做

插入一個字符
刪除一個字符
替換一個字符

因爲咱們是要讓操做的次數最小,因此咱們要尋找最佳操做。那麼有以下關係式:

1、若是咱們 word1[i] 與 word2 [j] 相等,這個時候不須要進行任何操做,顯然有 dp[i] [j] = dp[i-1] [j-1]。(別忘了 dp[i] [j] 的含義哈)。

2、若是咱們 word1[i] 與 word2 [j] 不相等,這個時候咱們就必須進行調整,而調整的操做有 3 種,咱們要選擇一種。三種操做對應的關係試以下(注意字符串與字符的區別):

(1)、若是把字符 word1[i] 替換成與 word2[j] 相等,則有 dp[i] [j] = dp[i-1] [j-1] + 1;

(2)、若是在字符串 word1末尾插入一個與 word2[j] 相等的字符,則有 dp[i] [j] = dp[i] [j-1] + 1;

(3)、若是把字符 word1[i] 刪除,則有 dp[i] [j] = dp[i-1] [j] + 1;

那麼咱們應該選擇一種操做,使得 dp[i] [j] 的值最小,顯然有

dp[i] [j] = min(dp[i-1] [j-1],dp[i] [j-1],dp[[i-1] [j]]) + 1;

因而,咱們的關係式就推出來了,

步驟3、找出初始值

顯然,當 dp[i] [j] 中,若是 i 或者 j 有一個爲 0,那麼還能使用關係式嗎?答是不能的,由於這個時候把 i - 1 或者 j - 1,就變成負數了,數組就會出問題了,因此咱們的初始值是計算出全部的 dp[0] [0….n] 和全部的 dp[0….m] [0]。這個仍是很是容易計算的,由於當有一個字符串的長度爲 0 時,轉化爲另一個字符串,那就只能一直進行插入或者刪除操做了。

代碼以下
public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[][] dp = new int[n1 + 1][n2 + 1];
    // dp[0][0...n2]的初始值
    for (int j = 1; j <= n2; j++) 
        dp[0][j] = dp[0][j - 1] + 1;
    // dp[0...n1][0] 的初始值
    for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;
        // 經過公式推出 dp[n1][n2]
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            // 若是 word1[i] 與 word2[j] 相等。第 i 個字符對應下標是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                p[i][j] = dp[i - 1][j - 1];
            }else {
               dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }         
        }
    }
    return dp[n1][n2];  
}

最後說下,若是你要練習,能夠去 leetcode,選擇動態規劃專題,而後連續刷幾十道,保證你之後不再怕動態規劃了。固然,遇到很難的,咱仍是得掛。

Leetcode 動態規劃直達:https://leetcode-cn.com/tag/dynamic-programming/

3、如何優化?

前兩天寫一篇長達 8000 子的關於動態規劃的文章告別動態規劃,連刷40道動規算法題,我總結了動規的套路

這篇文章更多講解我平時作題的套路,不過因爲篇幅過長,舉了 4 個案例以後,沒有講解優化,今天這篇文章就來說解下,對動態規劃的優化如何下手,而且之前幾天那篇文章的題做爲例子直接講優化,若是沒看過的建議看一下(不看也行,我會直接給出題目以及沒有優化前的代碼):告別動態規劃,連刷40道動規算法題,我總結了動規的套路

4、優化核心:畫圖!畫圖!畫圖

沒錯,80% 的動態規劃題均可以畫圖,其中 80% 的題均可以經過畫圖一會兒知道怎麼優化,固然,DP 也有一些很難的題,想優化可沒那麼容易,不過,今天我要講的,是屬於不怎麼難,且最多見,面試筆試最常常考的難度的題。

下面咱們直接經過三道題目來說解優化,你會發現,這些題,優化事後,代碼只有細微的改變,你只要會一兩道,能夠說是會了 80% 的題。

O(n*m) 空間複雜度優化成 O(n)

上次那個青蛙跳臺階的 dp 題是能夠把空間複雜度 O( n) 優化成 O(1),原本打算從這道題講起的,但想了下,想要學習 dp 優化的感受至少都是 小小大佬了,因此就不講了,就從二維數組的 dp 講起。

案例1:最多路徑數

問題描述

一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲「Start」 )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記爲「Finish」)。

問總共有多少條不一樣的路徑?

在這裏插入圖片描述

這是 leetcode 的 62 號題:https://leetcode-cn.com/problems/unique-paths/

這道題的 dp 轉移公式是 dp[i] [j] = dp[i-1] [j] + dp[i] [j-1],代碼以下

不懂的看我以前文章:告別動態規劃,連刷40道動規算法題,我總結了動規的套路

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[][] dp = new int[m][n]; // 
    // 初始化
    for(int i = 0; i < m; i++){
      dp[i][0] = 1;
    }
    for(int i = 0; i < n; i++){
      dp[0][i] = 1;
    }
        // 推導出 dp[m-1][n-1]
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = dp[i-1][j] + dp[i][j-1];
        }
    }
    return dp[m-1][n-1];
}

這種作法的空間複雜度是 O(n * m),下面咱們來說解如何優化成 O(n)。

dp[i] [j] 是一個二維矩陣,咱們來畫個二維矩陣的圖,對矩陣進行初始化

在這裏插入圖片描述
而後根據公式 dp[i][j] = dp[i-1][j] + dp[i][j-1] 來填充矩陣的其餘值。下面咱們先填充第二行的值。
在這裏插入圖片描述
你們想一個問題,當咱們要填充第三行的值的時候,咱們須要用到第一行的值嗎?答是不須要的,不行你試試,當你要填充第三,第四....第 n 行的時候,第一行的值永遠不會用到,只要填充第二行的值時會用到。

根據公式 dp[i][j] = dp[i-1][j] + dp[i][j-1],咱們能夠知道,當咱們要計算第 i 行的值時,除了會用到第 i - 1 行外,其餘第 1 至 第 i-2 行的值咱們都是不須要用到的,也就是說,對於那部分用不到的值咱們還有必要保存他們嗎?

答是不必,咱們只須要用一個一維的 dp[] 來保存一行的歷史記錄就能夠了。而後在計算機的過程當中,不斷着更新 dp[] 的值。單說估計你可能很差理解,下面我就手把手來演示下這個過程。

一、剛開始初始化第一行,此時 dp[0..n-1] 的值就是第一行的值。
在這裏插入圖片描述

二、接着咱們來一邊填充第二行的值一邊更新 dp[i] 的值,一邊把第一行的值拋棄掉。

爲了方便描述,下面咱們用arr (i,j)表示矩陣中第 i 行 第 j 列的值。從 0 開始哈,就是說有第 0 行。

(1)、顯然,矩陣(1, 0) 的值至關於以往的初始化值,爲 1。而後這個時候矩陣 (0,0)的值不在須要保存了,由於再也用不到了。
在這裏插入圖片描述
這個時候,咱們也要跟着更新 dp[0] 的值了,剛開始 dp[0] = (0, 0),如今更新爲 dp[0] = (1, 0)。

(2)、接着繼續更新 (1, 1) 的值,根據以前的公式 (i, j) = (i-1, j) + (i, j- 1)。即 (1,1)=(0,1)+(1,0)=2。
在這裏插入圖片描述
你們看圖,以往的二維的時候, dp[i][j] = dp[i-1] [j]+ dp[i][j-1]。如今轉化成一維,不就是 dp[i] = dp[i] + dp[i-1] 嗎?

即 dp[1] = dp[1] + dp[0],並且還動態幫咱們更新了 dp[1] 的值。由於剛開始 dp[i] 的保存第一行的值的,如今更新爲保存第二行的值。
在這裏插入圖片描述
(3)、一樣的道理,按照這樣的模式一直來計算第二行的值,順便把第一行的值拋棄掉,結果以下
在這裏插入圖片描述
此時,dp[i] 將徹底保存着第二行的值,而且咱們能夠推導出公式

dp[i] = dp[i-1] + dp[i]

dp[i-1] 至關於以前的 dp[i-1][j],dp[i] 至關於以前的 dp[i][j-1]。

因而按照這個公式不停着填充到最後一行,結果以下:
在這裏插入圖片描述
最後 dp[n-1] 就是咱們要求的結果了。因此優化以後,代碼以下:

public static int uniquePaths(int m, int n) {
    if (m <= 0 || n <= 0) {
        return 0;
    }

    int[] dp = new int[n]; // 
    // 初始化
    for(int i = 0; i < n; i++){
      dp[i] = 1;
    }

        // 公式:dp[i] = dp[i-1] + dp[i]
    for (int i = 1; i < m; i++) {
        // 第 i 行第 0 列的初始值
        dp[0] = 1;
        for (int j = 1; j < n; j++) {
            dp[j] = dp[j-1] + dp[j];
        }
    }
    return dp[n-1];
}

案例2:編輯距離

接着咱們來看昨天的另一道題,就是編輯矩陣,這道題的優化和這一道有一點點的不一樣,上面這道 dp[i][j] 依賴於 dp[i-1][j] 和 dp[i][j-1]。而還有一種狀況就是 dp[i][j] 依賴於 dp[i-1][j],dp[i-1][j-1] 和 dp[i][j-1]。

問題描述

給定兩個單詞 word1 和 word2,計算出將 word1 轉換成 word2 所使用的最少操做數 。

你能夠對一個單詞進行以下三種操做:

插入一個字符
刪除一個字符
替換一個字符

示例:
輸入: word1 = "horse", word2 = "ros"
輸出: 3
解釋: 
horse -> rorse (將 'h' 替換爲 'r')
rorse -> rose (刪除 'r')
rose -> ros (刪除 'e')

解答

昨天的代碼以下所示,不懂的記得看以前的文章哈:告別動態規劃,連刷40道動規算法題,我總結了動規的套路

public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[][] dp = new int[n1 + 1][n2 + 1];
    // dp[0][0...n2]的初始值
    for (int j = 1; j <= n2; j++) 
        dp[0][j] = dp[0][j - 1] + 1;
    // dp[0...n1][0] 的初始值
    for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;
        // 經過公式推出 dp[n1][n2]
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            // 若是 word1[i] 與 word2[j] 相等。第 i 個字符對應下標是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                p[i][j] = dp[i - 1][j - 1];
            }else {
               dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }         
        }
    }
    return dp[n1][n2];  
}

沒有優化之間的空間複雜度爲 O(n*m)

你們能夠本身動手作下,按照上面的那個模式,你會優化嗎?
在這裏插入圖片描述
對於這道題其實也是同樣的,若是要計算 第 i 行的值,咱們最多隻依賴第 i-1 行的值,不須要用到第 i-2 行及其之前的值,因此同樣能夠採用一維 dp 來處理的。

不過這個時候要注意,在上面的例子中,咱們每次更新完 (i, j) 的值以後,就會把 (i, j-1) 的值拋棄,也就是說以前是一邊更新 dp[i] 的值,一邊把 dp[i] 的舊值拋棄的,不過在這道題中則不能夠,由於咱們還須要用到它。

哎呀,直接舉例子看圖吧,文字繞來繞去估計會繞暈大家。當咱們要計算圖中 (i,j) 的值的時候,在案例1 中,咱們值須要用到 (i-1, j) 和 (i, j-1)。(看圖中方格的顏色)
在這裏插入圖片描述
不過這道題中,咱們還須要用到 (i-1, j-1) 這個值(可是這個值在以往的案例1 中,它會被拋棄掉)
在這裏插入圖片描述
因此呢,對於這道題,咱們還須要一個額外的變量 pre 來時刻保存 (i-1,j-1) 的值。推導公式就能夠從二維的

dp[i][j] = min(dp[i-1][j] , dp[i-1][j-1] , dp[i][j-1]) + 1

轉化爲一維的

dp[i] = min(dp[i-1], pre, dp[i]) + 1。

因此呢,案例2 其實和案例1 差異不大,就是多了個變量來臨時保存。最終代碼以下(可是初學者話,代碼也沒那麼好寫)

代碼以下
public int minDistance(String word1, String word2) {
    int n1 = word1.length();
    int n2 = word2.length();
    int[] dp = new int[n2 + 1];
    // dp[0...n2]的初始值
    for (int j = 0; j <= n2; j++) 
        dp[j] = j;
    // dp[j] = min(dp[j-1], pre, dp[j]) + 1
    for (int i = 1; i <= n1; i++) {
        int temp = dp[0];
        // 至關於初始化
        dp[0] = i;
        for (int j = 1; j <= n2; j++) {
            // pre 至關於以前的 dp[i-1][j-1]
            int pre = temp;
            temp = dp[j];
            // 若是 word1[i] 與 word2[j] 相等。第 i 個字符對應下標是 i-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)){
                dp[j] = pre;
            }else {
               dp[j] = Math.min(Math.min(dp[j - 1], pre), dp[j]) + 1;
            } 
            // 保存要被拋棄的值       
        }
    }
    return dp[n2]; 
}

總結

上面的這些題,基本都是不怎麼難的入門題,除了最後一道相對難一點。而且基本 80% 的二維矩陣 dp 均可以像上面的方法同樣優化成 一維矩陣的 dp,核心就是要畫圖,看他們的值依賴,固然,還有不少其餘比較難的優化,可是,我遇到的題中,大部分都是我上面這種類型的優化。後面如何遇到其餘的,我會做爲案例來說,今天就先講最廣泛最通用的優化方案。記住,畫二維 dp 的矩陣圖,而後看元素之間的值依賴,而後就能夠很清晰着知道該如何優化了。

在以後的文章中,我也會按照這個步驟,在給你們講四五道動態規劃 hard 級別的題,會放在天天推文的第二條給你們學習。若是以爲有收穫,不放三連走起來(點贊、感謝、分享),嘻嘻。

有收穫?但願老鐵們來個三連擊,給更多的人看到這篇文章

一、點贊,可讓更多的人看到這篇文章
二、關注個人原創微信公衆號『苦逼的碼農』,第一時間閱讀個人文章,已寫了 150+ 的原創文章。

數據結構與算法文章截圖

公衆號後臺回覆『電子書』,還送你一份電子書大禮包哦。

做者簡潔

做者:帥地,一位熱愛、認真寫做的小夥,目前維護原創公衆號:『苦逼的碼農』,已寫了150多篇文章,專一於寫 算法、計算機基礎知識等提高你內功的文章,期待你的關注。
轉載說明:務必註明來源(註明:來源於公衆號:苦逼的碼農, 做者:帥地)

相關文章
相關標籤/搜索