代码随想录:动态规划|子序列问题全集

希望通过这篇文章能看到你的收获和感悟,或许你有更好的理解与建议与我沟通交流,
希望能看到你的留言,即使一句话也非常有意义

300. 最长递增子序列


动态规划6部曲:

1.问题分析与转化 类比背包问题 物品:序列中的元素,背包容量:序列的长度。 求最长子序列,这个物品必须得装,如果不装,不能从当前位置转化到下一个位置。

2.dp含义:dp[i][j] 记录以nums[i]结尾最长递增子序列的长度

3.递推公式: 是找状态的转化过程!如何由上一个推导当前的状态?
对于当前位置元素 nums[i],从i 位置前面的序列(j < i)中找到一个比它小的元素nums[j] ,然后从这个位置添加nums[i] ,之后我们可以更新长度 dp[i] = dp[j] + 1

比如:[1, 4, 2, 3, 4, 6]
遍历过程:[1,6], [1, 4, 6], [2, 6], [2, 3, 6],[2, 3, 4, 6], 保存最长序列的长度dp[5]=4。

4.遍历顺序: 前一个状态到当前状态,自然的从前向后遍历。但内层循环也可以从后向前,为什么?根据递推公式的原理可以找到答案,我们记录的是最大值,跟顺序无关。

5.初始化 全置为1。 递推中求最大值,我们就去找最小值。最小长度递增子序列只包含自己1个元素,长度为1。

6.边界处理与优化 i可以从1开始。dp[0]包含一个元素已经初始化为1了。j < i 从i位置前边找一个数

class Solution {
 public:
  /**
   * @brief dp[i]记录以nums[i]结尾的最长递增的子序列长度(可以不连续但严格递增)
   * 然后返回dp[i]的最大值
   */
  int lengthOfLIS(vector<int>& nums) {
    vector<int> dp(nums.size(), 1);
    // 类比背包问题,外层是容量
    for (int i = 1; i < nums.size(); ++i) {
      // 内层扫描物品
      for (int j = 0; j < i; ++j) {
        // 如果找到比当前物品小的值,递增序列从j位置添加nums[i]元素。长度+1
        if (nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1);
      }
    }
    return *max_element(dp.begin(), dp.end());
  }
};

674. 最长连续递增子序列


思路就是:记录每一段的连续递增序列长度,然后选出最大值。

优化版本

class Solution {
 public:
  int findLengthOfLCIS(vector<int>& nums) {
    int res = 1;
    int sub = 1; ///< 连续递增序列长度
    for (int i = 1; i < nums.size(); ++i) {
      if (nums[i] > nums[i - 1]) {
      	//记录连续递增的长度 并更新最大值
        res = max(res, ++sub);
      } else {
      // 注意不连续的时候,从1开始计算
        sub = 1;
      }
    }
    return res;
  }
};

或者

int _findLengthOfLCIS(vector<int>& nums) {
    int res = 1;
    int sub = 1;
    for (int i = 1; i < nums.size(); ++i) {
      if (nums[i] > nums[i - 1]) {
      // 连续递增序列长度
        sub++;
      } else {
      // 求最大值
        res = max(res, sub);
        sub = 1;
      }
    }
    return res;
  }

718. Maximum Length of Repeated Subarray

思路: 把两个序列进行逐个元素对比A[1,2,3,2,1],B[3,2,1,4,7]

用双层循环,逐个位置对比,如果元素相等,那么他们就是由上一个dp存储的长度+1。

上一个dp是什么?dp[i-1][j-1]即A上一个元素和B中上一个元素的比较结果。i和j都需要减去1,即A序列要回退一个位置,B序列也要回退。

例子:
比较两个序列A[1,2,3,2,1],B[3,2,1,4,7],
dp[i-1][j-1]就是A[1,2,3,2,1],B[3,2,1,4,7]

看图理解,加深印象
在这里插入图片描述

确定dp含义:dp[i][j] 表示以 nums1[i-1]nums2[j-1] 结尾的最长重复子数组的长度。 注意:dp中索引代表第几个元素,dp表示第i个nums1元素nums1[i-1],第j个nums2元素nums2[j-1].

**递推公式:**如果第i个,第j个相等,那么在之前重复子序列长度+1.if (nums1[i - 1] == nums2[j - 1]) dp[j] = dp[j - 1] + 1;
为什么要-1?为什么不-2? 为什么重复序列必须是连续的。

**初始化:**初始化为0,最小值不相等,长度为0.

为什么dp是i,j 而nums中是i-1,j-1了?
因为便于初始化, dp[0][0]如果代表是第一个元素,那么dp[i-1][j-1]就越界了。如果我们手动把第一行和第一列初始化,代码就显得很繁琐。

**遍历顺序:**从前向后。如果是滚动数组,内层从后向前,避免覆盖历史数据。

二维比较简单

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        int result = 0;
        for (int i = 1; i <= nums1.size(); i++) {
            for (int j = 1; j <= nums2.size(); j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                if (dp[i][j] > result) result = dp[i][j];
            }
        }
        return result;
    }
};

滚动数组优化版
注意,元素不相等的时候,需要置0
if (nums1[i - 1] != nums2[j - 1]) dp[i][j]=0;

class Solution {
 public:
  int findLength(vector<int> &nums1, vector<int> &nums2) {
    // 索引从1开始代表第i个元素,空出0位置便于初始化进行递推
    vector<int> dp(nums2.size() + 1, 0);

    // 遍历两个数组
    for (int i = 1; i <= nums1.size(); ++i) {
      // 从后向前遍历,避免覆盖历史记录
      for (int j = nums2.size(); j > 0; --j) {
        // 第i个元素索引为i-1
        if (nums1[i - 1] == nums2[j - 1]) {
          dp[j] = dp[j - 1] + 1;
        } else dp[j] = 0;  ///< 注意滚动数组需要手动置0
      }
    }
    return *max_element(dp.begin(), dp.end());
  }
};

1143. 最长公共子序列

不要求连续!
dp[i][j]含义:前i个索引[0,i-1]的text1 和前j个索引[0,j-1]的text2的最长公共子序列长度。

初始化: 当 i 或 j 为 0,即任意一个字符串的长度为 0 时,dp[i][j] 也为 0,因为空字符串与任何字符串的最长公共子序列长度为 0。

递推公式:

  1. text1[i - 1] == text2[j - 1] 时,这意味着这两个字符匹配,我们可以在之前的最长公共子序列的基础上加上这个字符,因此 dp[i][j] = dp[i - 1][j - 1] + 1
  2. text1[i - 1] != text2[j - 1] 时,最长公共子序列要么是 text1 的前 i - 1 个字符和 text2 的前 j 个字符的最长公共子序列,要么是text1前i个和text2前j-1个最长公共子序列,取二者较大者,因此 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    如图所示:
    在这里插入图片描述

如果不理解,看详细解释:
当 text1[i - 1] != text2[j - 1] 的情况时,我们实际上在处理两个字符不匹配的情况。在这种情况下,我们需要找出不包含当前不匹配字符的最长公共子序列。有两种可能性:

  1. 不包含 text1 中的第 i 个字符:这意味着我们考虑 text1 中前 i - 1 个字符和 text2 中前 j 个字符的最长公共子序列。在这种情况下,我们忽略了 text1 中的第 i 个字符,因为它与 text2 中的第 j 个字符不匹配。
  2. 不包含 text2 中的第 j 个字符:这意味着我们考虑 text1 中前 i 个字符和 text2 中前 j - 1 个字符的最长公共子序列。在这种情况下,我们忽略了 text2 中的第 j 个字符,因为它与 text1 中的第 i 个字符不匹配。
/**
 * @file 1143. Longest Common Subsequence.cpp
 * @brief
 *  不要求连续
 * @author Chris
 * @date 2023-12-30
 * @see https://leetcode.cn/problems/longest-common-subsequence/
 */

#include <string>
#include <vector>
using namespace std;
class Solution {
 public:
  /**
   * @brief 动态规划  当前状态只与上一个状态有关。
   *                 当前状态可以由之前的状态推导出来。
   * 第i个位置的下标是i-1
   * dp[i][j]含义:前i个索引[0,i-1]的text1 和
   * 前j个索引[0,j-1]的text2的最长公共子序列长度。
   * 递推公式:
   * 1。text[i-1]与text[j-1]相同,2。text[i-1]与text[j-1]不同
   * dp[i][j] = dp[i-1][j-1] + 1
   * dp[i][j] = max(dp[i-1][j], dp[i][j-1])
   */
  int longestCommonSubsequence(string text1, string text2) {
    vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
    for (int i = 1; i <= text1.size(); ++i) {
      for (int j = 1; j <= text2.size(); ++j) {
        if (text1[i - 1] == text2[j - 1]) {
          /// 如果相等,那么就加上这个元素
          dp[i][j] = dp[i - 1][j - 1] + 1;
        } else {
          /// 如果不想等,那么选择少一个text1元素或者少一个text2元素的最大值。
          dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
        }
      }
    }
    return dp.back().back();
  }
};

1035. 不相交的线

最大公共子序列就是把相等的元素连起来,一一对应。

/**
 * @file 1035. Uncrossed Lines.cpp
 * @brief
 *
 * @author Chris
 * @date 2023-12-30
 * @see https://leetcode.cn/problems/uncrossed-lines/
 */

#include <vector>
using namespace std;
class Solution {
 public:
  /// @note 和求最大公共子字符串一致
  int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
    vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
    for (int i = 1; i <= nums1.size(); i++) {
      for (int j = 1; j <= nums2.size(); ++j) {
        dp[i][j] = nums1[i - 1] == nums2[j - 1]
                       ? dp[i - 1][j - 1] + 1
                       : max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
    return dp.back().back();
  }
};

53. 最大子数组和

@attention 从例子中注意到子序列需要是连续的

  • 比如输入序列[1,2,3,4,5]
  • 答案子序列可以是[2,3,4],但不可是[1,4,5]
  1. dp的含义:dp[i] = 长度为i的子序列的的和的最大值
  2. 递推公式:子序列求和必须是连续的,所以我们不考虑不连续的求和
    这意味着,dp[i]是必须要包含nums[i]的,
    这就能确保从上一个状态转化到下一个状态一定是连续的序列求和。
    因此, 当前状态 = 上一个状态+nums[i], 只有nums[i]本身求和
    也可以理解为,dp[j-1]<0,我们就从nums[i]开始求和。
    ==> dp[i] = (dp[i-1] + nums[i], nums[i]);
  3. 初始化 dp[0] = 第一个元素的自己求和
  4. 遍历顺序 从前向后
  5. 边界处理 for 从[1, nums.size()-1],注意只有dp的索引代表第几个元素的时候,声明的时候才+1
/**
 * @attention 从例子中注意到子序列需要是连续的
 * 比如输入序列[1,2,3,4,5]
 * 答案子序列可以是[2,3,4],但不可是[1,4,5]
 */
class Solution {
 public:
  int maxSubArray(vector<int>& nums) {
    /// 当dp索引代表个数的时候才+1. 即dp[i]遍历nums[i-1]
    vector<int> dp(nums.size());
    dp[0] = nums[0];
    for (int i = 1; i < nums.size(); ++i) {
      dp[i] = max(dp[i - 1] + nums[i], nums[i]);
    }
    return *max_element(dp.begin(), dp.end());
  }
};

优化

注意 res要初始化为nums[0],不能是0,第1个元素时是自身求和

class Solution {
 public:
  int maxSubArray(vector<int>& nums) {
  ///  注意要初始化不能是0,第1个元素时是自身求和
    int max_val = 0, res = nums[0];
    for (int num : nums) {
      max_val = max(max_val + num, num);
      res = max(res, max_val);
    }
    return res;
  }
};

深入思考的话,注意不能是max_val初始化为nums[0],
因为max_value代表的是dp值,代码中从index=0元素开始遍历,第一次遍历结束max_value预期应为nums[0].
如果初始化max_val就不对了,很简单,可以举例验证nums[0]>0时候。
因为max_valu = nums[0],且nums[0]>0
所以第一次:max_val = max(nums[0] + nums[0], nums[0]) = 2 * nums[0]❌

392.Is Subsequence

dp[i][j]含义:以索引i-1为结尾的字符串s,和以索引j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。

好了,**为什么dp[i][j]对应的是i-1??因为便于初始化,**在空出来了第一行和第一列,之后dp[1]代表s[0]

看图比较明显。

在这里插入图片描述

递推公式: 如果s[i - 1] == t[j - 1] 字符相等,那么 长度+1. dp[i][j] = dp[i-1][j-1] + 1;

如果不相等,按照之前题目的思路, dp[i][j] = max(dp[i-1][j], dp[i][j-1]); 但这题s是t的字串,s≤t的,我们尽量去匹配s的字符串,让s的全部字符都能与t匹配。如图我们匹配”g”与”b”的时候,dp[i-1][j] 是不包含字s的字符“b”的。不能匹配s字符串字符的答案是没有意义的。

所以 if not, dp[i][j] = dp[i][j-1];

在这里插入图片描述

class Solution {
 public:
  /// whether s is a subsequence of t.
  bool isSubsequence(string s, string t) {
    vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
    for (int i = 1; i <= s.size(); ++i) {
      for (int j = 1; j <= t.size(); ++j) {
        if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
        /// s 是 t的子序列,s要完整匹配,t可以缺少元素
        else dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
      }
    }
    return dp.back().back() == s.size();
  }
};

115. 不同的子序列

与上一题不同点:

  1. 这一题子串t为第二个参数,for循环用j表示。
  2. 这一题求方案数,上一题求长度。

dp[i][j]含义
以i-1为结尾的s子序列中出现以j-1为结尾的t的方案数为dp[i][j]。

递推关系
1.字符不相等 s[i-1] != t[j-1]
不匹配该字符,继承上一个状态(方案数),和392题相同,需要继承包含子串t的状态
dp[i][j] = dp[i-1][j] .注意这一题子串t用j遍历。

2.字符相等s[i-1] == t[j-1]
求的是方案数,需要将不同的情况求和
a) 匹配该字符,即从dp[i-1][j-1] 匹配的字串长度+1,方案数不变dp[i-1][j-1] = dp[i-1][j-1]
b) 也可以不使用s[i-1],继承上一状态的方案数。添加该方案dp[i-1][j]
需要把两种情况都算进去!dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
当不用s[i-1] 匹配,可以用之前的匹配,比如s=“abb”,t=“ab” 可以用s的第一个b也可以用s的第二个b匹配。

初始化
dp[i][0]=1,表示字符串t=“”,s中匹配空字符串方案数为1,主要是为了便于递推公式的遍历

class Solution {
 public:
  int numDistinct(string s, string t) {
    if (s.size() < t.size()) return 0;

    vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1, 0));

    for (int i = 0; i < dp.size(); ++i) dp[i][0] = 1;

    for (int i = 1; i <= s.size(); ++i) {
      for (int j = 1; j <= t.size(); ++j) {
        if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
        else dp[i][j] = dp[i - 1][j];
      }
    }
    return dp.back().back();
  }
};

总结来说,类比背包问题,求方案数的时候,不可以选择该物品就继承上一状态!当可以选择的时候,可以选也可以不选,需要把两种方案加起来!

583. Delete Operation for Two Strings

找到最大公共子序列,减去公共长度,就是最小要编辑的次数了!

class Solution {
 public:
  /// 找到最大公共子序列,减去公共长度,就是最小要编辑的次数了!
  int minDistance(string word1, string word2) {
    int m = word1.size();
    int n = word2.size();
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
    for (int i = 1; i <= m; ++i) {
      for (int j = 1; j <= n; ++j) {
        if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
        else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
    ///< 注意每一个字符串都需要减去公共部分
    return m + n - dp[m][n] * 2;
  }
};

72. Edit Distance

求为了使得两个字符串相同,最少的操作次数。(操作有:增删改)

思路

相同的字符保持不变→无操作+0, 不同的地方进行删或改+1。
修改字符很好理解,改完就能相等,那如何通过增或删除达到字符串相同?

如图,比如 word1 = “a”, word2 = “abcdef”, word2可以通过不断的删除,使两者相同。同理要明白,word1也可以不通过不断的添加”bcdef”形成word2. 最终word1 == word2就行。

在这里插入图片描述

所以 ,一个子串的增 == 另一个子串的删,所以操作简化为2步。改+ 删(或改+增)

dp含义:

dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最少编辑次数为dp[i][j]。

递推公式:

分析状态转移:
1.**上一个状态是什么?**如何找?要从dp的含义下手。
2. **状态如何转化而来?**根据思路中的操作去想。

相等→无操作:不相等→增或改(删或改)取其中最少步骤的操作,我们不必关心它的具体操作是删最小还是改最小?只需要确保想法的可行性就行。

初始化:0,顺序不可重复,从前向后,没啥好说的,没啥特殊要求就默认的操作。

package main

// \brief  最小编辑
func minDistance(word1 string, word2 string) int {
	m, n := len(word1), len(word2)
	dp := make([][]int, m+1)

	for i := range dp {
		dp[i] = make([]int, n+1)
		dp[i][0] = i
	}

	for j := 0; j <= n; j++ {
		dp[0][j] = j
	}

	for i := 1; i <= m; i++ {
		for j := 1; j <= n; j++ {
			if word1[i-1] == word2[j-1] { // 不用编辑,次数+0
				dp[i][j] = dp[i-1][j-1]
			} else { // 增删改, 一个增=另外一个删
				//                改         ,word1增      word2增 /编辑一次
				dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
			}
		}
	}
	return dp[m][n]
}

所以dp的核心就是这1.确定状态2.状态如何转移的。确定状态要从dp的含义入手,理解了含义才知道上一个状态应该是什么。状态转移就要从想法和操作入手,这个需要多动脑思考,或多刷题总结才能想得出来。

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值