這一篇文章從最簡單的動態規劃題目開始,結合上一節動態規劃三要素,以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三大要素,抽菸喝酒燙頭,不,是最優子結構,狀態轉移方程和邊界。
- 最優子結構,就是分析問題的關鍵,構建解題框架。之前以斐波那契數列爲例,這次我們以這道題目爲例。想知道到達n層臺階,有多少種方式,即f(n)是最終想要的答案,抽象爲f(i) 即到達任何一層臺階的方式;所謂子結構,即f(i-1),f(i-2),f(i-3),...,f(2),f(1)。很明顯,f(i)的方案數目和 f(i-1)等子問題的答案是有關係的。這就是第一步分析問題我們要做的。
- 狀態轉移方程。f(i)和子問題具體是什麼關係呢?這一步要定量分析。由於每次只能走一步或兩步,一步之前的臺階在i-1層,兩步之前在i-2層,那麼第i層的方案數目不就是:f(i) = f(i-1) + f(i-2),(還是斐波那契數列)。這一步定量分析問題和子問題之間的數量關係,
- 邊界。到這兒就很好理解了,可以定義 f(1)=1, f(2)=2.
- 其實還有第四步,就是編碼啦,能把你的思路寫出來纔行。
下面我們看代碼實現,在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]