LeetCode|動態規劃入門三題
一、爬樓梯
假設你正在爬樓梯。需要 n階你才能到達樓頂。
每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數。
示例 1:
輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
- 1 階 + 1 階
- 2 階
示例 2:
輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
- 1 階 + 1 階 + 1 階
- 1 階 + 2 階
- 2 階 + 1 階
解題思路
本題是動態規劃裡面最簡單的題目了,印象中第一次見到這個題目還是在藍橋杯的練習題中。
雖然本篇是動態規劃的入門文章,但是我並不會說任何書面上寫的動態規劃演算法的定義。這是因為動態規劃的概念還是很複雜的,看完動態規劃演算法的思路和特性估計你就被動態規劃嚇到了。
關於動態規劃的定義,我只說一句個人看法——動態規劃求的是全域性最優解。全域性意味著動態規劃是要全盤考慮問題的。話不多說,我們先從題目解答中開始體會。
本題中,我們看到,爬1階梯只有1種方法,爬2階梯有2種方法。那我們就能想到,爬3階梯有3種,這是因為爬3階等於爬2階的基礎上再爬1階。同理爬4階等於在3階的基礎上爬1階和2階的基礎上爬2階;
這種“站在巨人的肩膀上”的思路就是動態規劃的思想。
實現程式碼
首先我們能想到的是使用遞迴的方法。遞迴方法跟我們上面的思路正好反過來。遞迴的思路是爬n階樓梯的方式等於爬n-1階樓梯的次數加上n-2階樓梯的次數。但是遞迴方法不會儲存計算結果,導致有很多重複的計算,因此超時也就不可避免了。
1. 遞迴
class Solution { public int climbStairs(int n) { if(n==1) return 1; if(n==2) return 2; return climbStairs(n-1)+climbStairs(n-2); } }
執行結果:超時
2. 動態規劃
動態規劃的整體思路跟遞迴很像,但是會通過一個dp陣列儲存計算結果。這樣計算速度就會快很多。
class Solution { public int climbStairs(int n) { int[] steps=new int[n]; if(n==1){ return 1; } steps[0]=1; steps[1]=2; for(int i=2;i<n;i++){ steps[i]=steps[i-1]+steps[i-2]; } return steps[n-1]; } }
二、連續子陣列的最大和
給定一個整數陣列 nums,找到一個具有最大和的連續子陣列(子陣列最少包含一個元素),返回其最大和。
示例 1:
輸入:nums = [-2,1,-3,4,-1,2,1,-5,4]
輸出:6
解釋:連續子陣列[4,-1,2,1] 的和最大,為6 。
示例 2:
輸入:nums = [1]
輸出:1
示例 3:
輸入:nums = [0]
輸出:0
示例 4:
輸入:nums = [-1]
輸出:-1
示例 5:
輸入:nums = [-100000]
輸出:-100000
提示:
1 <= nums.length <= 3 * 104
-105 <= nums[i] <= 105
進階: 如果你已經實現複雜度為 O(n) 的解法,嘗試使用更為精妙的 分治法 求解。
解題思路
動態規劃是本題的最優解。動態規劃的思路也很清晰。假設有一個數組[-2,1]
,那麼我們可以看到[-2,1]
兩數的和比陣列第二個元素1要小。因此連續子陣列的最大和為1。當陣列變為[-2,1,-3]
的時候,我們已經知道[-2,1]
這部分陣列的連續子陣列的最大和為1。那麼1跟[-3]
比較是1比較大,因此這個陣列的連續子陣列的最大和依然為1。
簡而言之,每次陣列新增一個元素,都需要跟增加前的陣列的連續子陣列的最大和相比較,結果較大的那個就是新增後陣列的連續子陣列的最大和。程式碼實現如下:
實現程式碼
class Solution {
public int maxSubArray(int[] nums) {
int dp[]=new int[nums.length];
dp[0]=nums[0];
int max=dp[0];
for(int i=1;i<dp.length;i++){
//compare the num last status plus current num and current num
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
//compare max and dp[i],it will update max
max=Math.max(max,dp[i]);
}
return max;
}
}
三、遊戲幣組合
硬幣。給定數量不限的硬幣,幣值為25分、10分、5分和1分,編寫程式碼計算n分有幾種表示法。(結果可能會很大,你需要將結果模上1000000007)
示例1:
輸入: n = 5
輸出:2
解釋: 有兩種方式可以湊成總金額:
5=5
5=1+1+1+1+1
示例2:
輸入: n = 10
輸出:4
解釋: 有四種方式可以湊成總金額:
10=10
10=5+5
10=5+1+1+1+1+1
10=1+1+1+1+1+1+1+1+1+1
說明:
注意:
你可以假設:
0 <= n (總金額) <= 1000000
解題思路
這道題的狀態轉移方程似乎不那麼容易看出來,不過我們可以列一個表格來觀察一下,看能不能發現一點什麼規律。
硬幣數\總面值 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
2 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
3 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
4 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
5 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |
6 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
7 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
f(n) | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 4 |
首先我們定義一個dp陣列和所有的硬幣陣列
int[] dp = new int[n + 1];
int[] coins = {1,5,10,25};
並且設定dp[0]=1,為邊界的條件,作為完美能被一個硬幣表示的情況為 1。
dp[i]+=dp[i-coin]
當i-coin為0時結果為1,表示一個硬幣能表示的情況。
我們可以簡單的得出狀態轉移方程
// 題目中需求對結果取模1000000007
dp[i] = dp[i] + dp[i - coin]
程式碼實現
class Solution {
public int waysToChange(int n) {
int dp[]=new int[n+1];
int coins[]={1,5,10,25};
dp[0]=1;
for(int coin : coins) {
for(int i = coin; i <= n; i++) {
dp[i] = (dp[i] + dp[i - coin]) % 1000000007;
}
}
return dp[n];
}
}