小白學習動態規劃:優化篇

上一篇講述了動態規劃入門級題目,代碼都是沒有優化的,如果沒有看過的讀者也沒關係,在下面會貼出這兩道題目的所有代碼,包括沒有優化的和優化之後的。感興趣的讀者可以先去看一下上一篇的題目,都是EASY級別的題目。
小白學習動態規劃:入門篇

今天主要是對上一篇博客的兩道題目進行優化,對於絕大多數利用動態規劃的算法題作優化時,個人認爲最重要的優化方法就是:畫DP的圖!找出狀態之間的依賴關係,將其它沒有依賴的狀態廢棄掉。

如果你不懂沒有關係,可以看以下例子,很好理解~

優化類型一:一維降變量

LeetCode70. 爬樓梯

問題描述:假設你需要爬樓梯,需要爬n階才能到達樓頂,每次可以爬1或2階,由多少種不同的方法可以爬到樓頂?

未優化前的代碼:

class Solution {
    public int climbStairs(int n) {
		int[] dp = new int[n];
		for (int i = 0; i < dp.length; i++) {
            if(i == 0){ //第1階
				dp[i] = 1;
			}else if(i == 1){ //第2階
				dp[i] = 2;
			}else{ //第3階及以上
				dp[i] = dp[i-1]+dp[i-2];
			}
		}
	    return dp[n-1];
    }
}

首先,我們先分析dp的填充過程,觀察有什麼值是會在某個時間段後作廢(一直沒有使用到)
圖片1
圖片2
圖片3圖片4
從上面幾幅圖可以很清晰地觀察到,從求第四階樓梯的爬樓梯方法數之後,每求下一階樓梯的方法數時,就會有多一個值被廢棄掉,而求某個狀態的值(爬樓梯的方法數)只與它的第(n-2)個狀態和第(n-1)個狀態[n >= 3]有依賴關係,所以在第(n-2)個狀態以前的值就被廢棄掉了。

所以,這個定義一維DP數組在某種程度上是浪費了內存空間的。

未優化前的狀態轉移方程:
dp[i]={dp[i1]+dp[i2],if i2iif i<2 dp[i] = \begin{cases} dp[i-1] + dp[i-2],&if\ i≥2\\ i &if\ i < 2 \end{cases}
優化的過程可以觀察下圖:
在這裏插入圖片描述

顏色的含義(幫助你理解整個過程):

藍色:初始化的狀態值

黃色:得出下一個狀態的狀態值

紅色:將當前狀態的第(n-1)個狀態轉化爲第(n-2)個狀態

綠色:將第n個狀態轉化爲第(n-1)個狀態

動態規劃過程:

求出一個新的狀態時,將當前狀態的第(n-1)狀態更新爲第(n-2)個狀態,將當前狀態更新爲第(n-1)個狀態,繼續求下一個新的狀態,直到循環結束。

優化後的代碼:

class Solution {
    public int climbStairs(int n) {
        if(n <= 2){return n;}
        int dp0 = 1;
        int dp1 = 2;
        for(int i = 3; i <= n; i++){
            int result = dp0 + dp1;
            dp0 = dp1;
            dp1 = result;
        }
        return dp1;
    }
}

優化類型二:二維降一維

LeetCode62. 不同路徑

問題描述:一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記爲“Start” )。

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

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

圖片6

說明:m 和 n 的值均不超過 100。

未優化前的代碼:

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++){
            for (int j = 0; j < n; j++){
                //上邊界
                if(i == 0 && j >= 0){dp[i][j] = 1;continue;}
                //左邊界
                if(j == 0 && i >= 0){dp[i][j] = 1;continue;}
                //其它情況
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

觀察未優化前的DP矩陣:

圖片7
狀態轉移方程:
dp[i][j]{dp[i1][j]+dp[i][j1],if j1 AND i11,if (i=0 AND j0)OR(i0 AND j=0) dp[i][j]\begin{cases}dp[i-1][j]+dp[i][j-1], &if\ j≥1\ AND\ i≥1\\ 1, &if\ (i=0\ AND\ j≥0)OR(i≥0\ AND\ j=0)\\ \end{cases}
當填充第二行時,情況是這樣的:

圖片8
當填充第三行時,情況是這樣的:
圖片9
**細心的我們會發現:**第一行數據已經沒有用處了,它的存在與否不影響我們求第三行的數據。所以,我們只需要保存上一行數據,就可以得出下一行的數據,並且每求出下一行的一個數據時,都可以捨棄掉上一行的那一個數據,所以只需要一維數組保存一行數據就可以求出下一行的數據。

如果你不是很懂,那麼看下面幾幅圖就會秒懂~

注意左邊數組行下標一直爲0,表示該數組本質上只有一行
圖片10
圖片11
原本二維dp數組的狀態轉移方程爲:
dp[i][j]={dp[i1][j]+dp[i][j1],if j1 AND i11,if (i=0 AND j0)OR(i0 AND j=0) dp[i][j]=\begin{cases}dp[i-1][j]+dp[i][j-1], &if\ j≥1\ AND\ i≥1\\ 1, &if\ (i=0\ AND\ j≥0)OR(i≥0\ AND\ j=0)\\ \end{cases}
在圖中從位置上表現出來的結果是正確的,但是!!!重點來了!

注意看!我們會發現狀態轉移方程轉化爲:
dp[i]={dp[i1]+dp[i],if i11,if i=0 dp[i]=\begin{cases}dp[i-1]+dp[i], &if\ i≥1\\ 1, &if\ i=0\\ \end{cases}

DP的空間複雜度由O(m * n) 降低爲O(m)

最後數組就會呈現出下圖得到狀態:
圖片12
優化後的一維dp代碼:

class Solution {
    public int uniquePaths(int m, int n) {
        if(m <= 1 || n <= 1){return 1;}
        int[] dp = new int[n];
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(j == 0){dp[j] = 1;continue;}
                dp[j] = dp[j-1] + dp[j];
            }
        }
        return dp[dp.length - 1];
    }
}

總結

這兩道題目雖然簡單,但是它們是動態規劃入門的必做題目中的兩道,掌握這兩道題目的思想和優化技巧是非常重要的,優化DP的核心在於:畫圖

掌握畫圖這一種優化方法,尋找值的依賴關係,觀察能否將空間複雜度降低,DP的核心本質上是用空間換取時間的算法,在面試中會遇到許多面試官提出優化方法的場景,掌握這一種技巧,刷多一些相關的題目,自然會熟能生巧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章