談到動態規劃,最經典的當然是揹包問題,可以百度下<<揹包九講>>。
但是這裏不準備從揹包問題講起,主要是覺得用揹包問題來講DP的思想,還不夠通俗易懂。
先來看一個金典的算法題:
爬臺階問題
有n個臺階,你每次可以爬1階或2階,問:爬到頂總共有多少種爬法?
例子: n = 3
第一種:1 1 1
第二種:2 1
第三種:1 2
總共有3種爬法。
解法一:遞歸
代碼如下:
public int climbStairs(int n) {
return (n<=2) ? n : climbStairs(n-1) + climbStairs(n-2);
}
如果n=1,只有一種爬法。
如果n=2,有[(1,1),(2)]兩種爬法
如果n>2,分兩種情況:
- 第一種,最後一次爬了1階,climbStairs(n-1)
- 第一種,最後一次爬了2階,climbStairs(n-2)
所以n>2時,climbStairs(n) = climbStairs(n-1) + climbStairs(n-2);
遞歸的思想是:至上而下,想求climbStairs(n) ,轉化爲求climbStairs(n-1)和climbStairs(n-2)的問題
解法二:DP
public int climbStairs(int n) {
if(n <= 2) return n;
//dp[i] = m ,表示高度爲i的臺階,有m種爬法
int[] dp= new int[n+1];
dp[1] = 1;
dp[2] = 2;
for(int i=3; i <= n ; i++)
dp[i] = dp[i-1] + dp[i-2];
return dp[n];
}
比如這裏n=4,數組dp的長度爲5,值爲:
dp = [0,1,2,3,5]
dp[4]=5,表示高度爲4的臺階有5種爬法。
DP的思想是至下而上的,想求dp[n]需要把前面從0到n-1的值全部求出來,而且是從0開始
DP的關鍵是能否通過之前求的值,推導出當前的值 也就是這行代碼:
dp[i] = dp[i-1] + dp[i-2]
如果dp[i]無法通過之前的值求得,那麼就無法使用dp求解。
而dp[i] = dp[i-1] + dp[i-2]
叫狀態轉移方程。DP求解的關鍵就是找到合適的狀態轉移方程
由於這個問題比較簡單,這個狀態轉移方程很容易理解。
與上一種遞歸解法相比,它的時間複雜度肯定要低些,但是他需要一個長度爲n+1的數組輔助,所以空間複雜度爲O(n+1)。
解法三:優化DP的空間複雜度
public int climbStairs(int n) {
if(n <= 2)
return n;
int first = 1;
int second = 2;
for(int i=3; i <= n ; i++){
int third = first + second;
first = second;
second = third;
}
return second;
}
對比上面的DP,這裏應該很容易理解,由於我們需要的是dp[n],而dp[0~n-1]我們沒必要一直存着,空間複用就行。優化後,這裏的空間複雜度爲O(1)。
從n=4,dp = [0,1,2,3,5]的例子不難看出,[1,2,3,5]其實就是一個斐波那契數列。所以這個題其實就是求解斐波那契數列。以上三種均不是最優解。存在時間複雜度爲O(log(n))的解法,感興趣可以去leetcode上去看看矩陣的解法。
推薦另一道比較有趣的DP算法題:house robber