宅家防疫期间leetcode上小刷了十余道线性动态规划算法题,是时候自己总结提炼一下DP思想了。
我把总结的题目主要分为两类序列匹配类和生活类。
第一类的代表热题:
leetcode300. 最长上升子序列

leetcode53.最大子序和

leetcode1143. 最长公共子序列

leetcode72. 编辑距离

第二类的代表热题:
leetcode121. 买卖股票的最佳时机

leetcode122. 买卖股票的最佳时机 II

leetcode322. 零钱兑换

以下为7道题的思路和代码实现

300.最长上升子序列

java中编写线性规划 java线性规划最优解类库_leetcode

  • 思路 :用一个数组来存放上升序列,关键在于最后输出的是序列长度,所以保证长度而不需要时刻记录真正的子序。
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. 最长公共子序列

java中编写线性规划 java线性规划最优解类库_算法_02

java中编写线性规划 java线性规划最优解类库_java_03

  • 思路:
    这里推荐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. 编辑距离

java中编写线性规划 java线性规划最优解类库_java_04

  • 思路:这类解决两个字符串的动态规划问题,可以用最长上升子序列一样的方法,用两个指针 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 道股票问题

java中编写线性规划 java线性规划最优解类库_java中编写线性规划_05

  • 思路:前 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];
    }
}

java中编写线性规划 java线性规划最优解类库_算法_06


+思路:和上一题的思路基本一致,区别在于交易次数从 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. 零钱兑换

java中编写线性规划 java线性规划最优解类库_算法_07

  • 思路:最优化状态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;
    }
}