10.2 動態規劃算法套路及空間優化 —— Climbing Stairs & Unique Paths

這一篇文章從最簡單的動態規劃題目開始,結合上一節動態規劃三要素,以LeetCode兩道基礎的DP題目闡述DP問題的基本套路解法。


 

70. Climbing Stairs

You are climbing a stair case. It takes n steps to reach to the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Note: Given n will be a positive integer.

 

題目解析:

這是一道最基礎的動態規劃問題,用於我們初步瞭解。首先我們來回憶DP三大要素,抽菸喝酒燙頭,不,是最優子結構,狀態轉移方程和邊界。

  1.  最優子結構,就是分析問題的關鍵,構建解題框架。之前以斐波那契數列爲例,這次我們以這道題目爲例。想知道到達n層臺階,有多少種方式,即f(n)是最終想要的答案,抽象爲f(i) 即到達任何一層臺階的方式;所謂子結構,即f(i-1),f(i-2),f(i-3),...,f(2),f(1)。很明顯,f(i)的方案數目和 f(i-1)等子問題的答案是有關係的。這就是第一步分析問題我們要做的。
  2. 狀態轉移方程。f(i)和子問題具體是什麼關係呢?這一步要定量分析。由於每次只能走一步或兩步,一步之前的臺階在i-1層,兩步之前在i-2層,那麼第i層的方案數目不就是:f(i) = f(i-1) + f(i-2),(還是斐波那契數列)。這一步定量分析問題和子問題之間的數量關係,
  3. 邊界。到這兒就很好理解了,可以定義 f(1)=1, f(2)=2.
  4. 其實還有第四步,就是編碼啦,能把你的思路寫出來纔行。

下面我們看代碼實現,在dp問題中,對於f(i)我們一般存爲數組dp.

class Solution:
    def climbStairs(self, n: int) -> int:
        # 特殊情況
        if n <= 2:
            return n
        # dp數組必備
        dp = [0] * (n+1)
        # 邊界
        dp[1] = 1
        dp[2] = 2
        
        for i in range(3, n+1):
            # 狀態轉移方程
            dp[i] = dp[i-1] + dp[i-2]
            
        return dp[-1]        

 

這個一維的問題就圖森破了,下面我們看經典的二維dp問題。說來說去,二維不一定比一維的就難,一維的數組也有很多難題。

62. Unique Paths

A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below).

The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid (marked 'Finish' in the diagram below).

How many possible unique paths are there?

 

題目解析:

我們仿照上題的思路來解析,看三大要素。f(i,j)表示到達i行j列的路徑數,f(m,n)即最終結果。i行j列是從左側或上邊走過來的,必然有一定的最優子結構的形式。那麼接下來分析狀態轉移方程。和上一題類似,i行j列的上一步或者在i-1行j列,或者在i行j-1列,只有這兩種情況,且每種情況下只有一條路徑到i,j,因此直接得到f(i,j) = f(i-1,j) + f(i,j-1)。二維問題的邊界也在二維數組的邊界,下面我們直接看代碼實現:

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # dp數組
        dp = [[0] * n for _ in range(m)]
        # 邊界
        for i in range(m):
            dp[i][0] = 1
        for i in range(n):
            dp[0][i] = 1
        
        for i in range(1, m):            
            for j in range(1, n):
                # 狀態轉移方程
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
        return dp[m-1][n-1] # 或dp[-1][-1]

再額外解釋一下邊界,沿着第一行或者第一列都是隻有一種路徑,所以邊界的初始值都是1,也可以直接把dp數組初始化爲1.


先充分的消化一下上面兩道問題的dp套路,從三要素理解解題思路,進而理解dp的核心思想,最優子結構和無後效性。

根據無後效性,下面我們看一下dp算法的優化思路。

以climbing stairs爲例,基本思路中我們定義了長度爲n的數組用於存儲i處的最終解,時間複雜度O(n);但是從關係式dp[i] = dp[i-1] + dp[i-2]可以看出,在i處我們只需要i-1和i-2的值,更早的值我們不需要了,這就體現了無後效性,因此我們只需要三個變量即可解決問題,下面看代碼:

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2:
            return n
        pre1 = 2
        pre2 = 1
        dp = 0
        for i in range(3, n+1):            
            dp = pre1 + pre2
            pre2, pre1 = pre1, dp
        return dp        

就是這樣,將空間複雜度降到了O(1)。我們再看unique paths題目的優化。

對於第i行第j列,我們看關係式 f(i,j) = f(i-1,j) + f(i,j-1),在前面的解法中,我們的空間複雜度是O(m*n),對於每個位置來說,我們需要左邊的和上邊的,對於每一行來說,我們只需要上一行的結果即可。因此我們可以將空間複雜度降到O(n),結合下面代碼來看,我們只定義一個一維dp數組,在每行遍歷中,不斷更新dp數組的值,不斷拋棄上一行的結果,直到最後一行。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # 定義一維dp數組,同時初始化了第一行的邊界值
        dp = [1] * n
        
        for i in range(1, m):            
            for j in range(n):
                # 第一列的邊界
                if j == 0:
                    dp[j] = 1
                else:
                    # 相當於原狀態轉移方程,不斷更新dp數組值
                    dp[j] += dp[j-1]
        return dp[-1]

 

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