1. 程式人生 > 其它 >LeetCode|動態規劃入門三題

LeetCode|動態規劃入門三題

一、爬樓梯

假設你正在爬樓梯。需要 n階你才能到達樓頂。
每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
注意:給定 n 是一個正整數。

示例 1:

輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。

  1. 1 階 + 1 階
  2. 2 階

示例 2:

輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。

  1. 1 階 + 1 階 + 1 階
  2. 1 階 + 2 階
  3. 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];
    }
}