Leetcode上動態規劃系列經典題的Java詳解

宅家防疫期間leetcode上小刷了十餘道線性動態規劃算法題,是時候自己總結提煉一下DP思想了。
我把總結的題目主要分爲兩類序列匹配類和生活類。
第一類的代表熱題:
leetcode300. 最長上升子序列
leetcode53.最大子序和
leetcode1143. 最長公共子序列
leetcode72. 編輯距離

第二類的代表熱題:
leetcode121. 買賣股票的最佳時機
leetcode122. 買賣股票的最佳時機 II
leetcode322. 零錢兌換
以下爲7道題的思路和代碼實現

300.最長上升子序列在這裏插入圖片描述

  • 思路 :用一個數組來存放上升序列,關鍵在於最後輸出的是序列長度,所以保證長度而不需要時刻記錄真正的子序。
class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length==0) return 0;
        for(int i=1; i<nums.length; i++){
            dp[1] = 1;
            for(int j=i-1; j>1; j--){
                 if(dp[j] < dp[i]) dp[j] = dp[j-1]+1; 
            }
        }
    }
}

之所以最先寫這道題,因爲之前用的是貪心算法,二分搜索比較難想到

public static int lengthOfLIS(int[]nums) {
        if(nums.length < 2) return nums.length;
        //LIS數組存放最長子序列的數組,但並非時刻都存放的是最長子序列
        int[] LIS = new int[nums.length]; 
        LIS[0] = nums[0];//數組有負整數的情況
        int end = 0;
        for(int i=1; i<nums.length; i++){
            //如果該位元素大於子序列最後一位,上升子序列變長1
            if(nums[i]>LIS[end]){
                end++;   LIS[end]=nums[i];
            }
 //如果當前nums[i]小於子序列最後一位,則用二分法搜索子序列中比nums[i]大的最小數
            else{
                int left = 0,right =end;
                while(left<right){
                    int pivot = left+(right-left)/2;
                    if( LIS[pivot]< nums[i]){
                        left = pivot+1;
                    }
                    else{
                        assert LIS[pivot] >= nums[i];
                        right = pivot;
                    }
                }
                LIS[right]=nums[i];
            }
        }
        return end+1;
    }

LeetCode53. 最大子序和

  • 思路:子序列問題難點在於是不連續的序列。定義dp[i] 表⽰以 nums[i] 這個數結尾的最⻓遞增⼦序列的⻓度。dp[i]依賴於dp[i-1]的結果,每次遞推都要max記錄,自底向上推算出最後結果啦。先用一個簡單的例子來推:nums = [1,-2,3],則dp[0] = nums[0] = 1;
  • step1:dp[1] = dp[0]+nums[1] = -1; 此時max =1>dp[1],所以當前max不變;
  • step2: dp[ 2] = dp[1] + nums[2] = 1;此時 max<dp[2],所以max更新爲dp[2]
  • 如此這般,max最後就是最大的dp[i]啦
    算法思路的動畫
   public int maxSubArray(int[] nums) {
		int[] dp = new int[nums.length];
		dp[0] = nums[0];   
		int max = nums[0];
		for (int i = 1; i < nums.length; i++) {
		//nums[i] > 0,說明對結果有增益,dp[i]再加當前遍歷值
		//nums[i] <= 0,說明對結果無增益,dp[i]直接更新爲當前遍歷數字
			dp[i] = Math.max(dp[i- 1] + nums[i], nums[i]);	
			if (max < dp[i]) {      //關鍵步:取每次遍歷的當前最大和
				max = dp[i];
			}
		}
		return max;
   }

LeetCode1143. 最長公共子序列

在這裏插入圖片描述在這裏插入圖片描述

  • 思路:
    這裏推薦youtube上一個白板演示算法思路的小哥,他的視頻講解的非常細,既可以幫助透徹理解,又可以提高下英語,兩全其美:【LeetCode算法詳解】最長公共子序列
    中間狀態都存在二維數組中,dp[i][j] 表示 串1的前 i 位和串2的前 j 位的位置。
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int n1 = text1.length(); int n2 = text2.length();
        int[][] dp = new int[n1+1][n2+1];
        for(int i=0; i<n1; i++) { dp[i][0] = 0; }
        for(int j =0; j<n2; j++){ dp[0][j] = 0; }
        for(int i=1; i<=n1; i++){
            for(int j =1; j<=n2; j++){
                if(text1.charAt(i-1)==text2.charAt(j-1))
                    dp[i][j] = dp[i-1][j-1] + 1;
                else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[n1][n2];
    }
}

LeetCode72. 編輯距離

題目

  • 思路:這類解決兩個字符串的動態規劃問題,可以用最長上升子序列一樣的方法,用兩個指針 i,j 分別指向兩個字符串的最後,然後一步步往前走,縮小問題的規模。
    dp[i][j]—word1的前 i 位轉換成 word2的前 j 位需要的最小步數
public int minDistance(String word1, String word2) {
        int n1 = word1.length(); int n2 = word2.length();
        int[][] dp = new int[n1+1][n2+1];
        for(int i=1; i<=n1; i++) dp[i][0] = dp[i-1][0] +1;   
        for(int j=1; j<=n2; j++) dp[0][j] = dp[0][j-1] +1;  
        for(int i=1; i<=n1; i++){
            for(int j=1; j<=n2; j++){
            //word1和word2的該位字符相同,不需要改動。
                if(word1.charAt(i-1)==word2.charAt(j-1))
                   dp[i][j] = dp[i-1][j-1];
            //如果字符不同,則取該步之前的狀態基礎上做刪除,修改,插入中的最小改動
                else 
                   dp[i][j] = Math.min(Math.min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1])+1;
            }
        }
        return dp[n1][n2];
    }

股票系列的問題推薦一個總結很到位的思路:一個方法團滅 6 道股票問題
在這裏插入圖片描述

  • 思路:前 i 天所獲利潤以來於前 i- 天利潤;第 i 天是如果 “買入股票”,就要從利潤中減去 prices[i],如果“賣出股票”,就要給利潤增加 prices[i]。此時最大利潤就是這兩種可能選擇中較大的那個。
    因此可有最優子結構:
    dp[i][0]表示第i天不持股票時擁有的利潤;dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
    而dp[i][1]表示第i天持有股票所獲利潤 dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
class Solution {
   public int maxProfit(int[] prices) {
        int n = prices.length;
        if(prices.length==0)return 0;
        int[][] dp = new int[n][2]; //行表示第 i天,列表示是否買入當天股票
        dp[0][0] = 0; //i = 0 時 dp[i-1] 是不合法的。
        dp[0][1] = -prices[0];
        for (int i = 1; i < n; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
        }
        return dp[n - 1][0];
    }
}

在這裏插入圖片描述
+思路:和上一題的思路基本一致,區別在於交易次數從 1 變成 無限次
那麼dp[i][1] 就等於dp[i-1][1]和dp[i-1][0]-prices[i]中更大的結果。因爲前 i 天也可以有買入交易了,也就是買股票的第 i天所獲利潤和第 i-1 直接相關。

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length==0) return 0;
        int[][] dp = new int[prices.length][2];
        dp[0][0] = 0; 
        dp[0][1] = -prices[0];
        for(int i=1; i<prices.length;i++){
              dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
              dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
        } 
        return dp[prices.length-1][0];
    }
}

leetcode322. 零錢兌換

在這裏插入圖片描述

  • 思路:最優化狀態dp[i]表示金額 i 需要的最少硬幣個數。要注意的問題在於初始化時每個dp[]數都要大於金額總數,是極端假設硬幣都是 1的情況。
class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount+1];
      // 注意:因爲要比較的是最小值,不可能是結果的初始化值就得賦一個最大值
        Arrays.fill(dp, amount + 1);
        dp[0] =0;
        for(int i=1; i<=amount; i++){
            for(int coin: coins){
        //如果可包含coin,那麼剩餘錢是i−coins,要兌換的硬幣數是 dp[i−coins]+1
                if(coin <= i)
                   dp[i] = Math.min(dp[i],dp[i-coin]+1);
            }
        }
        return dp[amount]<=amount ? dp[amount] : -1;
    }
}

如果這篇文章對你有幫助,請點贊讓我知道吧,我會更加努力給大家分享好的內容,謝謝~

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