LeetCode-动态规划

本文详细解析了LeetCode中动态规划类问题的解题思路,包括一维、二维动态规划,分割类、子序列问题,背包问题,字符串编辑和股票问题等,通过实例讲解了如何构建状态转移方程和边界条件,帮助读者掌握动态规划技巧。
摘要由CSDN通过智能技术生成

LeetCode-动态规划(持续更新中)

本文主要对LeetCode中的动态规划类型题进行解析,讲解一些问题的解题思路和动态规划的基础解法。

1.动态规划基础解法

我将动态规划基础解法分为三个步骤,具体如下:

  1. 定义数组dp,通过dp数组记录最终的函数返回值;
  2. 确认递推公式,确认数组之间的递推关系,如dp[i] = dp[i-1] + 1;
  3. 定义边界条件,确认边界处的临界值,如 dp[0] = 1 等。

2.动态规划题目类型

2.1 一维动态规划

70.爬楼梯

解题思路:
1.定义dp[i] 表示爬i层台阶的方法数;
2.找递推公式, 第i层台阶只能从 i-1层或i-2层爬上,故dp[i] = dp[i-1] + dp[i-2];
3.定义边界 dp[1] = 1,dp[2] = 2;

 public int climbStairs(int n) {
     if(n<=2){
         return n;
     }
     //定义数组 dp[i]表示爬i层台阶的方法数
     int[] dp = new int[n+1];
     // 定义递推关系: dp[i] = dp[i-1] + dp[i-2]
     // 定义边界条件
     dp[1] = 1;
     dp[2] = 2;
     for(int i=3;i<=n;i++){
         dp[i] = dp[i-1] + dp[i-2];
     }
     return dp[n];
 }

显然我们可以对这种解法进行空间优化,使用pre1、pre2、cur三个变量记录即可。

 public int climbStairs(int n) {
     if(n<=2){
         return n;
     }
     int pre2=1,pre1=2,cur=0;
     for(int i=3;i<=n;i++){
         cur = pre1 + pre2;
         pre2 = pre1;
         pre1 = cur;
     }
     return cur;
 }
198.打家劫舍

解题思路:
1.定义dp[i] 表示第i间房屋所能偷的最大金额;
2.找递推公式, i位置的金额与前一个和前二个相关,故递推公式:dp[i] = Math.max(dp[i-2],dp[i-1]);;
3.定义边界 dp[0] = nums[0]; dp[1] = Math.max(nums[0],nums[1]);;

 public int rob(int[] nums) {
     if(nums.length == 1){
         return nums[0];
     }
     if(nums.length == 2){
         return Math.max(nums[0],nums[1]);
     }
     // 定义数组dp[i] 表示第i间房屋所能偷的最大金额
     int[] dp = new int[nums.length];
     // 递推公式:dp[i] = Math.max(dp[i-2],dp[i-1]);
     // 边界:
     dp[0] = nums[0];
     dp[1] = Math.max(nums[0],nums[1]);
     for(int i=2;i<nums.length;i++){
         dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
     }
     return dp[nums.length-1];
 }

优化:

 public int rob(int[] nums) {
     if(nums.length == 1){
         return nums[0];
     }
     if(nums.length == 2){
         return Math.max(nums[0],nums[1]);
     }
     int pre2 = nums[0],pre1 = Math.max(nums[0],nums[1]),cur =0;
     for(int i=2;i<nums.length;i++){
         cur = Math.max(pre1,pre2+nums[i]);
         pre2 = pre1;
         pre1 = cur;
     }
     return cur;
 }

2.2 二维动态规划

基本问题:找出题目的动态转移方程。

64.最小路径和

解题思路:
1.首先定义二维数组dp[i][j] 表示从左上角到当前位置的最小路径和,定义返回值:dp[grid.length-1][grid[0].length-1];
2.找出数组之间关系的递推公式,上边界:dp[i][j] = grid[i][j] + dp[i][j-1]; 左边界:dp[i][j] = grid[i][j] + dp[i-1][j]; 内部:dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
3.确定边界条件:左上角的点:dp[i][j] = grid[i][j];

 public int minPathSum(int[][] grid) {
     // 定义数组 dp[i][j] 表示从左上角到i,j位置的最短路径
     int[][] dp = new int[grid.length][grid[0].length];
     // 定义数组递推关系 dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])
     // 定义边界条件
     for(int i=0;i<grid.length;i++) {
         for(int j=0;j<grid[0].length;j++){
             if(i == 0){
                 if(j == 0){
                     dp[i][j] = grid[i][j];
                     continue;
                 }
                 dp[i][j] = grid[i][j] + dp[i][j-1];
             }else if(j == 0){
                 dp[i][j] = grid[i][j] + dp[i-1][j];
             }else {
                 dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
             }
         }
     }
     return dp[grid.length-1][grid[0].length-1];
 }
542.01矩阵

解题思路:
1.定义数组dp[i[[j] 表示当前位置到0最近的距离;
2.递推公式:此题遍历分为两步骤,先从左上角往右下角遍历,再从右下角往左上角遍历;两次遍历过程会覆盖到所有的位置;公式与上题类似,不再赘述。
3.边界,需要保证初始数组中的每个值保证足够大;

 public int[][] updateMatrix(int[][] mat) {
     // 定义数组dp[i][j] 表示当前位置到周围0的最小距离
     int m = mat.length;
     int n = mat[0].length;
     int[][] dp = new int[m][n];
     // 遍历分为两步骤,先从左上角往右下角遍历,再从右下角往左上角遍历;
     for(int i=0;i<m;i++){
         for(int j=0;j<n;j++){
             dp[i][j] = Integer.MAX_VALUE-1;
             if(mat[i][j] == 0){
                 dp[i][j] = 0;
             }else {
                 if(i > 0){
                     dp[i][j] = Math.min(dp[i][j],dp[i-1][j]);
                 }

                 if(j > 0){
                     dp[i][j] = Math.min(dp[i][j],dp[i][j-1]);
                 }
             }
         }
     }

     for(int i=m-1;i>=0;i--){
         for(int j=n-1;j>=0;j--){
             if(mat[i][j] != 0){
                 if(i < m-1){
                     dp[i][j] = Math.min(dp[i][j],dp[i+1][j]);
                 }
                 if(j < n-1){
                     dp[i][j] = Math.min(dp[i][j],dp[i][j+1]);
                 }
             }
         }
     }

     return dp;
 }

2.3 分割类问题

对于分割类型的动态规划题目,状态转移方程往往取决于满足分割条件处的位置。

139.单词拆分

此题状态转移方程 dp[i] 与 dp[i-len] 分割位置的数组有关。

 public boolean wordBreak(String s, List<String> wordDict) {
     // 遍历字符串
     int n = s.length();
     // 记录状态
     boolean[] dp = new boolean[n+1];
     dp[0] = true;
     for(int i=1;i<=n;i++){
         for(String word:wordDict){
             int len = word.length();
             if(i >= len && s.substring(i-len,i).equals(word)){
                 dp[i] = dp[i] || dp[i-len];
             }
         }
     }
     return dp[n];
 }
279.完全平方数

此题的状态转移方程,dp[i] 与满足条件的 dp[i-j*j] 相关

 public int numSquares(int n) {
     // 定义数组dp[i]表示i数组最少组成的完全平方数;
     int[] dp = new int[n+1];
     dp[0] = 0;
     for(int i=1;i<=n;i++){
         dp[i] = Integer.MAX_VALUE;
     }
     for(int i=1;i<=n;i++){
         for(int j=1;j*j <= i;j++){
             dp[i] = Math.min(dp[i],dp[i - j*j] + 1);
         }
     }
     return dp[n];
 }

2.4 子序列问题

对于子序列问题的常见解法为:1.定义一个数组 dp[i] 表示 i 结尾的子序列的特性,处理好每个位置后,再统计一遍即可。

300.最长递增子序列
 public int lengthOfLIS(int[] nums) {
     // 定义数组 dp[i]表示 i位置的nums最长的递增子序列;
     int[] dp = new int[nums.length];
     // 边界条件
     for (int i = 0; i < dp.length; i++) {
         dp[i] = 1;
     }
     // 定义最大返回子序列长度
     int max = 0;
     for (int i = 0; i < nums.length; i++) {
         for (int j = 0; j < i; j++) {
             if(nums[i] > nums[j]) {
                 dp[i] = Math.max(dp[i],dp[j] + 1);
             }
         }
         // 统计一遍最大的dp[i]
         max = Math.max(max,dp[i]);
     }

     return max;
 }
583.两个字符串的删除操作
646.最长对数链

2.5 背包问题

背包问题:有 N 个物品和容量为 W 的背包,每个物品都有自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。
核心代码:
1.0-1背包,外层遍历物品,内层逆向遍历价值或重量;
2.完全背包:外层遍历物品,内层正向遍历价值或重量。

2.5.1 0-1背包问题

如果限定每种物品只能选择 0 个或 1 个,则此背包问题称为0-1背包问题。
解法1:

  1. 定义数组dp[i][j] 表示i个物品,容量为j所能取到的最大价值。
  2. 递推关系,当前位置存在取或不取两种状态,所以取两种情况的最大价值 dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w]+v);
  3. 边界条件:默认为0即可。
 public int maxBagValue(int[] weights,int[] values,int N,int W){
     // 定义数组 dp[i][j] 表示 i个物品,j个容量所能取到的最大价值;
     int[][] dp = new int[N+1][W+1];
     for (int i = 1; i <= N; i++) {
         int w = weights[i-1],v = values[i-1];
         for (int j = 1; j <= W; j++) {
             if (j >= w){
                 dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w]+v);
             }else {
                 dp[i][j] = dp[i-1][j];
             }
         }
     }
     return dp[N][W];
 }

在此我们可以发现递推公式之间,dp[i][] 只与dp[i-1][]相关,故可对解法1的空间进行优化。
解法2如下:

  1. 定义数组dp[i] 表示容量为i的背包所能取得的最大价值;
  2. 递推公式:dp[i] = Math.max(dp[i],dp[i-w]+v).
  3. 边界无。
 public int maxBagValue2(int[] weights,int[] values,int N,int W){
     // 定义数组 dp[i] 表示 重量为W的背包,所能取到的最大价值;
     int[] dp = new int[W+1];
     for (int i = 1; i <= N; i++) {
         int w = weights[i-1],v = values[i-1];
         // 需要倒序往前推,防止dp[j-w]不会被覆盖;若按照从左往右的顺序进行正向遍历,则 dp[j-w] 的值在遍历到
         // j 之前就已经被更新成物品 i 的值了
         for (int j = W; j >= w; j--) {
             dp[j] = Math.max(dp[j],dp[j-w]+v);
         }
     }
     return dp[W];
 }

同类题目:

416.分割等和子集

可以转换为是否可以获取数组总值一半的0-1背包问题

 public boolean canPartition(int[] nums) {
     // 等价于0-1背包问题
     int sum =0;
     for(int i=0;i<nums.length;i++){
         sum += nums[i];
     }

     if(sum % 2 != 0){
         return false;
     }

     int target = sum/2;

     // 定义数组dp[i] 表示数组是否能够取到目标值i
     boolean[] dp = new boolean[target+1];
     dp[0] = true;
     for(int i=0;i<nums.length;i++){
         for(int j=target;j>= nums[i];j--){
             dp[j] = dp[j] || dp[j-nums[i]];
         }
     }

     return dp[target];
 }
2.5.2 完全背包问题

同上0-1背包,核心代码可看背包问题下方区别。

 public int maxBagValue(int[] weights,int[] values,int N,int W){
     // 定义数组 dp[i] 表示 重量为W的背包,所能取到的最大价值;
     int[] dp = new int[W+1];
     for (int i = 1; i <= N; i++) {
         int w = weights[i-1],v = values[i-1];
         for (int j = w; j < W; j++) {
             dp[j] = Math.max(dp[j],dp[j-w]+v);
         }
     }
     return dp[W];
 }
322. 零钱兑换

外层遍历物品,内层正向遍历体积或价值。

 public int coinChange(int[] coins, int amount) {
     // 完全背包问题
     if(amount == 0){
         return 0;
     }
     // 定义数组 dp[i] 表示凑成i所需的最小硬币个数
     int[] dp = new int[amount+1];
     for(int i=1;i<dp.length;i++){
     	 // 保证初始值足够大
         dp[i] = amount+2;
     }
     dp[0] = 0;
     for(int i=0;i<coins.length;i++){
         for(int j=coins[i];j<=amount;j++){
             dp[j] = Math.min(dp[j],dp[j-coins[i]] + 1);
         }
     }

     return dp[amount] == amount+2?-1:dp[amount];

 }

2.6 字符串编辑问题

72.编辑距离

1.此题我们需要维护一个二维数组dp[]i[j] 表示 word1 到 i 位置和 word2 到 j 位置所需要的最小编辑数;
2. 当 i 和 j 位置的字符相同时,dp[i][j] = dp[i-1][j-1]; 当 i 和 j 位置的字符不同时,修改的需要 dp[i][j] = dp[i-1][j-1] + 1; 插入 i 位置/删除 j 位置的是 dp[i][j-1] + 1,插入 j 位置/删除 i 位置的是 dp[i-1][j] + 1 。

  public int minDistance(String word1, String word2) {
      int m = word1.length();
      int n = word2.length();
      int[][] dp = new int[m+1][n+1];
      for (int i = 0; i <= m; i++) {
          for(int j = 0;j <= n;j++) {
              if (i == 0){
                  dp[i][j] = j;
              }else if (j == 0){
                  dp[i][j] = i;
              }else{
                  dp[i][j] = Math.min(dp[i-1][j-1] + (word1.charAt(i-1) == word2.charAt(j-1)?0:1),Math.min(dp[i-1][j]+1,dp[i][j-1]+1));
              }
          }
      }
      return dp[m][n];
  }

2.7 股票问题

121.买卖股票的最佳时机

此题的主要解题思路在于,记录买入的最小值,同时记录卖出的最大利润即可。

   public int maxProfit(int[] prices) {
       // sell表示卖出的最大值,buy表示买入的最小值
       int sell=0,buy = Integer.MAX_VALUE;
       for(int i=0;i<prices.length;i++){
           buy = Math.min(buy,prices[i]);
           sell = Math.max(sell,prices[i] - buy);
       }
       return sell;
   }
188.买卖股票的最佳时机IV

此题的解题思路在于,维护两个数组buy[i] 表示在i时刻买入股票时取得的最大利润,sell[i] 表示在i时刻卖出的最大利润。同时区分K大于数组prices长度时,选择盈利即可。小于时正常处理。

    public int maxProfit(int k, int[] prices) {
        // 区分情况 k>prices.length 则赚钱就卖出
        if (k > prices.length){
            return profit(prices);
        }
        // 定义数组buy[j] 表示j时刻买入时的最大收入,sell[j] 表示j时刻卖出的最大收入
        int[] buy = new int[k+1];
        int[] sell = new int[k+1];
        for (int i = 0; i <= k; i++) {
            buy[i] = Integer.MIN_VALUE;
        }
        sell[0] = 0;

        for(int i=0;i<prices.length;i++){
            for(int j=1;j<=k;j++){
                // j位置买入的最大价值
                buy[j] = Math.max(buy[j],sell[j-1] - prices[i]);
                // j位置卖出的最大价值,此处转移方程
                sell[j] = Math.max(sell[j],buy[j] + prices[i]);
            }
        }

        return sell[k];
    }

    public int profit(int[] prices){
        int res = 0;
        for(int i=1;i<prices.length;i++){
            if(prices[i] > prices[i-1]){
                res += prices[i] - prices[i-1];
            }
        }

        return res;
    }
714.买卖股票的最佳时机含手续费

此题关键点在于找到状态转移方程,找到当前位置最大利润与前一步的关系。

  public int maxProfit(int[] prices, int fee) {
      // 股票
      int maxProfit = 0;
      // 定义数组,buy[i] sell[i]位置的数组分别表示买入和卖出的最大价值
      int[] buy = new int[prices.length];
      int[] sell = new int[prices.length];
      // 边界问题
      buy[0] = -prices[0];
      sell[0] = 0;

      for(int i=1;i<prices.length;i++){
          // 如果当前买入,最大值为前一个买入的最大值,或前一个位置卖出的最大值减当前位置买入
          buy[i] = Math.max(buy[i-1],sell[i-1] - prices[i]);
          // 建立状态机,如果当前卖出,取得最大值,前一个位置卖出的最大值或前一个买入的最大值+当前卖出
          sell[i] = Math.max(sell[i-1],buy[i-1]+prices[i]-fee);
      }
      return sell[prices.length-1];
  }
309、最佳买卖股票时机含冷冻期

此题需要四个数组表示状态转换,比较复杂,先不记录了…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值