LeetCode刷题笔记(7):动态规划练习

本文围绕LeetCode上的动态规划题目展开,涵盖打家劫舍II、最大子数组和等多道题。针对不同题目给出动态规划解法,如分情况讨论、状态转移方程推导等,还介绍了空间压缩方法。最后总结了常用动态规划模型,包括序列字符串问题、背包问题和股票交易问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

213. 打家劫舍 II

本题是198题的变形题。把线性的一维数组变成了环形数组,这导致第0个房间和第n-1个房间不能同时抢劫。因此我们可以分成两种情况讨论:

1)抢0号房间,然后对[1, n-2]进行动态规划过程;

2)不抢0号房间,然后对[1, n-1]进行动态规划过程。

class Solution {
public:
    int rob(vector<int>& nums) {
      int n = nums.size();
      int pre1=nums[0], pre2=nums[0], cur1=nums[0],cur2=0;
      //抢第一个房间
      for(int i=2;i<n-1;i++){
        cur1 = max(pre1 + nums[i], pre2);
        pre1 = pre2;
        pre2 = cur1;
      }

      //不抢第一个房间
      pre1 = 0, pre2 = 0;
      for(int i=1;i<n;i++){
        cur2 = max(pre1+nums[i],pre2);
        pre1 = pre2;
        pre2 = cur2;
      }
      return max(cur1,cur2);
    }
};

53. 最大子数组和

设dp[i]表示以nums[i]结尾的最大连续子数组的和,则dp[i] = max(dp[i-1] +nums[i], nums[i])

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
      int n = nums.size();
      vector<int> dp(n);
      dp[0] = nums[0];
      int ans = nums[0];
      for(int i=1; i<n; i++){
        dp[i] = max(dp[i-1] + nums[i], nums[i]);
        ans = max(ans,dp[i]);
      }
      return ans;
    }
};

空间压缩

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
      int n = nums.size();
      int ans = nums[0], pre = nums[0], cur;
      for(int i=1; i<n; i++){
        cur = max(nums[i], pre + nums[i]);
        pre = cur;
        ans = max(ans,cur);
      }
      return ans;
    }
};

343. 整数拆分

动态规划方法:

设dp[i]表示i拆分得到整数积的最大值。我们很自然想到把 i 拆分成 j 和 i-j,那么dp[i] = max(dp[j]*dp[i-j]) 1<=j<=i/2。测试后,发现这样有个漏洞,所有结果都为1,因为j和i-j会被一直拆分下去,所以相当于把n拆分成n个1相乘。对于j和i-j可以不继续拆分,直接用j或i-j乘。因此正确的状态转移方程为

dp[i] = max( max(j, dp[j]) * max(i-j, dp[i-j]) ) 1<=j<=i/2

class Solution {
public:
    int integerBreak(int n) {
      vector<int> dp(n+1,0);
      dp[1] = 1;
      for(int i=2; i<=n; i++){
        for(int j=1; j<i/2; j++){
          dp[i] = max(dp[i], max(j,dp[j])*max(i-j,dp[i-j]));
        }
      }
      return dp[n];
    }
};

583. 两个字符串的删除操作

删除操作数 = m + n - 2*l(m和n为两字符串长度,l为LCS长度)。因此可以直接用LCS方法求出最长公共子序列长度,再用上述公式。使用空间压缩减少空间复杂度。

class Solution {
public:
    int minDistance(string word1, string word2) {
      int m = word1.length(), n = word2.length();
      if(m<n){
        swap(m,n);
        swap(word1,word2);
      }
      vector<int> dp(n+1,0);
      for(int i=1;i<=m;i++){
        int pre = 0;
        for(int j=1;j<=n;j++){
          int next = dp[j];
          if(word1[i-1] == word2[j-1]){
            dp[j] = pre + 1;
          }else{
            dp[j] = max(dp[j-1],dp[j]);
          }
          pre = next;
        }
      }

      return m+n-2*dp[n];
    }
};

也可以直接针对本题列出状态转移方程,

dp[i][j] = dp[i-1][j-1] if word1[i-1] == word[j-1]

dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1 if word1[i-1] != word[j-1]

注意初值为dp[i][0] = i, dp[0][j] = j;

同样进行空间压缩。

class Solution {
public:
    int minDistance(string word1, string word2) {
      int m = word1.length(), n = word2.length();
      if(m<n){
        swap(m,n);
        swap(word1,word2);
      }
      vector<int> dp(n+1);
      //初值dp[0][j] = j, dp[i][0] = i;
      for(int j=0;j<=n;j++){
        dp[j] = j;
      }
      for(int i=1;i<=m;i++){
        int pre = i-1;//pre保存dp[i-1][j-1]
        dp[0] = i;//dp[i][0]初值为i       
        for(int j=1;j<=n;j++){
          int next = dp[j];
          if(word1[i-1] == word2[j-1]){
            dp[j] = pre;
          }else{
            dp[j] = min(dp[j-1],dp[j]) + 1;
          }
          pre = next;
        }
        
      }

      return dp[n];
    }
};

646. 最长数对链

本题是300题最长递增子序列的变种题,只不过把单个数字换成了数对,且允许以任何顺序使用数对。

方法一:简单动态规划

先将数对按第一个数字从小到大排序。设dp[i]表示以pairs[i]结尾的最长数对长度。则dp[i] = max(dp[j] + 1) ,  if pairs[j][1] < pairs[i][0] && 0<=j<i。

class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        int n = pairs.size();
        vector<int> dp(n,1);
        sort(pairs.begin(),pairs.end(),[](vector<int> a, vector<int> b){
          if(a[0]!=b[0]) return a[0] < b[0];
          else return a[1]<b[1];
        });
        for(int i=1;i<n;i++){
          for(int j=0;j<i;j++){
            if(pairs[j][1] < pairs[i][0]){
              dp[i] = max(dp[i], dp[j]+1);
            }
          }
        }
        return dp[n-1];
    }
};

方法二:动态规划+二分法

设dp[k]存储长为k+1的数对链最小的最后一个数对的后一个数字。之后遍历一遍数组,如果pairs[i][0] > dp.back(),则直接添加到dp末尾;否则找到pairs[i]能插入的位置,并更新对应最小值。排序过程,实际只需按pair[i][1]排序即可,使第2个数字尽量小,更容易插入新数对。

class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        int n = pairs.size();
        vector<int> dp;//dp[k]存储长为k+1的数对链最后一个数对的后一个数字        
        sort(pairs.begin(),pairs.end(),[](vector<int> a, vector<int> b){
          return a[1]<b[1]
        });
        dp.push_back(pairs[0][1]);
        for(int i=1;i<n;i++){
          //int size = dp.size()-1;
          if(pairs[i][0] > dp.back()){
            dp.push_back(pairs[i][1]);
          }else{
            auto it = lower_bound(dp.begin(), dp.end(), pairs[i][0]);
            if(*it > pairs[i][1]) *it = pairs[i][1];//排序后,*it肯定小于pairs[i][1],根本不执行
          }
        }
        return dp.size();
    }
};

方法三:贪心

进一步思考,发现dp[k]存储的中间结果,实际上是不需要的。在最长递增子序列问题中,由于数组本身是无序的,所以当前最长的子序列可能在后序遍历中被更短的子序列超过长度,因此需要始终维护每个长度结果。而这里我们先排序了,后面进来的元素pairs[i][1]肯定大于之前的,因此只要执行pairs[i][0] > dp.back()这一部分即可。

class Solution {
public:
    int findLongestChain(vector<vector<int>>& pairs) {
        sort(pairs.begin(), pairs.end(), [](vector<int> &a, vector<int>b) {
            return a[1] < b[1];
        });
        int n = pairs.size(), ans = 1, r = pairs[0][1];
        for (int i = 1; i < n; i++) {
           if (pairs[i][0] > r) {
               ans++;
               r = pairs[i][1];
           } 
        }
        return ans;
    }
};

376. 摆动序列

方法一:

设dp[i]表示以nums[i]结尾的最长摆动子序列长,diff[i]以nums[i]结尾的最长摆动子序列的最后一个差值,则dp[i] = max(dp[j] + 1), if 0<=j<i && nums[i] - nums[j] 与 diff[j]异号。时间复杂度为O(n)

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
      int n = nums.size();
      
      vector<int> dp(n,1), diff(n,0);
      int ans = 1;
      for(int i=1; i<n; i++){
        for(int j=0; j<i; j++){
          int dij = nums[i] - nums[j];
          if((j==0 && dij!=0) || (dij<0 && diff[j]>0) || (dij>0 && diff[j]<0)){//与上一结尾差值异号
          //(j==0 && dij!=0) 与首元素不等时也要更新 
             dp[i] = max(dp[i], dp[j]+1);
             diff[i] = dij;
          }
        }
        ans = max(ans,dp[i]);
      }
      return ans;
    }
};

方法二:

考虑到摆动序列是正负交替出现,那么可以分开讨论,减少复杂度。

摆动序列可分为:最后一个元素上升的上升摆动序列和最后一个元素下降的摆动序列。

设up[i]和down[i]分别表示前i个元素中最长的上升摆动序列和下降摆动序列长度,这样一来up[i]和down[i] 都只取决于up[i-1]和dowm[i-1],不需要从0开始遍历。

对up[i]:

1) 当nums[i] <= nums[i-1]时,如果需要nums[i]结尾,同样可以用nums[i-1]代替,故up[i] = up[i-1];

2) 当nums[i] > nums[i-1]时,由摆动序列定义,up[i] = max(up[i-1], down[i-1]+1);

同理对down[i]:

1) 当nums[i] >= nums[i-1]时,down[i] = down[i-1];

2) 当nums[i] < nums[i-1]时,down[i] = max(up[i-1]+1, down[i-1]);

class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
      int n = nums.size();     
      vector<int> up(n), down(n);
      up[0] = down[0] = 1;
      for(int i=1; i<n; i++){
        if(nums[i]<nums[i-1]){
          up[i] = up[i-1];
          down[i] = max(up[i-1]+1, down[i-1]);
        }else if(nums[i]>nums[i-1]){
          down[i] = down[i-1];
          up[i] = max(down[i-1]+1, up[i-1]);
        }else{
          up[i] = up[i-1];
          down[i] = down[i-1];
        }
      }
      return max(up[n-1], down[n-1]);
    }
};

总结:从本题可以看出,对序列或字符串(设为A)动态规划问题,在状态设计时,有两种思路:

1)令dp[i]表示以A[i]结尾或开头的XX,这样设计状态转移方程一般比较简单,但可能要考察i前面多项的关系,如从[0,i-1]中选出符合条件的最优值;

2)令dp[i]表示A[i]的前i项范围中满足XX,这样设计状态转移方程比较复杂,但通常就dp[i]就只与前面dp[i-1]有关,复杂度较低。

494. 目标和

本题是0-1背包问题的变式,0-1背包问题是在给定体积下使物品价值尽量大,我们要把握好体积和价值这两个量。对于本题,体积限制对应目标和target,价值对应方案数。因此我们可以设dp[i][j]表示前i项中组合得到j的方案数,对第i个数,在组合时有+和-两种选择,因此状态转移方程为:

dp[i][j] = dp[i-1][j-w] + dp[i-1][j+w]; 其中w=nums[i]

 由于和范围可能在[-sum, sum]之间,整体平移sum,对应于j范围是[0,2*sum]

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size();
        int sum = accumulate(nums.begin(),nums.end(),0);
        target += sum;        
        if(target>2*sum || target < 0) return 0;
        vector<vector<int>> dp(n, vector<int>(2*sum + 1,0));

        int w = nums[0];
        ++dp[0][w+sum], ++dp[0][sum-w];//w=0时,sum位置应为2,故采用自加赋值        
        for(int i=1; i<n;i++){
          w = nums[i];
          for(int j=0; j<2*sum+1;j++){            
            if((j-w)>=0 && (j-w)<(2*sum+1)) dp[i][j] += dp[i-1][j-w];
            if((j+w)<(2*sum+1)) dp[i][j] += dp[i-1][j+w];
          }
        }
        return dp[n-1][target];
    }
};

同样进行空间压缩

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size();
        int sum = accumulate(nums.begin(),nums.end(),0);
        target += sum;        
        if(target>2*sum || target < 0) return 0;
        vector<int> dp(2*sum + 1,0);

        int w = nums[0];
        ++dp[w+sum], ++dp[sum-w];//w=0时,sum位置应为2,故采用自加赋值
        for(int i=1; i<n;i++){
          w = nums[i];
          vector<int> temp(2*sum+1,0);
          for(int j=0; j<2*sum+1;j++){                        
            if((j-w)>=0 && (j-w)<(2*sum+1)) temp[j] = dp[j-w];
            if((j+w)<(2*sum+1)) temp[j] += dp[j+w];
          }
          swap(temp,dp);
        }
        return dp[target];
    }
};

当然本题还可进一步改进。

设所有元素的和为sum,取负号的元素和为neg,则题目要求为sum - 2*neg = target,即

neg = (sum - target)/2

 若sum - target小于0或不是2的倍数则一定不能实现,直接return 0.

若在此范围内,则题目变为从nums数组中选出若干数字,使之和为neg,一个标准的0-1背包问题。设dp[i][j]表示前i个数中选取若干数得到和为j的方案数。

边界条件:当没有任何元素可以选取时,元素和只能是 0,对应的方案数是 1。

使用空间压缩

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size();
        int sum = accumulate(nums.begin(),nums.end(),0);              
        if(sum<target || (sum-target)%2!=0) return 0;
        int neg = (sum - target)/2;
        vector<int> dp(neg+1,0);
        dp[0] = 1;//dp[0][0]=1,0个数得到0,方案为1
        for(int i=0; i<n;i++){
          int w = nums[i];
          for(int j=neg; j>=0;j--){                        
            if(j>=w) dp[j] = dp[j-w] + dp[j];            
          }          
        }
        return dp[neg];
    }
};

动态规划总结

常用的动态规划模型问题:

1.序列、字符串性质类问题

这是最常见的动态规划问题。如最长递增子序列,最长公共子序列LCS等问题,一般是从序列中选出满足性质的子序列。一般思路为:

1)令dp[i]表示以A[i]结尾、或前i项中满足性质的XXX,

2)令dp[i][j]表示A[i]到A[j]范围内的XXX

在设计状态转移方程时,如果dp[i]设成前i项中XXX,则一般要考虑前面多项;如果dp[i]表示以A[i]结尾的XXX则一般考虑相邻项;另外,如果是分割类型的问题,则一般考虑分割位置。

2、背包问题

包括0-1背包问题和完全背包问题两大类。背包问题可以表述为:给定总体积V下,从n项物品做出价值W最大的选择。关键在于找准问题中的体积V和价值W是什么。同时学会使用空间压缩的方法减少空间复杂度。

3、股票交易问题

通解情况参看文章:https://leetcode-cn.com/circle/article/qiAgHn/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值