动态规划汇总5

2 篇文章 0 订阅
1 篇文章 0 订阅

1.最长递增子序列

力扣题目链接(opens new window)

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

  • 输入:nums = [10,9,2,5,3,7,101,18]
  • 输出:4
  • 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

  • 输入:nums = [0,1,0,3,2,3]
  • 输出:4

示例 3:

  • 输入:nums = [7,7,7,7,7,7,7]
  • 输出:1

提示:

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 104
public class Longest_Increasing_Subsequence {
    public int lengthOfLIS(int[] nums) {//接受一个整数数组 nums 作为参数,并返回一个整数,表示数组 nums 中最长递增子序列的长度。
        if (nums.length <= 1) return nums.length;//如果数组 nums 的长度小于或等于 1,那么最长递增子序列的长度就是数组的长度,因为单个元素或者空数组本身就是一个递增序列。
        int[] dp = new int[nums.length];//创建一个长度与 nums 相同的数组 dp,用于存储以 nums 中每个元素结尾的最长递增子序列的长度。
        int res = 1;//初始化 dp 数组的所有元素为 1,因为每个元素至少可以构成长度为 1 的递增子序列(即它自己)。同时,声明一个变量 res 用于存储全局的最长递增子序列长度,并初始化为 1。
        Arrays.fill(dp, 1);
        for (int i = 1; i < dp.length; i++) {//双层循环,外层循环遍历数组 nums 的每个元素,从数组的第二个元素开始遍历(因为第一个元素已经初始化为 1)。
            for (int j = 0; j < i; j++) {//内层循环则比较当前元素 nums[i] 与之前的所有元素 nums[j](j 从 0 到 i-1)。
                if (nums[i] > nums[j]) {//如果 nums[i] 大于 nums[j],则说明 nums[i] 可以接在 nums[j] 后面形成一个更长的递增子序列,因此更新 dp[i] 为 dp[j] + 1 和 dp[i] 中的较大值。
                    dp[i] = Math.max(dp[i], dp[j] + 1);//比较两种情况:情况一:当前以 nums[i] 结尾的递增子序列的长度(即 dp[i])。情况二:dp[j] + 1 表示以 nums[j] 结尾的递增子序列长度加 1(因为 nums[i] 可以接在后面)。取两者中的最大值作为新的 dp[i] 的值,确保 dp[i] 存储的是以 nums[i] 结尾的最长递增子序列的长度。
                }
            }
            res = Math.max(res, dp[i]);//每次更新 dp[i] 后,我们需要检查以 nums[i] 结尾的最长递增子序列的长度是否比当前已知的全局最长递增子序列的长度更长。比较两种情况:情况一:当前已知的全局最长递增子序列的长度(即 res)。情况二:以 nums[i] 结尾的最长递增子序列的长度(即 dp[i])。取两者中的最大值作为新的 res 的值,确保 res 始终存储的是全局的最长递增子序列的长度。
        }
        return res;//返回 res,即最长递增子序列的长度。
    }
}

假设 nums = [10, 9, 2, 5, 3, 7, 101, 18]

  1. 初始化

    • dp = [1, 1, 1, 1, 1, 1, 1, 1]
    • res = 1
  2. 当 i = 1

    • nums[1] = 9nums[0] = 10,因为 9 <= 10,不满足 if 条件,dp[1] 保持为 1。
  3. 当 i = 2

    • nums[2] = 2,对于 j = 0nums[0] = 10)和 j = 1nums[1] = 9),因为 2 小于它们,不满足 if 条件,dp[2] 保持为 1。
  4. 当 i = 3

    • nums[3] = 5,对于 j = 0 到 j = 2
      • 当 j = 2nums[2] = 2),因为 5 > 2
        • dp[3] = Math.max(dp[3], dp[2] + 1) = Math.max(1, 1 + 1) = 2
      • 其他 j 值不满足条件。
    • res = Math.max(res, dp[3]) = Math.max(1, 2) = 2
  5. 当 i = 4

    • nums[4] = 3,对于 j = 0 到 j = 3
      • 当 j = 2nums[2] = 2),因为 3 > 2
        • dp[4] = Math.max(dp[4], dp[2] + 1) = Math.max(1, 1 + 1) = 2
      • 其他 j 值不满足条件。
    • res = Math.max(res, dp[4]) = Math.max(2, 2) = 2
  6. 当 i = 5

    • nums[5] = 7,对于 j = 0 到 j = 4
      • 当 j = 3nums[3] = 5),因为 7 > 5
        • dp[5] = Math.max(dp[5], dp[3] + 1) = Math.max(1, 2 + 1) = 3
      • 当 j = 4nums[4] = 3),因为 7 > 3
        • dp[5] = Math.max(dp[5], dp[4] + 1) = Math.max(3, 2 + 1) = 3
      • 其他 j 值不满足条件。
    • res = Math.max(res, dp[5]) = Math.max(2, 3) = 3
  7. 当 i = 6

    • nums[6] = 101,对于 j = 0 到 j = 5
      • 当 j = 5nums[5] = 7),因为 101 > 7
        • dp[6] = Math.max(dp[6], dp[5] + 1) = Math.max(1, 3 + 1) = 4
      • 其他 j 值不满足条件。
    • res = Math.max(res, dp[6]) = Math.max(3, 4) = 4
  8. 当 i = 7

    • nums[7] = 18,对于 j = 0 到 j = 6
      • 当 j = 5nums[5] = 7),因为 18 > 7
        • dp[7] = Math.max(dp[7], dp[5] + 1) = Math.max(1, 3 + 1) = 4
      • 当 j = 6nums[6] = 101),不满足条件。
      • 其他 j 值不满足条件。
    • res = Math.max(res, dp[7]) = Math.max(4, 4) = 4

最终,res 的值为 4,表示最长递增子序列的长度为 4。

2.最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:

  • 输入:nums = [1,3,5,4,7]
  • 输出:3
  • 解释:最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

示例 2:

  • 输入:nums = [2,2,2,2,2]
  • 输出:1
  • 解释:最长连续递增序列是 [2], 长度为1。

提示:

  • 0 <= nums.length <= 10^4
  • -10^9 <= nums[i] <= 10^9
  • 最长连续递增子序列
    • 要求元素在原数组中位置连续,即子序列元素在原数组中必须是相邻的元素。
  • 最长递增子序列
    • 元素在原数组中的位置可以不连续,只需要满足递增的条件。
public class Longest_Continuous_Increasing_Subsequence {
 public int findLengthOfLCIS2(int[] nums) {
        int beforeOneMaxLen = 1, currentMaxLen = 0;//beforeOneMaxLen用于存储当前位置前一个递增子序列的最大长度。初始化为 1,因为第一个元素本身可以构成一个长度为 1 的递增子序列。currentMaxLen 存储的是当前位置的递增子序列的最大长度,
        int res = 1;//用于存储全局的最长连续递增子序列的长度,初始化为 1,因为至少有一个元素时,长度为 1。
        for (int i = 1; i < nums.length; i ++) {//从数组的第二个元素开始遍历,因为第一个元素已经考虑过(长度为 1)。检查当前元素 nums[i] 是否大于前一个元素 nums[i - 1]。
            currentMaxLen = nums[i] > nums[i - 1] ? beforeOneMaxLen + 1 : 1;//如果当前元素比前一个元素大,说明当前元素可以加入到前一个递增子序列中,形成一个更长的递增子序列。则 currentMaxLen更新为 beforeOneMaxLen + 1,表示当前位置的递增子序列长度在前一个递增子序列长度的基础上加 1。否则说明当前元素不能加入到前一个递增子序列中,当前递增子序列中断。因此 currentMaxLen 重置为 1,开始新的递增子序列。
            beforeOneMaxLen = currentMaxLen;//将 beforeOneMaxLen 更新为 currentMaxLen 的值,为下一次迭代做准备。
            res = Math.max(res, currentMaxLen);//如果 currentMaxLen 更大,更新 res 为 currentMaxLen,确保 res 存储的是全局最长连续递增子序列的长度。每次计算 currentMaxLen 后,都会更新 res。
        }
        return res;
    }
}

假设 nums = [1, 3, 5, 4, 7]

  1. 初始化

    • beforeOneMaxLen = 1
    • currentMaxLen = 0
    • res = 1
  2. 当 i = 1

    • nums[1] = 3nums[0] = 1,因为 3 > 1
      • currentMaxLen = beforeOneMaxLen + 1 = 1 + 1 = 2
        • 当前元素可以加入前一个递增子序列,长度加 1。
      • beforeOneMaxLen = currentMaxLen = 2
        • 更新 beforeOneMaxLen
      • res = Math.max(res, currentMaxLen) = Math.max(1, 2) = 2
        • 更新 res
  3. 当 i = 2

    • nums[2] = 5nums[1] = 3,因为 5 > 3
      • currentMaxLen = beforeOneMaxLen + 1 = 2 + 1 = 3
        • 当前元素可以加入前一个递增子序列,长度加 1。
      • beforeOneMaxLen = currentMaxLen = 3
        • 更新 beforeOneMaxLen
      • res = Math.max(res, currentMaxLen) = Math.max(2, 3) = 3
        • 更新 res
  4. 当 i = 3

    • nums[3] = 4nums[2] = 5,因为 4 < 5
      • currentMaxLen = 1
        • 当前元素不能加入前一个递增子序列,开始新的递增子序列。
      • beforeOneMaxLen = currentMaxLen = 1
        • 更新 beforeOneMaxLen
      • res = Math.max(res, currentMaxLen) = Math.max(3, 1) = 3
        • res 不变。
  5. 当 i = 4

    • nums[4] = 7nums[3] = 4,因为 7 > 4
      • currentMaxLen = beforeOneMaxLen + 1 = 1 + 1 = 2
        • 当前元素可以加入前一个递增子序列,长度加 1。
      • beforeOneMaxLen = currentMaxLen = 2
        • 更新 beforeOneMaxLen
      • res = Math.max(res, currentMaxLen) = Math.max(3, 2) = 3
        • res 不变。

最终,res 的值为 3,表示最长连续递增子序列的长度为 3。

3.最长重复子数组

力扣题目链接(opens new window)

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

示例:

输入:

  • A: [1,2,3,2,1]
  • B: [3,2,1,4,7]
  • 输出:3
  • 解释:长度最长的公共子数组是 [3, 2, 1] 。

提示:

  • 1 <= len(A), len(B) <= 1000
  • 0 <= A[i], B[i] < 100
public class Longest_Repeating_Subarray {
    public int findLength1(int[] nums1, int[] nums2) {
        int result = 0;//result变量用来存储全局最大值,即最终结果,初始值为0。
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];//dp数组的大小为nums1.length + 1乘以nums2.length + 1,因为数组索引是从1开始的,所以需要额外的空间来处理边界情况。当 i = 0 或 j = 0 时,我们可以认为它们代表了空数组或还未开始考虑元素的情况。额外的一行和一列(dp[0][j] 和 dp[i][0])用于处理边界情况,初始值默认为 0。初始值为 0 的原因:dp[0][j] = 0:当 i = 0 时,意味着我们还没有考虑 nums1 中的任何元素,因此无论 nums2 中的元素如何,都不存在公共子数组,所以 dp[0][j] 的长度为 0。dp[i][0] = 0:当 j = 0 时,意味着我们还没有考虑 nums2 中的任何元素,因此无论 nums1 中的元素如何,都不存在公共子数组,所以 dp[i][0] 的长度为 0。dp[i][j] 表示以 nums1[i - 1] 和 nums2[j - 1] 结尾的公共子数组的长度。
        for (int i = 1; i < nums1.length + 1; i++) {//外层循环遍历nums1,从第二个元素开始(索引1),因为我们需要比较两个数组的元素,所以索引从1开始。
            for (int j = 1; j < nums2.length + 1; j++) {//内层循环遍历nums2,同样从第二个元素开始。
                if (nums1[i - 1] == nums2[j - 1]) {//如果nums1[i - 1]和nums2[j - 1]相等,说明这两个元素可以构成一个公共子数组。
                    dp[i][j] = dp[i - 1][j - 1] + 1;//dp[i][j]就是dp[i - 1][j - 1](即不包含当前元素的子数组长度)加1。以 nums1[i - 1] 和 nums2[j - 1] 结尾的公共子数组的长度是前一个公共子数组(以 nums1[i - 2] 和 nums2[j - 2] 结尾)的长度加 1。这是因为找到了新的公共元素,公共子数组长度可以在前一个公共子数组的基础上增加 1。如果不相等,dp[i][j]就是0,因此,以 nums1[i - 1] 和 nums2[j - 1] 结尾的公共子数组的长度为 0。因为当前元素不构成公共子数组。
                    result = Math.max(result, dp[i][j]);//每次更新dp[i][j]后,都会用Math.max(result, dp[i][j])来更新全局最大值result。
                } else {
                    dp[i][j] = 0;
                }
            }
        }
            return result;//遍历完成后,result中存储的就是两个数组的最长公共子数组的长度。
        }
}

假设 nums1 = [1, 2, 3, 2, 1] 和 nums2 = [3, 2, 1, 4, 7]

  1. 初始化 dp 数组

    • 初始的 dp 数组如下:
      [0, 0, 0, 0, 0, 0]
      [0, 0, 0, 0, 0, 0]
      [0, 0, 0, 0, 0, 0]
      [0, 0, 0, 0, 0, 0]
      [0, 0, 0, 0, 0, 0]
      [0, 0, 0, 0, 0, 0]
  1. 当 i = 1, j = 1

    • nums1[0] = 1nums2[0] = 3,不相等,dp[1][1] = 0result = 0
  2. 当 i = 1, j = 2

    • nums1[0] = 1nums2[1] = 2,不相等,dp[1][2] = 0result = 0
  3. 当 i = 1, j = 3

    • nums1[0] = 1nums2[2] = 1,相等,dp[1][3] = dp[0][2] + 1 = 0 + 1 = 1result = Math.max(0, 1) = 1
  4. 当 i = 2, j = 1

    • nums1[1] = 2nums2[0] = 3,不相等,dp[2][1] = 0result = 1
  5. 当 i = 2, j = 2

    • nums1[1] = 2nums2[1] = 2,相等,dp[2][2] = dp[1][1] + 1 = 0 + 1 = 1result = Math.max(1, 1) = 1
  6. 当 i = 2, j = 3

    • nums1[1] = 2nums2[2] = 1,不相等,dp[2][3] = 0result = 1
  7. 当 i = 3, j = 1

    • nums1[2] = 3nums2[0] = 3,相等,dp[3][1] = dp[2][0] + 1 = 0 + 1 = 1result = Math.max(1, 1) = 1
  8. 当 i = 3, j = 2

    • nums1[2] = 3nums2[1] = 2,不相等,dp[3][2] = 0result = 1
  9. 当 i = 3, j = 3

    • nums1[2] = 3nums2[2] = 1,不相等,dp[3][3] = 0result = 1
  10. 当 i = 4, j = 1

    • nums1[3] = 2nums2[0] = 3,不相等,dp[4][1] = 0result = 1
  11. 当 i = 4, j = 2

    • nums1[3] = 2nums2[1] = 2,相等,dp[4][2] = dp[3][1] + 1 = 1 + 1 = 2result = Math.max(1, 2) = 2
  12. 当 i = 4, j = 3

    • nums1[3] = 2nums2[2] = 1,不相等,dp[4][3] = 0result = 2
  13. 当 i = 5, j = 1

    • nums1[4] = 1nums2[0] = 3,不相等,dp[5][1] = 0result = 2
  14. 当 i = 5, j = 2

    • nums1[4] = 1nums2[1] = 2,不相等,dp[5][2] = 0result = 2
  15. 当 i = 5, j = 3

    • nums1[4] = 1nums2[2] = 1,相等,dp[5][3] = dp[4][2] + 1 = 2 + 1 = 3result = Math.max(2, 3) = 3

最终,result 的值为 3,表示 nums1 和 nums2 的最长公共子数组的长度为 3,即 [3, 2, 1]

4.最长公共子序列

力扣题目链接(opens new window)

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

  • 输入:text1 = "abcde", text2 = "ace"
  • 输出:3
  • 解释:最长公共子序列是 "ace",它的长度为 3。

示例 2:

  • 输入:text1 = "abc", text2 = "abc"
  • 输出:3
  • 解释:最长公共子序列是 "abc",它的长度为 3。

示例 3:

  • 输入:text1 = "abc", text2 = "def"
  • 输出:0
  • 解释:两个字符串没有公共子序列,返回 0。

提示:

  • 1 <= text1.length <= 1000
  • 1 <= text2.length <= 1000 输入的字符串只含有小写英文字符。
  • 最长公共子序列允许不连续的元素,通过状态转移方程中的 dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); 可以在不相等元素时继续寻找公共部分。
  • 最长公共子数组要求元素连续,不相等元素会导致公共子数组中断,将 dp[i][j] 置为 0。
public class Longest_Common_Subsequence {
    public int longestCommonSubsequence1(String text1, String text2) {
        int[][] dp = new int[text1.length() + 1][text2.length() + 1]; //创建一个二维数组dp,其大小为text1.length() + 1乘以text2.length() + 1。p[i][j] 表示 text1 的前 i 个字符和 text2 的前 j 个字符的最长公共子序列的长度。数组的索引从1开始,因为我们需要留出第一行和第一列来处理边界情况。额外的一行和一列(dp[0][j] 和 dp[i][0])用于处理边界情况,初始值默认为 0。
        for (int i = 1 ; i <= text1.length() ; i++) {//外层循环遍历text1,从第二个字符开始(索引1)。
            char char1 = text1.charAt(i - 1);//获取 text1 的第 i - 1 个字符,因为 i 从 1 开始,所以需要 i - 1 来正确索引字符。
            for (int j = 1; j <= text2.length(); j++) {//内层循环遍历text2,同样从第二个字符开始。
                char char2 = text2.charAt(j - 1);//获取 text2 的第 j - 1 个字符,因为 j 从 1 开始,所以需要 j - 1 来正确索引字符。
                if (char1 == char2) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;//当 text1 的第 i - 1 个字符和 text2 的第 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]);//如果当前字符不同,dp[i][j]就是dp[i - 1][j]和dp[i][j - 1]中的最大值,即不包含text1的当前字符或text2的当前字符的子序列长度中较大的那个。因为我们可以选择在 text1 中跳过当前字符或者在 text2 中跳过当前字符,取较长的公共子序列长度。
                }
            }
        }
        return dp[text1.length()][text2.length()];//遍历完成后,dp[text1.length()][text2.length()]中存储的就是两个字符串的最长公共子序列的长度。
    }
}

假设 text1 = "abcde" 和 text2 = "ace"

  1. 初始化 dp 数组

    • 初始的 dp 数组如下:
      [0, 0, 0, 0]
      [0, 0, 0, 0]
      [0, 0, 0, 0]
      [0, 0, 0, 0]
      [0, 0, 0, 0]
      [0, 0, 0, 0]
  1. 当 i = 1, j = 1

    • char1 = 'a'char2 = 'a',相等,dp[1][1] = dp[0][0] + 1 = 1
  2. 当 i = 1, j = 2

    • char1 = 'a'char2 = 'c',不相等,dp[1][2] = Math.max(dp[0][2], dp[1][1]) = Math.max(0, 1) = 1
  3. 当 i = 1, j = 3

    • char1 = 'a'char2 = 'e',不相等,dp[1][3] = Math.max(dp[0][3], dp[1][2]) = Math.max(0, 1) = 1
  4. 当 i = 2, j = 1

    • char1 = 'b'char2 = 'a',不相等,dp[2][1] = Math.max(dp[1][0], dp[2][0]) = Math.max(0, 0) = 0
  5. 当 i = 2, j = 2

    • char1 = 'b'char2 = 'c',不相等,dp[2][2] = Math.max(dp[1][2], dp[2][1]) = Math.max(1, 0) = 1
  6. 当 i = 2, j = 3

    • char1 = 'b'text2 = 'e',不相等,dp[2][3] = Math.max(dp[1][3], dp[2][2]) = Math.max(1, 1) = 1
  7. 当 i = 3, j = 1

    • char1 = 'c'char2 = 'a',不相等,dp[3][1] = Math.max(dp[2][1], dp[3][0]) = Math.max(0, 0) = 0
  8. 当 i = 3, j = 2

    • char1 = 'c'char2 = 'c',相等,dp[3][2] = dp[2][1] + 1 = 0 + 1 = 1
  9. 当 i = 3, j = 3

    • char1 = 'c'char2 = 'e',不相等,dp[3][3] = Math.max(dp[2][3], dp[3][2]) = Math.max(1, 1) = 1
  10. 当 i = 4, j = 1

    • char1 = 'd'char2 = 'a',不相等,dp[4][1] = Math.max(dp[3][1], dp[4][0]) = Math.max(0, 0) = 0
  11. 当 i = 4, j = 2

    • char1 = 'd'char2 = 'c',不相等,dp[4][2] = Math.max(dp[3][2], dp[4][1]) = Math.max(1, 0) = 1
  12. 当 i = 4, j = 3

    • char1 = 'd'char2 = 'e',不相等,dp[4][3] = Math.max(dp[3][3], dp[4][2]) = Math.max(1, 1) = 1
  13. 当 i = 5, j = 1

    • char1 = 'e'char2 = 'a',不相等,dp[5][1] = Math.max(dp[4][1], dp[5][0]) = Math.max(0, 0) = 0
  14. 当 i = 5, j = 2

    • char1 = 'e'char2 = 'c',不相等,dp[5][2] = Math.max(dp[4][2], dp[5][1]) = Math.max(1, 0) = 1
  15. 当 i = 5, j = 3

    • char1 = 'e'char2 = 'e',相等,dp[5][3] = dp[4][2] + 1 = 1 + 1 = 2

最终,dp[text1.length()][text2.length()] 的值为 3,表示 text1 和 text2 的最长公共子序列的长度为 3,即 "ace"

5.不相交的线

力扣题目链接(opens new window)

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

public class non_intersecting_lines {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int len1 = nums1.length;//创建一个二维数组dp,其大小为len1 + 1乘以len2 + 1,其中len1和len2分别是nums1和nums2的长度。数组的索引从1开始,因为我们需要留出第一行和第一列来处理边界情况。dp[i][j] 表示 nums1 的前 i 个元素和 nums2 的前 j 个元素所能构成的不相交线的最大数量。额外的一行和一列(dp[0][j] 和 dp[i][0])用于处理边界情况,初始值默认为 0。
        int len2 = nums2.length;
        int[][] dp = new int[len1 + 1][len2 + 1];
        for (int i = 1; i <= len1; i++) {//外层循环从1开始遍历nums1。
            for (int j = 1; j <= len2; j++) {//内层循环从1开始遍历nums2。
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;//如果nums1的当前元素和nums2的当前元素相同,说明这两个元素可以构成一条不相交线,那么dp[i][j]就是dp[i - 1][j - 1](即不包含当前元素的不相交线数量)加1。因为之前的不相交线数量加上当前新构成的这条。
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);//如果当前元素不同,dp[i][j]就是dp[i - 1][j]和dp[i][j - 1]中的最大值,即不包含nums1的当前元素或nums2的当前元素的不相交线数量中较大的那个。因为我们可以选择跳过 nums1 的当前元素或者 nums2 的当前元素,以达到最大不相交线数量。
                }
            }
        }
        return dp[len1][len2];//遍历完成后,dp[len1][len2]中存储的就是两个数组中不相交线的最大数量。
    }
}

假设 nums1 = [1, 4, 2] 和 nums2 = [1, 2, 4]

  1. 初始化 dp 数组

    • 初始的 dp 数组如下:
      [0, 0, 0, 0]
      [0, 0, 0, 0]
      [0, 0, 0, 0]
      [0, 0, 0, 0]
  1. 当 i = 1, j = 1

    • nums1[0] = 1nums2[0] = 1,相等,dp[1][1] = dp[0][0] + 1 = 1
  2. 当 i = 1, j = 2

    • nums1[0] = 1nums2[1] = 2,不相等,dp[1][2] = Math.max(dp[0][2], dp[1][1]) = Math.max(0, 1) = 1
  3. 当 i = 1, j = 3

    • nums1[0] = 1nums2[2] = 4,不相等,dp[1][3] = Math.max(dp[0][3], dp[1][2]) = Math.max(0, 1) = 1
  4. 当 i = 2, j = 1

    • nums1[1] = 4nums2[0] = 1,不相等,dp[2][1] = Math.max(dp[1][0], dp[2][0]) = Math.max(0, 0) = 0
  5. 当 i = 2, j = 2

    • nums1[1] = 4nums2[1] = 2,不相等,dp[2][2] = Math.max(dp[1][2], dp[2][1]) = Math.max(1, 0) = 1
  6. 当 i = 2, j = 3

    • nums1[1] = 4nums2[2] = 4,相等,dp[2][3] = dp[1][2] + 1 = 1 + 1 = 2
  7. 当 i = 3, j = 1

    • nums1[2] = 2nums2[0] = 1,不相等,dp[3][1] = Math.max(dp[2][1], dp[3][0]) = Math.max(0, 0) = 0
  8. 当 i = 3, j = 2

    • nums1[2] = 2nums2[1] = 2,相等,dp[3][2] = dp[2][1] + 1 = 0 + 1 = 1
  9. 当 i = 3, j = 3

    • nums1[2] = 2nums2[2] = 4,不相等,dp[3][3] = Math.max(dp[2][3], dp[3][2]) = Math.max(2, 1) = 2

最终,dp[len1][len2] 的值为 2,表示两个数组中不相交线的最大数量为 2。

6. 最大子序和

力扣题目链接(opens new window)

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

  • 输入: [-2,1,-3,4,-1,2,1,-5,4]
  • 输出: 6
  • 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
public class Maximum_Subarray_Sum {
    public static int maxSubArray1(int[] nums) {
        if (nums.length == 0) {//首先检查输入数组 nums 是否为空。如果数组长度为 0,那么最大子数组和为 0,直接返回 0。
            return 0;
        }
        int res = nums[0];//初始化结果 res 为 nums[0],因为初始时,假设最大子数组和至少是数组的第一个元素。
        int[] dp = new int[nums.length];//创建一个与 nums 长度相同的 dp 数组,dp[i] 用于存储以 nums[i] 结尾的最大子数组和。
        dp[0] = nums[0];//初始时,dp[0] 等于 nums[0],因为以 nums[0] 结尾的最大子数组和就是 nums[0] 本身。
        for (int i = 1; i < nums.length; i++) {//从数组的第二个元素开始遍历,因为第一个元素已经处理过。
            dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);//对于每个元素nums[i],有两种选择:选择一:将 nums[i] 加入到以 nums[i - 1] 结尾的最大子数组中,此时子数组和为 dp[i - 1] + nums[i]。选择二:不加入前面的子数组,以 nums[i] 作为新的子数组的起始元素,此时子数组和为 nums[i]。dp[i]是dp[i - 1] + nums[i]和nums[i]中的最大值,这表示我们可以选择包含前一个元素或者不包含。
            res = Math.max(res, dp[i]);//每次更新 dp[i] 后,将 res 更新为当前 res 和 dp[i] 的最大值。这样可以确保 res 存储的是全局最大子数组和,而不是仅以 nums[i] 结尾的最大子数组和。
        }
        return res;//遍历完成后,res中存储的就是数组中的最大子数组和。
    }
}

假设 nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

  1. 初始化

    • res = nums[0] = -2
    • dp[0] = nums[0] = -2
  2. 当 i = 1

    • dp[1] = Math.max(dp[0] + nums[1], nums[1]) = Math.max(-2 + 1, 1) = 1
      • 对于 nums[1] = 1,不加入前面的子数组 [-2],因为 1 > -1,所以 dp[1] 为 1。
    • res = Math.max(res, dp[1]) = Math.max(-2, 1) = 1
      • 更新 res 为 1。
  3. 当 i = 2

    • dp[2] = Math.max(dp[1] + nums[2], nums[2]) = Math.max(1 + (-3), -3) = -2
      • 对于 nums[2] = -3,加入前面的子数组 [1] 得到 [1, -3] 的和为 -2,不加入得到 -3,所以 dp[2] 为 -2。
    • res = Math.max(res, dp[2]) = Math.max(1, -2) = 1
      • res 保持为 1。
  4. 当 i = 3

    • dp[3] = Math.max(dp[2] + nums[3], nums[3]) = Math.max(-2 + 4, 4) = 4
      • 对于 nums[3] = 4,不加入前面的子数组 [1, -3],以 4 为新的起始元素,所以 dp[3] 为 4。
    • res = Math.max(res, dp[3]) = Math.max(1, 4) = 4
      • 更新 res 为 4。
  5. 当 i = 4

    • dp[4] = Math.max(dp[3] + nums[4], nums[4]) = Math.max(4 + (-1), -1) = 3
      • 对于 nums[4] = -1,加入前面的子数组 [4] 得到 [4, -1] 的和为 3,不加入得到 -1,所以 dp[4] 为 3。
    • res = Math.max(res, dp[4]) = Math.max(4, 3) = 4
      • res 保持为 4。
  6. 当 i = 5

    • dp[5] = Math.max(dp[4] + nums[5], nums[5]) = Math.max(3 + 2, 2) = 5
      • 对于 nums[5] = 2,加入前面的子数组 [4, -1] 得到 [4, -1, 2] 的和为 5,不加入得到 2,所以 dp[5] 为 5。
    • res = Math.max(res, dp[5]) = Math.max(4, 5) = 5
      • 更新 res 为 5。
  7. 当 i = 6

    • dp[6] = Math.max(dp[5] + nums[6], nums[6]) = Math.max(5 + 1, 1) = 6
      • 对于 nums[6] = 1,加入前面的子数组 [4, -1, 2] 得到 [4, -1, 2, 1] 的和为 6,不加入得到 1,所以 dp[6] = 6
    • res = Math.max(res, dp[6]) = Math.max(5, 6) = 6
      • 更新 res 为 6。
  8. 当 i = 7

    • dp[7] = Math.max(dp[6] + nums[7], nums[7]) = Math.max(6 + (-5), -5) = 1
      • 对于 nums[7] = -5,加入前面的子数组 [4, -1, 2, 1] 得到 [4, -1, 2, 1, -5] 的和为 1,不加入得到 -5,所以 dp[7] = 1
    • res = Math.max(res, dp[7]) = Math.max(6, 1) = 6
      • res 保持为 6。
  9. 当 i = 8

    • dp[8] = Math.max(dp[7] + nums[8], nums[8]) = Math.max(1 + 4, 4) = 5
      • 对于 nums[8] = 4,加入前面的子数组 [4, -1, 2, 1, -5] 得到 [4, -1, 2, 1, -5, 4] 的和为 5,不加入得到 4,所以 dp[8] = 5
    • res = Math.max(res, dp[8]) = Math.max(6, 5) = 6
      • res 保持为 6。

最终,res 的值为 6,表示该数组的最大子数组和为 6,最大子数组是 [4, -1, 2, 1]

7.判断子序列

力扣题目链接(opens new window)

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:

  • 输入:s = "abc", t = "ahbgdc"
  • 输出:true

示例 2:

  • 输入:s = "axc", t = "ahbgdc"
  • 输出:false

提示:

  • 0 <= s.length <= 100
  • 0 <= t.length <= 10^4

两个字符串都只由小写字符组成。

public class Subsequence_Checking {
public boolean isSubsequence3(String s, String t) {
        int[] dp = new int[s.length() + 1];//创建一个一维数组dp,其大小为s.length() + 1。dp[j] 可以理解为在 t 的当前遍历位置,s 的前 j - 1 个字符已经匹配到的字符数量。
        for (int i = 0; i < t.length(); i ++) {//遍历 t 字符串,每次迭代处理 t 的一个字符。
            for (int j = dp.length - 1; j > 0; j --) {//从 dp 数组的最后一个有效位置(dp.length - 1)开始,倒序遍历到位置 1(不包括 0)。
                if (t.charAt(i) == s.charAt(j - 1)) {//当 t 的当前字符(t.charAt(i))和 s 的第 j - 1 个字符相等时:那么dp[j]就是dp[j-1] + 1。表示在当前 t 的字符匹配下,s 的前 j - 1 个字符的匹配数量可以在前 j - 2 个字符匹配数量的基础上加 1。
                    dp[j] = dp[j - 1] + 1;
                }
            }
        }
        return dp[s.length()] == s.length();//遍历完成后,如果dp[s.length()]等于s.length(),说明s是t的子序列。
    }
}

假设 s = "abc" 和 t = "ahbgdc"

  1. 初始化 dp 数组

    • 初始的 dp 数组如下:
[0, 0, 0, 0]
  1. 当 i = 0t 的第一个字符 'a'

    • 内层循环:
      • 当 j = 3s 的最后一个字符 'c'),不匹配,dp[3] 不变。
      • 当 j = 2s 的字符 'b'),不匹配,dp[2] 不变。
      • 当 j = 1s 的字符 'a'),匹配,dp[1] = dp[0] + 1 = 1
  2. 当 i = 1t 的第二个字符 'h'

    • 内层循环:
      • 当 j = 3,不匹配,dp[3] 不变。
      • 当 j = 2,不匹配,dp[2] 不变。
      • 当 j = 1,不匹配,dp[1] 不变。
  3. 当 i = 2t 的第三个字符 'b'

    • 内层循环:
      • 当 j = 3,不匹配,dp[3] 不变。
      • 当 j = 2,匹配,dp[2] = dp[1] + 1 = 2
      • 当 j = 1,不匹配,dp[1] 不变。
  4. 当 i = 3t 的第四个字符 'g'

    • 内层循环:
      • 当 j = 3,不匹配,dp[3] 不变。
      • 当 j = 2,不匹配,dp[2] 不变。
      • 当 j = 1,不匹配,dp[1] 不变。
  5. 当 i = 4t 的第五个字符 'd'

    • 内层循环:
      • 当 j = 3,不匹配,dp[3] 不变。
      • 当 j = 2,不匹配,dp[2] 不变。
      • 当 j = 1,不匹配,dp[1] 不变。
  6. 当 i = 5t 的第六个字符 'c'

    • 内层循环:
      • 当 j = 3,匹配,dp[3] = dp[2] + 1 = 3
      • 当 j = 2,不匹配,dp[2] 不变。
      • 当 j = 1,不匹配,dp[1] 不变。

最终,dp[s.length()] 的值为 3,等于 s.length(),说明 s 是 t 的子序列。

8.不同的子序列

力扣题目链接(opens new window)

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

115.不同的子序列示例

提示:

  • 0 <= s.length, t.length <= 1000
  • s 和 t 由英文字母组成

前 i - 1 个字符” 的范围是从 s 的第一个字符到第 i - 2 个字符时,对于 i = 4(即前 3 个字符的情况),确实不包含第 3 个字符。

假设我们有一个字符串 s = "abcde"

  • 当我们说 “前 3 个字符”,我们指的是从索引 0 到索引 2 的字符,也就是 "abc"。这里,我们是从 s 的开始位置(索引 0)数到第 2 个位置(索引 2),不包括第 3 个位置(索引 3)的字符 'd'
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]

dp[i][j] 表示 s 的前 i 个字符中包含 t 的前 j 个字符的不同子序列的数量。当 s 的第 i - 1 个字符和 t 的第 j - 1 个字符相等时,我们可通过以下两种情况来得到 s 的前 i 个字符中包含 t 的前 j 个字符的不同子序列数量:

1. dp[i - 1][j - 1]

dp[i - 1][j - 1] 代表在不考虑 s 的第 i - 1 个字符和 t 的第 j - 1 个字符时,s 的前 i - 1 个字符(也就是从 s 的第一个字符到第 i - 2 个字符)包含 t 的前 j - 1 个字符(即从 t 的第一个字符到第 j - 2 个字符)的不同子序列的数量。当 s 的第 i - 1 个字符和 t 的第 j - 1 个字符相等时,这些子序列可以通过添加 s 的第 i - 1 个字符形成新的子序列。

2. dp[i - 1][j]

dp[i - 1][j] 表示不考虑 s 的第 i - 1 个字符,仅使用 s 的前 i - 1 个字符(从 s 的第一个字符到第 i - 2 个字符)包含 t 的前 j 个字符(从 t 的第一个字符到第 j - 1 个字符)的不同子序列的数量。

s = "rabbbit"t = "rabbit"

当 i = 4j = 3 时
  • 此时 s.charAt(3) = 'b't.charAt(2) = 'b',这两个字符相等。
  • dp[3][2] 代表不考虑 s 的第 3 个字符 'b' 和 t 的第 2 个字符 'b' 时,s 的前 3 个字符 "rab" 包含 t 的前 2 个字符 "ra" 的不同子序列的数量。在 "rab" 中,能找到包含 "ra" 的子序列就是 "ra" 本身,所以 dp[3][2] = 1。由于 s 的第 3 个字符 'b' 和 t 的第 2 个字符 'b' 相等,我们可以把这个 'b' 添加到 dp[3][2] 所代表的子序列 "ra" 后面,形成新的子序列 "rab"。这个新子序列 "rab" 是满足 s 的前 4 个字符 "rabb" 包含 t 的前 3 个字符 "rab" 的子序列之一。
  • dp[3][3] 表示不考虑 s 的第 3 个字符 'b',仅使用 s 的前 3 个字符 "rab" 包含 t 的前 3 个字符 "rab" 的不同子序列的数量。显然,"rab" 本身就是一个满足条件的子序列,所以 dp[3][3] = 1
  • 得 dp[4][3] = 1 + 1 = 2。这表明 "rabb" 中包含 "rab" 的不同子序列有 2 个,即之前不使用第 3 个 'b' 时的 "rab",以及通过添加第 3 个 'b' 形成的新子序列 "rab"(从 "ra" 扩展而来)。

当 i = 3j = 3 时,s.charAt(2) = 'b't.charAt(2) = 'b',即 s 的第 3 个字符和 t 的第 3 个字符相等。

dp[i - 1][j - 1](即 dp[2][2]

  • dp[2][2] 表示不考虑 s 的第 2 个字符(这里指 'b')和 t 的第 2 个字符(这里指 'b')时,s 的前 2 个字符 "ra" 包含 t 的前 2 个字符 "ra" 的不同子序列的数量。很明显,"ra" 中包含 "ra" 的子序列只有 "ra" 这一个,所以 dp[2][2] = 1。因为 s 的第 2 个字符 'b' 和 t 的第 2 个字符 'b' 相等,我们可以把这个 'b' 添加到 dp[2][2] 所代表的子序列 "ra" 后面,形成新的子序列 "rab"。这个新子序列 "rab" 是满足 s 的前 3 个字符 "rab" 包含 t 的前 3 个字符 "rab" 的子序列之一。

dp[i - 1][j](即 dp[2][3]

  • dp[2][3] 表示不考虑 s 的第 2 个字符 'b',仅使用 s 的前 2 个字符 "ra" 包含 t 的前 3 个字符 "rab" 的不同子序列的数量。显然,"ra" 中无法构成 "rab" 这个子序列,所以 dp[2][3] = 0

dp[3][3] = 1 + 0 = 1。这意味着 "rab" 中包含 "rab" 的不同子序列有 1 个,就是通过在 "ra" 后面添加 'b' 得到的 "rab"

如果当前字符不同,dp[i][j]就是dp[i - 1][j],

假设 s = "abcde"t = "abd"。现在我们要计算 dp[4][3],也就是 s 的前 4 个字符 "abcd" 中包含 t 的前 3 个字符 "abd" 的不同子序列的数量。

当我们比较 s 的第 3 个字符 c 和 t 的第 2 个字符 b 时,发现 c != b。由于 c 无法用于构成 "abd",所以 "abcd" 中包含 "abd" 的子序列数量和 "abc" 中包含 "abd" 的子序列数量是相同的,即 dp[4][3] = dp[3][3]

public class Distinct_Subsequences {
    public int numDistinct(String s, String t) {
        int[][] dp = new int[s.length() + 1][t.length() + 1];//创建一个二维数组dp,其大小为s.length() + 1乘以t.length() + 1。dp[i][j] 表示 s 的前 i 个字符中包含 t 的前 j 个字符的不同子序列的数量。初始化dp[i][0]为1,表示空字符串总是任何字符串的子序列,因此对于s的任何前缀,都有1个不同的子序列是空字符串(即t的前0个字符)。
        for (int i = 0; i < s.length() + 1; i++) {
            dp[i][0] = 1;
        }
        for (int i = 1; i < s.length() + 1; i++) {//从 1 开始遍历 s 的前缀,因为 dp[0][j] 已经处理了空字符串的情况。
            for (int j = 1; j < t.length() + 1; j++) {//从 1 开始遍历 t 的前缀,因为 dp[i][0] 已经处理了 t 为空字符串的情况。
                if (s.charAt(i - 1) == t.charAt(j - 1)) {//如果s的当前字符和t的当前字符相同,那么dp[i][j]就是dp[i - 1][j - 1](即s的前i-1个字符和t的前j-1个字符的匹配子序列数量)加上dp[i - 1][j](即s的前i-1个字符和t的前j个字符的不匹配子序列数量)。
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];//前 i - 1 个字符” 的范围是从 s 的第一个字符到第 i - 2 个字符,不包含第 i - 1 个字符。当 s 的第 i - 1 个字符和 t 的第 j - 1 个字符相等时,有两种情况可以得到 s 的前 i 个字符包含 t 的前 j 个字符的子序列:dp[i - 1][j - 1]:考虑 s 的前 i - 1 个字符包含 t 的前 j - 1 个字符的子序列,再加上 s 的第 i - 1 个字符。dp[i - 1][j]:不考虑 s 的第 i - 1 个字符,只使用 s 的前 i - 1 个字符来包含 t 的前 j 个字符的子序列。
                }else{//如果当前字符不同,dp[i][j]就是dp[i - 1][j],表示我们忽略s的当前字符。只能使用 s 的前 i - 1 个字符来包含 t 的前 j 个字符的子序列,所以 dp[i][j] 等于 dp[i - 1][j]。
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[s.length()][t.length()];//遍历完成后,dp[s.length()][t.length()]中存储的就是字符串s中有多少个不同的子序列是字符串t的子序列。
    }
}

假设 s = "rabbbit" 和 t = "rabbit"

初始化 dp 数组

我们首先创建一个大小为 (s.length() + 1) x (t.length() + 1) 的二维数组 dp,并将 dp[i][0] 初始化为 1,这是因为空字符串是任何字符串的子序列。

对于 s = "rabbbit" 和 t = "rabbit"s.length() = 7t.length() = 6,初始化后的 dp 数组如下:

rabbit
1000000
r1
a1
b1
b1
b1
i1
t1

双重循环填充 dp 数组

当 i = 1s 的第一个字符 r
  • 对于 j = 1s.charAt(0) == t.charAt(0)(都是 r),所以 dp[1][1] = dp[0][0] + dp[0][1] = 1 + 0 = 1
  • 对于 j > 1s.charAt(0) != t.charAt(j - 1),所以 dp[1][j] = dp[0][j] = 0

此时 dp 数组变为:

rabbit
1000000
r1100000
a1
b1
b1
b1
i1
t1
当 i = 2s 的第二个字符 a
  • 对于 j = 1s.charAt(1) != t.charAt(0),所以 dp[2][1] = dp[1][1] = 1
  • 对于 j = 2s.charAt(1) == t.charAt(1)(都是 a),所以 dp[2][2] = dp[1][1] + dp[1][2] = 1 + 0 = 1
  • 对于 j > 2s.charAt(1) != t.charAt(j - 1),所以 dp[2][j] = dp[1][j] = 0

此时 dp 数组变为:

rabbit
1000000
r1100000
a1110000
b1
b1
b1
i1
t1
当 i = 3s 的第三个字符 b
  • 对于 j = 1s.charAt(2) != t.charAt(0),所以 dp[3][1] = dp[2][1] = 1
  • 对于 j = 2s.charAt(2) != t.charAt(1),所以 dp[3][2] = dp[2][2] = 1
  • 对于 j = 3s.charAt(2) == t.charAt(2)(都是 b),所以 dp[3][3] = dp[2][2] + dp[2][3] = 1 + 0 = 1
  • 对于 j > 3s.charAt(2) != t.charAt(j - 1),所以 dp[3][j] = dp[2][j] = 0

此时 dp 数组变为:

rabbit
1000000
r1100000
a1110000
b1111000
b1
b1
i1
t1
当 i = 4s 的第四个字符 b
  • 对于 j = 1s.charAt(3) != t.charAt(0),所以 dp[4][1] = dp[3][1] = 1
  • 对于 j = 2s.charAt(3) != t.charAt(1),所以 dp[4][2] = dp[3][2] = 1
  • 对于 j = 3s.charAt(3) == t.charAt(2)(都是 b),所以 dp[4][3] = dp[3][2] + dp[3][3] = 1 + 1 = 2
  • 对于 j = 4s.charAt(3) == t.charAt(3)(都是 b),所以 dp[4][4] = dp[3][3] + dp[3][4] = 1 + 0 = 1
  • 对于 j > 4s.charAt(3) != t.charAt(j - 1),所以 dp[4][j] = dp[3][j] = 0

此时 dp 数组变为:

rabbit
1000000
r1100000
a1110000
b1111000
b1112100
b1
i1
t1
当 i = 5s 的第五个字符 b

同理继续填充 dp 数组,最终 dp 数组变为:

rabbit
1000000
r1100000
a1110000
b1111000
b1112100
b1113300
i1113330
t1113333

返回结果

最终我们返回 dp[s.length()][t.length()],即 dp[7][6] = 3。这意味着字符串 "rabbbit" 中有 3 个不同的子序列是字符串 "rabbit",分别是:

  1. rabbbit 中选取第 1, 2, 3, 4, 6, 7 个字符组成 "rabbit"
  2. rabbbit 中选取第 1, 2, 3, 5, 6, 7 个字符组成 "rabbit"
  3. rabbbit 中选取第 1, 2, 4, 5, 6, 7 个字符组成 "rabbit"

通过这个例子,你可以清楚地看到 numDistinct 方法是如何利用动态规划的思想来计算字符串 s 中包含字符串 t 的不同子序列的数量的。

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

力扣题目链接(opens new window)

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

  • 输入: "sea", "eat"
  • 输出: 2
  • 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
public class String_Deletion_Operation {
    // dp数组中存储word1和word2最长相同子序列的长度
        public int minDistance1(String word1, String word2) {
            int len1 = word1.length();//len1 和 len2 分别存储 word1 和 word2 的长度。
            int len2 = word2.length();
            int[][] dp = new int[len1 + 1][len2 + 1];//创建一个二维数组dp,其大小为len1 + 1乘以len2 + 1,dp[i][j] 表示 word1 的前 i 个字符和 word2 的前 j 个字符的最长公共子序列的长度。
            for (int i = 1; i <= len1; i++) {//外层循环遍历 word1 的每个字符i 和 j 从 1 开始,因为 dp[0][j] 和 dp[i][0] 表示其中一个字符串为空的情况,初始值默认为 0。
                for (int j = 1; j <= len2; j++) {//内层循环遍历 word2 的每个字符
                    if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                        dp[i][j] = dp[i - 1][j - 1] + 1;//若 word1 的第 i - 1 个字符和 word2 的第 j - 1 个字符相同,那么 dp[i][j] 等于 dp[i - 1][j - 1] + 1。这是因为当前字符能够匹配,最长公共子序列的长度在之前的基础上增加 1。
                    } else {//若当前字符不同,dp[i][j] 取 dp[i - 1][j] 和 dp[i][j - 1] 中的最大值。dp[i - 1][j] 表示不考虑 word1 的第 i - 1 个字符时的最长公共子序列长度,dp[i][j - 1] 表示不考虑 word2 的第 j - 1 个字符时的最长公共子序列长度,我们选择其中较大的一个。表示选择不包含当前字符的序列长度。
                        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                    }
                }
            }
            return len1 + len2 - dp[len1][len2] * 2;//遍历完成后,dp[len1][len2]中存储的就是两个字符串的最长公共子序列的长度。要得到最小删除次数,我们需要从两个字符串的总长度中减去最长公共子序列的长度的两倍(因为每个匹配的字符都不需要删除)。
        }
}

 word1 = "horse"word2 = "ros" 

步骤 1:初始化

首先,计算两个字符串的长度:

  • len1 = word1.length() = 5
  • len2 = word2.length() = 3

然后创建一个 (len1 + 1) x (len2 + 1) 的二维数组 dp,初始状态下 dp 数组元素全为 0,如下所示:

ros
0000
h0
o0
r0
s0
e0

步骤 2:填充 dp 数组

接下来,使用双重循环遍历两个字符串,根据字符是否相等更新 dp 数组的值。

外层循环 i = 1word1 的字符为 h
  • 内层循环 j = 1word2 的字符为 r):word1.charAt(0) = 'h' 不等于 word2.charAt(0) = 'r',所以 dp[1][1] = Math.max(dp[0][1], dp[1][0]) = 0
  • 内层循环 j = 2word2 的字符为 o):h 不等于 o,所以 dp[1][2] = Math.max(dp[0][2], dp[1][1]) = 0
  • 内层循环 j = 3word2 的字符为 s):h 不等于 s,所以 dp[1][3] = Math.max(dp[0][3], dp[1][2]) = 0

此时 dp 数组变为:

ros
0000
h0000
o0
r0
s0
e0
外层循环 i = 2word1 的字符为 o
  • 内层循环 j = 1word2 的字符为 r):o 不等于 r,所以 dp[2][1] = Math.max(dp[1][1], dp[2][0]) = 0
  • 内层循环 j = 2word2 的字符为 o):o 等于 o,所以 dp[2][2] = dp[1][1] + 1 = 0 + 1 = 1
  • 内层循环 j = 3word2 的字符为 s):o 不等于 s,所以 dp[2][3] = Math.max(dp[1][3], dp[2][2]) = 1

此时 dp 数组变为:

ros
0000
h0000
o0011
r0
s0
e0
外层循环 i = 3word1 的字符为 r
  • 内层循环 j = 1word2 的字符为 r):r 等于 r,所以 dp[3][1] = dp[2][0] + 1 = 0 + 1 = 1
  • 内层循环 j = 2word2 的字符为 o):r 不等于 o,所以 dp[3][2] = Math.max(dp[2][2], dp[3][1]) = 1
  • 内层循环 j = 3word2 的字符为 s):r 不等于 s,所以 dp[3][3] = Math.max(dp[2][3], dp[3][2]) = 1

此时 dp 数组变为:

ros
0000
h0000
o0011
r0111
s0
e0
外层循环 i = 4word1 的字符为 s
  • 内层循环 j = 1word2 的字符为 r):s 不等于 r,所以 dp[4][1] = Math.max(dp[3][1], dp[4][0]) = 1
  • 内层循环 j = 2word2 的字符为 o):s 不等于 o,所以 dp[4][2] = Math.max(dp[3][2], dp[4][1]) = 1
  • 内层循环 j = 3word2 的字符为 s):s 等于 s,所以 dp[4][3] = dp[3][2] + 1 = 1 + 1 = 2

此时 dp 数组变为:

ros
0000
h0000
o0011
r0111
s0112
e0
外层循环 i = 5word1 的字符为 e
  • 内层循环 j = 1word2 的字符为 r):e 不等于 r,所以 dp[5][1] = Math.max(dp[4][1], dp[5][0]) = 1
  • 内层循环 j = 2word2 的字符为 o):e 不等于 o,所以 dp[5][2] = Math.max(dp[4][2], dp[5][1]) = 1
  • 内层循环 j = 3word2 的字符为 s):e 不等于 s,所以 dp[5][3] = Math.max(dp[4][3], dp[5][2]) = 2

最终 dp 数组为:

ros
0000
h0000
o0011
r0111
s0112
e0112

步骤 3:计算最小删除次数

最后,根据公式 len1 + len2 - dp[len1][len2] * 2 计算最小删除次数:

  • len1 = 5len2 = 3dp[5][3] = 2
  • 最小删除次数 = 5 + 3 - 2 * 2 = 4

这意味着要让 "horse" 和 "ros" 变成相同的字符串,最少需要删除 4 个字符。例如,可以从 "horse" 中删除 hee 得到 "ros"

10.编辑距离

力扣题目链接(opens new window)

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符

  • 删除一个字符

  • 替换一个字符

  • 示例 1:

  • 输入:word1 = "horse", word2 = "ros"

  • 输出:3

  • 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')

  • 示例 2:

  • 输入:word1 = "intention", word2 = "execution"

  • 输出:5

  • 解释: intention -> inention (删除 't') inention -> enention (将 'i' 替换为 'e') enention -> exention (将 'n' 替换为 'x') exention -> exection (将 'n' 替换为 'c') exection -> execution (插入 'u')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1 和 word2 由小写英文字母组成
public class Edit_Distance {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m + 1][n + 1];//创建一个二维数组dp,其大小为m + 1乘以n + 1,其中m是word1的长度,n是word2的长度。dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。
        for (int i = 1; i <= m; i++) {//初始化第一行,表示将word1的前i个字符全部删除,使其变为空字符串,需要i次删除操作。
            dp[i][0] =  i;
        }
        for (int j = 1; j <= n; j++) {//初始化第一列,表示将word2的前j个字符全部插入,使其变为空字符串,需要j次插入操作。dp[0][j] 表示把空字符串转换为 word2 的前 j 个字符所需的最少操作次数。因为要进行 j 次插入操作。
            dp[0][j] = j;
        }
        for (int i = 1; i <= m; i++) {//外层循环遍历word1的每个字符。
            for (int j = 1; j <= n; j++) {//内层循环遍历word2的每个字符。
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {//如果word1的当前字符和word2的当前字符相同,word1 的第 i - 1 个字符和 word2 的第 j - 1 个字符相同,那么dp[i][j]就是dp[i - 1][j - 1],表示不需要额外操作,也就是将 word1 的前 i - 1 个字符转换为 word2 的前 j - 1 个字符所需的最少操作次数。
                    dp[i][j] = dp[i - 1][j - 1];
                } else {//如果当前字符不同,那么有三种操作可以选择:替换:dp[i - 1][j - 1](将word1的当前字符替换为word2的当前字符)删除:dp[i - 1][j](删除word1的当前字符)插入:dp[i][j - 1](在word1中插入word2的当前字符)选择这三种操作中的最小值,然后加1(因为至少需要一次操作,替换或删除或插入)。
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
                }
            }
        }
        return dp[m][n];//遍历完成后,dp[m][n]中存储的就是将word1转换为word2所需的最少操作次数。
    }
}

word1 = "intention"word2 = "execution"

步骤 1:初始化

首先计算两个字符串的长度:

  • m = word1.length() = 9
  • n = word2.length() = 9

创建一个 (m + 1) x (n + 1) 的二维数组 dp,并初始化第一行和第一列:

初始的 dp 数组如下:

execution
0123456789
i1
n2
t3
e4
n5
t6
i7
o8
n9

步骤 2:填充 dp 数组

使用双重循环遍历两个字符串,根据字符是否相等更新 dp 数组的值。

外层循环 i = 1word1 的字符为 i
  • 内层循环 j = 1word2 的字符为 e):word1.charAt(0) = 'i' 不等于 word2.charAt(0) = 'e',所以 dp[1][1] = Math.min(Math.min(dp[0][0], dp[1][0], dp[0][1])) + 1 = Math.min(0, 1, 1) + 1 = 1
  • 内层循环 j = 2word2 的字符为 x):i 不等于 x,所以 dp[1][2] = Math.min(Math.min(dp[0][1], dp[1][1], dp[0][2])) + 1 = Math.min(1, 1, 2) + 1 = 2
  • 以此类推,完成 i = 1 这一行的填充。
外层循环 i = 2word1 的字符为 n
  • 内层循环 j = 1word2 的字符为 e):n 不等于 e,所以 dp[2][1] = Math.min(Math.min(dp[1][0], dp[2][0], dp[1][1])) + 1 = Math.min(1, 2, 1) + 1 = 2
  • 继续填充其他列,直到完成 i = 2 这一行。
以此类推,完成整个 dp 数组的填充

最终填充好的 dp 数组如下:

execution
0123456789
i1123456678
n2223456777
t3333455678
e4343456678
n5444456777
t6555555678
i7666666567
o8777777656
n9888888765

步骤 3:返回结果

最后,返回 dp[m][n],即 dp[9][9] = 5。这意味着将 "intention" 转换为 "execution" 最少需要 5 次操作。

11.回文子串

力扣题目链接(opens new window)

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

  • 输入:"abc"
  • 输出:3
  • 解释:三个回文子串: "a", "b", "c"

示例 2:

  • 输入:"aaa"
  • 输出:6
  • 解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

提示:输入的字符串长度不会超过 1000 。

public class Palindromic_Substring {
  public int countSubstrings2(String s) {
        boolean[][] dp = new boolean[s.length()][s.length()];//二维布尔数组,dp[i][j] 代表字符串 s 中从索引 i 到索引 j 的子串是否为回文。
        int res = 0;//用于记录回文子串的数量,初始值为 0。
        for (int i = s.length() - 1; i >= 0; i--) {//外层循环 i 从字符串的最后一个字符开始向前遍历字符串。
            for (int j = i; j < s.length(); j++) {//内层循环 j 从 i 开始向后遍历,确保 i 到 j 构成一个有效的子串。
                if (s.charAt(i) == s.charAt(j) && (j - i <= 1 || dp[i + 1][j - 1])) {//s.charAt(i) == s.charAt(j):这表明子串的首尾字符相同。j - i <= 1 || dp[i + 1][j - 1]:这又分为两种情况:j - i <= 1:意味着子串的长度为 1 或者 2。长度为 1 的子串必然是回文(如 "a"),长度为 2 且首尾字符相同的子串也是回文(如 "aa")。dp[i + 1][j - 1]:若子串长度大于 2,那么需要判断去掉首尾字符后的子串(即从 i + 1 到 j - 1 的子串)是否为回文。如果该子串是回文,并且当前子串首尾字符相同,那么当前子串也是回文。
                    res++;//若满足上述条件,res 加 1 以记录新发现的回文子串,同时将 dp[i][j] 设为 true 表示该子串是回文。
                    dp[i][j] = true;
                }
            }
        }
        return res;//遍历完成后,res中存储的就是所有回文子串的数量。
    }
}

 s = "aaa"

步骤 1:初始化

  • 由于 s.length() 为 3,所以创建一个 3 x 3 的二维布尔数组 dp,初始状态下数组中的所有元素都为 false
  • res 用于记录回文子串的数量,初始值为 0。

步骤 2:双重循环遍历字符串并判断回文子串

外层循环 i = 2(字符为 a
  • 内层循环 j = 2
    • 此时子串为 "a",长度为 1。满足 s.charAt(2) == s.charAt(2) 且 j - i <= 1 这个条件。
    • 所以 res 加 1,变为 1;同时 dp[2][2] 被设为 true
外层循环 i = 1(字符为 a
  • 内层循环 j = 1
    • 子串为 "a",长度为 1。满足 s.charAt(1) == s.charAt(1) 且 j - i <= 1
    • res 加 1,变为 2;dp[1][1] 设为 true
  • 内层循环 j = 2
    • 子串为 "aa",首尾字符都是 a,满足 s.charAt(1) == s.charAt(2),且子串长度为 2(j - i <= 1 成立)。
    • res 加 1,变为 3;dp[1][2] 设为 true
外层循环 i = 0(字符为 a
  • 内层循环 j = 0
    • 子串为 "a",长度为 1。满足 s.charAt(0) == s.charAt(0) 且 j - i <= 1
    • res 加 1,变为 4;dp[0][0] 设为 true
  • 内层循环 j = 1
    • 子串为 "aa",首尾字符相同,且子串长度为 2(j - i <= 1 成立)。
    • res 加 1,变为 5;dp[0][1] 设为 true
  • 内层循环 j = 2
    • 子串为 "aaa",首尾字符都是 a,并且去掉首尾字符后的子串 "a" 对应的 dp[1][1] 为 true
    • res 加 1,变为 6;dp[0][2] 设为 true

步骤 3:返回结果

循环结束后,res 的值为 6,这就表明字符串 "aaa" 里有 6 个回文子串,分别是 "a""a""a""aa""aa""aaa"

整个过程中,通过双重循环和条件判断,不断检查不同子串是否为回文,并更新 dp 数组和 res 的值,最终得到回文子串的总数。

12.最长回文子序列

力扣题目链接(opens new window)

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bbbb"。

示例 2: 输入:"cbbd" 输出: 2 一个可能的最长回文子序列为 "bb"。

提示:

  • 1 <= s.length <= 1000
  • s 只包含小写英文字母
public class Longest_Palindromic_Subsequence {
    public int longestPalindromeSubseq(String s) {
        int len = s.length();
        int[][] dp = new int[len + 1][len + 1];//创建一个二维数组dp,其大小为(len + 1) x (len + 1),其中len是字符串s的长度。dp[i][j] 表示字符串 s 中从索引 i 到索引 j 的子串的最长回文子序列的长度。
        for (int i = len - 1; i >= 0; i--) {//单个字符必然是回文的,所以对于任意的 i,dp[i][i] 初始化为 1,表示长度为 1 的子串的最长回文子序列长度就是 1。
            dp[i][i] = 1;//外层循环 i 从字符串的最后一个字符开始向前遍历,内层循环 j 从 i + 1 开始向后遍历,这样可以确保我们是从较短的子串逐步扩展到较长的子串进行处理。
            for (int j = i + 1; j < len; j++) {//外层循环从后向前遍历字符串s,这样可以确保在检查子串时,我们总是从中心向外围扩展。
                if (s.charAt(i) == s.charAt(j)) {//内层循环也是从后向前遍历字符串s。
                    dp[i][j] = dp[i + 1][j - 1] + 2;//如果当前子串的首尾字符相同(s.charAt(i) == s.charAt(j)),则dp[i][j]的值是dp[i + 1][j - 1] + 2,因为我们可以在这个子串的最长回文子序列的基础上,加上这两个相同的字符。那么该子串的最长回文子序列长度等于去掉首尾字符后的子串(即从 i + 1 到 j - 1 的子串)的最长回文子序列长度加上 2(这 2 是首尾相同的两个字符)。
                } else {//当首尾字符不同时,需要考虑两种情况:dp[i + 1][j]:表示不包含首字符 s.charAt(i) 时,子串 s[i+1...j] 的最长回文子序列长度。dp[i][j - 1]:表示不包含尾字符 s.charAt(j) 时,子串 s[i...j - 1] 的最长回文子序列长度。取这两种情况中的最大值作为 dp[i][j] 的值。
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][len - 1];//遍历完成后,dp[0][len - 1]中存储的就是整个字符串s的最长回文子序列的长度。
    }
}

s = "cbbd"

步骤 1:初始化

  • 首先,字符串 s 的长度 len = 4,创建一个 5 x 5 的二维数组 dp
  • 初始化 dp 数组的对角线元素,即 dp[0][0] = dp[1][1] = dp[2][2] = dp[3][3] = 1,因为单个字符构成的子序列一定是回文的,其长度为 1。

此时 dp 数组如下:

0123
01000
10100
20010
30001

步骤 2:双重循环填充 dp 数组

外层循环 i = 3
  • 内层循环 j 从 i + 1 开始,由于 i = 3,此时没有满足 j > i 的情况,所以不进行操作。
外层循环 i = 2
  • 内层循环 j = 3
    • s.charAt(2) = 'b's.charAt(3) = 'd',首尾字符不同。
    • 根据 dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]),这里 dp[3][3] = 1dp[2][2] = 1,所以 dp[2][3] = Math.max(1, 1) = 1

此时 dp 数组变为:

0123
01000
10100
20011
30001
外层循环 i = 1
  • 内层循环 j = 2
    • s.charAt(1) = 'b's.charAt(2) = 'b',首尾字符相同。
    • 根据 dp[i][j] = dp[i + 1][j - 1] + 2,这里 dp[2][1] 初始为 0(未处理时默认为 0),所以 dp[1][2] = 0 + 2 = 2
  • 内层循环 j = 3
    • s.charAt(1) = 'b's.charAt(3) = 'd',首尾字符不同。
    • dp[2][3] = 1dp[1][2] = 2,所以 dp[1][3] = Math.max(1, 2) = 2

此时 dp 数组变为:

0123
01000
10122
20011
30001
外层循环 i = 0
  • 内层循环 j = 1
    • s.charAt(0) = 'c's.charAt(1) = 'b',首尾字符不同。
    • dp[1][1] = 1dp[0][0] = 1,所以 dp[0][1] = Math.max(1, 1) = 1
  • 内层循环 j = 2
    • s.charAt(0) = 'c's.charAt(2) = 'b',首尾字符不同。
    • dp[1][2] = 2dp[0][1] = 1,所以 dp[0][2] = Math.max(2, 1) = 2
  • 内层循环 j = 3
    • s.charAt(0) = 'c's.charAt(3) = 'd',首尾字符不同。
    • dp[1][3] = 2dp[0][2] = 2,所以 dp[0][3] = Math.max(2, 2) = 2

最终 dp 数组为:

0123
01122
10122
20011
30001

步骤 3:返回结果

最后返回 dp[0][3] 的值,即 2。这表明字符串 "cbbd" 的最长回文子序列长度为 2,例如 "bb" 就是一个最长回文子序列。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值