代码随想录-动态规划-子序列

子序列篇

T300-最长递增子序列

见LeetCode第300题[最长递增子序列]

题目描述

给你一个整数数组 nums ,找到其中最长严格递增 子序列的长度。 子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

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

我的思路

  • 定义dp[]为当前长度的子数组下,最长的子序列长度,初始化为1
  • 对于每个元素nums[i],从i + 1的位置开始遍历到数组尾部
  • 如果nums[j] > nums[i]dp[j] = Math.max(dp[j], dp[i + 1])
  • 最后返回dp[n - 1]即可 返回最大的dp
java 复制代码
public int lengthOfLIS(int[] nums) {
    if (nums.length <= 0) return nums.length;
    int n = nums.length;

    // 初始化 maxLen 数组,表示 0-i 的子数组最大的严格递增子序列的长度
    int[] maxLen = new int[n];
    // 默认的最大长度都为 1
    Arrays.fill(maxLen, 1);
    int res = 1;

    for (int i = 0; i < n - 1; i++) {
        for (int j = i + 1; j < n; j++) {
            if (nums[j] > nums[i]) { // 严格递增
                maxLen[j] = Math.max(maxLen[j], maxLen[i] + 1);
            }
        }
        res = Math.max(res, maxLen[i + 1]);
    }
    return res;
}

计算复杂度分析

  • 时间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2)
  • 空间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)

优化方法

二分法可以达到 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N log ⁡ N ) O(N\log N) </math>O(NlogN)的计算复杂度。

T674-最长连续递增子序列

见LeetCode第674题[最长连续递增子序列]

题目描述

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

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

示例 1:

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

我的思路

  • 连续严格递增子序列,就是严格递增子数组
  • 对于连续的子数组,可以使用快慢指针解决
java 复制代码
public int findLengthOfLCIS(int[] nums) {
    if (nums.length <= 1) return nums.length;

    int fast = 1;
    int slow = 0;
    int maxLen = 1;
    while (fast < nums.length) {
        // 找到非严格递增的索引,此时 fast 在极大点的下个位置
        while (fast < nums.length && nums[fast] > nums[fast - 1]) fast++;
        // 更新最长子数组的长度
        maxLen = Math.max(maxLen, fast - slow);
        // 更新快慢指针,注意更新完慢指针,快指针向前移动一步
        slow = fast++;
    }
    return maxLen;
}

计算复杂度分析

  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)遍历了数组中的每个元素
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)

T718-最长重复子数组

见LeetCode第718题[最长重复子数组]

题目描述

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

示例 1:

css 复制代码
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7] 
输出:3 
解释:长度最长的公共子数组是 [3,2,1] 。

我的思路[暴力匹配]

  • 遍历nums1中的每个元素,从nums2中寻找和nums1[i]相等的元素nums2[j]
  • 分别从ij开始匹配,记录当前匹配到的长度
  • 更新最大长度
java 复制代码
public int findLength(int[] nums1, int[] nums2) {
    if (nums1.length == 0 || nums2.length == 0) return 0;
    int maxLen = 0;
    int m = nums1.length;
    int n = nums2.length;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (nums1[i] != nums2[j]) continue;
            // 找到头部,开始匹配
            int len = findPublicSubArray(nums1, nums2, i, j);
            maxLen = Math.max(maxLen, len);
        }
    }
    return maxLen;
}

/**
 * 从当前位置开始匹配
 * @param nums1
 * @param nums2
 * @param i
 * @param j
 * @return
 */
private int findPublicSubArray(int[] nums1, int[] nums2, int start1, int start2) {

    int i = start1 + 1;
    int j = start2 + 1;
    while (i < nums1.length && j < nums2.length && nums1[i] == nums2[j]) {
        i++;
        j++;
    }
    return i - start1;
}

优秀思路

这是一个模拟卷积的过程,一个静止,另一个滑动,然后寻找重复部分的最大值。只不过,卷积需要相乘求和,但是这个不需要。

开始之前,固定比较短的数组nums1,滑动比较长的数组nums2

第一阶段 ,两者之间相交的部分越来越长,从1到较短数组的长度m

此时,固定数组的相交部分索引为[0, len),滑动部分nums2相交部分的索引为[n - len, n),需要在这两个公共部分开始寻找最大值,函数可以命名为findPublic(nums1, 0, len, nums2, n - len, n)

第二阶段 ,两者之间的相交部分保持不变,为较短数组的长度,即nums1的长度m

此时,相交部分的索引分别为[0, m)[n - m, n)

第三阶段 ,两者之间相交的部分越来越短,从m - 11

此时,nums1nums2相交部分的索引分别为[m - len, m)[0, len)

最后,返回整个过程中最大的长度maxLen即可。

java 复制代码
public int findLength(int[] nums1, int[] nums2) {
    if (nums1.length == 0 || nums2.length == 0) return 0;
    return nums1.length < nums2.length ? findLongest(nums1, nums2) : findLongest(nums2, nums1);
}

private int findLongest(int[] nums1, int[] nums2) {
    int maxLen = 0;
    int len1 = nums1.length;
    int len2 = nums2.length;

    // 第一阶段,二者相交的部分越来越大
    for (int len = 1; len <= len1; len++) {
        maxLen = Math.max(maxLen, findMax(nums1, 0, len, nums2, len2 - len, len2));
    }

    // 第二阶段,二者相交的部分相等
    for (int i = len2 - len1; i >= 0; i--) {
        maxLen = Math.max(maxLen, findMax(nums1, 0, len1, nums2, i, i + len1));
    }

    // 第三阶段,二者相交的部分越来越小
    for (int len = len1; len >= 1; len--) {
        maxLen = Math.max(maxLen, findMax(nums1, len1 - len, len1, nums2, 0, len));
    }
    return maxLen;
}

private int findMax(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2) {
    int max = 0;
    int count = 0;
    int j = start2;
    // 寻找最大的公共部分
    for (int i = start1; i < end1; i++) {
        if (nums1[i] == nums2[j++]) count++;
        else {
            max = Math.max(count, max);
            count = 0;
        }
    }
    return Math.max(count, max);
}

计算复杂度分析

  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)

T1143-最长公共子序列

见LeetCode第1143题[最长公共子序列]

题目描述

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

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

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

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

我的思路

  • 定义int[][] dp为子串ij的情况下,公共子序列的最大值

状态转移方程怎么求呢?

对于指针ij,其所指的字符有两种情况

第一种:指针所指的两个字符相等 ,即text[i] == text[j]。这种情况下,当前字符为公共字符,当前的最长公共子串应为,dp[i - 1][j - 1] + 1

第二种情况,指针所指的两个字符不相等 。既然不相等,那么某个指针i或者j往后退一格,仍然不会影响结果,取二者的最大值,即Math.max(dp[i - 1][j], dp[i][j - 1])

dp数组中的初始化条件?

第一行、第一列表示字符的范围为[0, 0),因此其应该置为 0.

java 复制代码
public int longestCommonSubsequence(String text1, String text2) {
    if (text1.length() == 0 || text2.length() == 0) return 0;
    int m = text1.length();
    int n = text2.length();

    // 初始化 dp 数组
    int[][] dp = new int[m + 1][n + 1];

    // 更新 dp
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; 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[m][n];
}

反思

像这种子序列的公共部分问题,大部分都是可使用动态规划解决的。为什么我解决不出来呢?

首先是dp数组的定义 ,一般情况下,dp数组的定义基本是子问题的最优解。一定要明确dp数组的定义。

其次是状态。站在当前的节点上,当前的子问题上,有几种状态可供选择?选还是不选都是通过条件决出的。

最后就是选择 ,从选择中推导出dp的递推公式。因为是局部最优解,因此有多个选择的话,选择也应该选择最优的。

T1035-不相交的线

见LeetCode第1035题[不相交的线]

题目描述

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

现在,可以绘制一些连接两个数字 nums1[i]nums2[j] 的直线,这些直线需要同时满足: nums1[i] == nums2[j] 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。 以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 2:

css 复制代码
输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2] 
输出:3

我的思路

这题看着挺唬人的,但是和T1143题是同宗题,本质上都是求最大公共子序列。

首先,不相交的线 确保了子序列的相等有序 。其次,每个数字只能连接一条线,保证了一对一的关系。

第一步,明确int[][] dp数组的定义 。即为在子数组为[0, i)[0, j)的情况下最长的公共子序列。

第二步,状态 。对于nums[i]nums[j],二者的值相等或者不相等。

第三步,选择。根据状态来选择出子问题的最优解。

  • 要么相等,则当前问题的最优解为dp[i - 1][j - 1] + 1
  • 要么不相等,则当前问题最优解为,Math.max(dp[i - 1][j], dp[i][j - 1])'
java 复制代码
public int maxUncrossedLines(int[] nums1, int[] nums2) {
    if (nums1.length == 0 || nums2.length == 0) return 0;

    int m = nums1.length;
    int n = nums2.length;

    // 初始化 dp 数组
    int[][] dp = new int[m + 1][n + 1];

    // 更新 dp 数组
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 当前两个数字相等的情况
            if (nums1[i - 1] == nums2[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[m][n];
}

计算复杂度分析

  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N × N ) O(N\times N) </math>O(N×N)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N × N ) O(N\times N) </math>O(N×N)

T53-最大子数组和

见LeetCode第53题[最大子数组和]

题目描述

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

子数组是数组中的一个连续部分。

示例 1:

ini 复制代码
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。

我的思路

数组下标是连续的,因此可以使用滑动窗口来解决

滑动窗口的要点就是,左右两个窗口应该如何移动?

  • 如果右边界的元素值为正数,判断当前和和该元素nums[r]的关系
    • 如果nums[r] >= curSum && curSum < 0,则左边界移动到该位置,右边界移动一位
    • 反之,则直接入窗
  • 如果右边界元素值为负数
    • 更新最大值maxSum,并将该元素纳入窗口
java 复制代码
public int maxSubArray(int[] nums) {
    if (nums.length == 1) return nums[0];

    int maxSum = nums[0];
    int curSum = nums[0];
    // 初始化窗口边界,是一个左闭右开的范围[l, r)
    int l = 0;
    int r = 1;

    while (r <= nums.length - 1) {
        // 当前元素的值大于窗口之和,并且窗口之和为负数,则直接滑动窗口到当前值
        if (nums[r] >= curSum && curSum < 0) {
            // 更新当前和 以及 最大和
            curSum = nums[r];
            maxSum = Math.max(maxSum, curSum);
            // 更新窗口
            l = r++;
        } else {
            // 直接将当前边界值纳入
            // 边界值大于0,则纳入之后更新最大值
            if (nums[r] > 0 ) {
                curSum += nums[r];
                maxSum = Math.max(maxSum, curSum);
            } else {
                // 纳入之前跟新最大值
                maxSum = Math.max(maxSum, curSum);
                curSum += nums[r];
            }
            // 更新右边界
            r++;
        }
    }
    return maxSum;
}

计算复杂度分析

  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)

优秀思路

通过观察上述代码可以发现,不太需要左边窗口的边界l,实际上就是遍历整个数组元素,然后判断该元素,是否应该去入窗winSum

当前窗口和应该如何更新?winSum = Math.max(curNum, winSum + curNum)

当前最大值如何更新?maxSum = Math.max(maxSum, winSum)

java 复制代码
public int maxSubArrayI(int[] nums) {
    // 初始化
    int winSum = 0;
    int maxSum = nums[0];

    for (int curNum : nums) {
        winSum = Math.max(curNum, winSum + curNum);
        maxSum = Math.max(winSum, maxSum);
    }
    return maxSum;
}

T392-判断子序列

见LeetCode第392题[判断子序列]

题目描述

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

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

进阶: 如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

示例 1

ini 复制代码
输入:s = "abc", t = "ahbgdc" 
输出:true

我的思路

  • 外层循环遍历t的每个元素
  • 如果当前元素tChar == sChar,则子字符串s的指针count++
  • 判断count == s.length()即可
java 复制代码
public boolean isSubsequence(String s, String t) {
    if (s.length() > t.length()) return false;

    int count = 0;
    for (char c : t.toCharArray()) {
        if (count == s.length()) return true;
        if (c == s.charAt(count)) {
            count++;
        }
    }
    return count == s.length();
}

计算复杂度分析

  • 时间复杂度:从代码中可以看出,我们分别遍历了st中的每一个字符串数组,因此计算复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( M + N ) O(M + N) </math>O(M+N)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)

进阶思路分析

在大量的s输入下,时间复杂度会变得非常高。我们主要的计算复杂度浪费在了,成功匹配当前字符串之后,寻找下一个字符存在的位置

当前寻找下一个字符是使用线性遍历的方式,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N ) O(N) </math>O(N),如果我在匹配成功之后,马上知道下一个字符的位置,时间复杂度就是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)了。如何去实现这样的效果呢?

我们可以创建一个int[][] dp数组,用来存储从当前索引i开始,字符j第一次出现的位置

如果当前字符元素为j,则dp[i][j] = i,否则dp[i][j] = dp[i + 1][j]。因此,我们需要从右往左更新dp数组。争取将母串t的所有元素都记录在案。

java 复制代码
public boolean isSubsequenceI(String s, String t) {
    if (s.length() > t.length()) return false;

    int m = t.length();
    // 初始化 dp 数组,表示从当前索引 i 开始,任意字符 j 第一次出现的位置
    int[][] dp = new int[m + 1][26];
    // 对边界填充非法值,表示不可达
    Arrays.fill(dp[m], m);

    // 逆向更新 dp
    for (int i = m - 1; i >= 0; i--) {
        for (int j = 0; j < 26; j++) {
            if (t.charAt(i) - 'a' == j) {
                dp[i][j] = i;
            } else {
                dp[i][j] = dp[i + 1][j];
            }
        }
    }

    // 处理子串,初始化母串的索引curIndex
    int curIndex = 0;
    for (char sChar : s.toCharArray()) {
        // 不存在的情况,即出现了非法值
        if (dp[curIndex][sChar - 'a'] == m) {
            return false;
        }
        curIndex = dp[curIndex][sChar - 'a'] + 1;
    }
    return true;
}

T115-不同的子序列

见LeetCode第115题[不同的子序列]

题目描述

给你两个字符串 s **和 t ,统计并返回在 s子序列t 出现的个数,结果需要对 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 9 + 7 10^9 + 7 </math>109+7 取模。

示例 1:

ini 复制代码
输入: s = "rabbbit", t = "rabbit"
输出 : 3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit

我的思路

  • 这题比较像编辑最短距离,都是通过删除字符来让s逼近t

通过观察示例可以发现,r 后面只有1个 a ,a 后面有3个 b,b后面只有一个i,i后面只有一个t,因此,答案可从下面的方式中取得:

  • 对于第一个 b,后面有 一个 i 和 1个 t,所以有1种方法
  • 对于第二个 b,后面有 一个 i 和 1个 t,所以有1种方法
  • 对于第三个 b,后面有 一个 i 和 1个 t,所以有1种方法

共计 3 中方法。

可以借鉴T392判别子序列的方式,先遍历母串s,获取其从当前坐标开始,每个字符出现的位置和次数,依次递归,将每种可能相加,即可得到最后的结果。

没思路,行不通

题解思路

对于大部分两个字符串的匹配问题,一般都是使用二维动态规划。两个维度分别表示两个字符串,而二维矩阵中的某个点,则表示子问题的最优解。

这种题的状态转移方程,可以在纸上手动画一个表格,研究一下状态转移的过程。比如从本题的rabbit入手,

css 复制代码
    r a b b b i t
 r  1 1 1 1 1 1 1
 a  0 1 1 1 1 1 1
 b  0 0 1 2 3 3 3
 b  0 0 0 1 3 3 3
 i  0 0 0 0 0 3 3
 t  0 0 0 0 0 0 3

对于子串i和母串j的字符,两者要么相等,要么不相等

  • 如果相等,那么dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1]
  • 如果不相等,那么dp[i][j] = dp[i][j - 1]

此外,我们可以看出,这个矩阵是一个上三角的,因此在更新的时候可以注意一下索引值。

java 复制代码
public int numDistinct(String s, String t) {
    if (s.length() < t.length()) return 0;

    int m = s.length();
    int n = t.length();

    // 初始化 dp 数组
    int[][] dp = new int[m + 1][n + 1];
    // 将第一列置为1,子字符串长度为0则仅有一种方法
    for (int i = 0; i <= m; i++) {
        dp[i][0] = 1;
    }
    // 更新 dp 数组
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if(j > i) break;
            // 如果母子串的当前值相等,则进行状态转移
            if (s.charAt(i - 1) == t.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j] // 母串左移一位,包含子串数量
                        + dp[i - 1][j - 1]; // 母串左移一位,子串左移一位,包含子串的数量
            } else {
                dp[i][j] = dp[i - 1][j]; // 母串左移一位,包含子串的数量
            }
        }
    }
    return dp[m][n];
}

计算复杂度分析

  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N × M ) O(N \times M) </math>O(N×M)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N × M ) O(N \times M) </math>O(N×M)

T583-两个字符串的删除操作

见LeetCode第583题[两个字符串的删除操作]

题目描述

给定两个单词 word1word2 ,返回使得 word1word2 **相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

示例 1:

arduino 复制代码
输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"

我的思路

两个字符串,不管是寻找子字符串还是编辑最短距离,都是通过动态规划来解决的。

首先要明确dp[][]数组的定义,一般情况下,dp数组定义为二维数组,行和列分别表示两个字符串,需要解决的子问题就是[0, i)[0, j)两个子串的问题。

第二步,寻找当前子问题的状态。对于dp[i][j],其值和当前两个字符s[i]t[j]相关,而这两个字符只有两种状态:相等 或者不相等

第三步,根据状态,确定选择。

  • 如果s[i] == t[j],则两个指针同时往后退一步,其状态转移方程为dp[i][j] = dp[i - 1][j - 1]
  • 如果s[i] != t[j],那么我们就需要考虑删除这个字符,步数增加 1 ,具体删除s还是t子串的字符,需要根据情况判定,即dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1

最后,返回结果dp[m][n]即可。

需要注意初始化dp数组的第一行和第一列。

java 复制代码
/**
 * 两个字符串的删除操作,求两个字符串的最短距离
 * @param word1
 * @param word2
 * @return
 */
public int minDistance(String word1, String word2) {

    int m = word1.length();
    int n = word2.length();

    // 初始化 dp 数组,dp[i][j]表示两个子串[0, i)和[0, j)之间的最小编辑距离
    int[][] dp = new int[m + 1][n + 1];
    // 初始化第一行和第一列
    for (int i = 1; i <= m; i++) {
        dp[i][0] = i;
    }
    for (int i = 1; i <= n; i++) {
        dp[0][i] = i;
    }

    // 更新 dp 数组
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 当前字符相等的情况
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else { // 不相等的情况,看从哪个子串删除当前字符合适
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
            }
        }
    }
    return dp[m][n];
}

计算复杂度分析

  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N × M ) O(N \times M) </math>O(N×M)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N × M ) O(N \times M) </math>O(N×M)

优化思路

可以将二维dp数组压缩至一维,但是内层循环在遍历的时候,注意从后往前遍历,防止更新之后的数据影响后续数据。

T72-编辑距离

见LeetCode第72题[编辑距离]

题目描述

给你两个单词 word1word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。 你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1

rust 复制代码
输入:word1 = "horse", word2 = "ros" 
输出:3 
解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')

我的思路

这一题和第583题[两个字符串的删除距离]非常相似,解法思路一致。

现在,我们考虑一下int[][] dp的状态转移方程,对于s[i]t[j]

  • s[i] == t[j],则dp[i][j] = dp[i - 1][j - 1]
  • s[i] == t[j],当前有三种选择,插入、删除和替换
    • 插入:dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + 1
    • 删除:dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + 1
    • 替换:dp[i][j] = dp[i - 1][j - 1] + 1

插入,和删除是否是等价的?是的!

java 复制代码
/**
 * 编辑最短距离
 * @param word1
 * @param word2
 * @return
 */
public int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();

    // 初始化 dp 数组
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 0; i <= m; i++) {
        dp[i][0] = i;
    }
    for (int i = 0; i <= n; i++) {
        dp[0][i] = i;
    }

    // 更新 dp 数组
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            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 - 1][j], dp[i][j - 1]), // 删除或者插入
                        dp[i - 1][j - 1] // 替换
                ) + 1;
            }
        }
    }
    return dp[m][n];
}

T647-回文子串

见LeetCode第647题[回文子串]

题目描述

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

示例:

arduino 复制代码
输入: s = "aaa"
输出: 6
解释: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

我的思路

dp数组如何定义?dp[i][j]表示从区间[i, j]之间的子串是否为回文串

不难看出,这应该是一个上三角矩阵,也就是说,j > i

我们先根据一个例子,看看他的dp矩阵是怎么样的

复制代码
aaa
   0 1 2
0  1 1 1
1  0 1 1
2  0 0 1

我们可以看出,对于i == j的情况,也就是单个字符,这样他肯定是一个回文串。

而对于i == j - 1的情况,也就是仅有两个字符的情况,这时dp[i][j] = s.charAt[i] == s.charAt(j)

对于其他的任意的j > i的情况,也就是大于三个字符的情况,这时dp[i][j] == dp[i + 1][j - 1] && s.charAt[i] == s.charAt(j)

dp[i][j]true的时候,就使结果值加1

需要注意的是,对于外层循环,一定要从n - 10去更新。

java 复制代码
/**
 * 回文子串的数量
 * @param s
 * @return
 */
public int countSubstrings(String s) {
    if (s.length() <= 1) return s.length();

    int n = s.length();

    int res = n;

    // dp 数组定义为 区间[i, j)之间是否是回文串
    boolean[][] dp = new boolean[n][n];
    // 更新 dp 数组
    for (int i = 0; i < n; i++) {
        dp[i][i] = true;
    }
    for (int i = n - 1; i >= 0; i--) {
        for (int j = i + 1; j < n; j++) {
            if (i == j - 1) dp[i][j] = s.charAt(i) == s.charAt(j);
            else dp[i][j] = dp[i + 1][j - 1] && s.charAt(i) == s.charAt(j);
            if (dp[i][j]) res++;
        }
    }
    return res;
}

计算复杂度分析

  • 时间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2)
  • 空间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)

T516-最长回文子序列

见LeetCode第516题[最长回文子序列]

题目描述

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

ini 复制代码
输入: s = "bbbab"
输出: 4
解释: 一个可能的最长回文子序列为 "bbbb" 。

我的思路

注意,这和上题不同的点是,本题是最长的子序列,而不是子数组,因此不能用中心扩散的方式了。

定义 。那么,如何去定义dp[][]数组?dp[i][j]可否定义为[i, j]之间的最长回文子串的长度?

状态。 对于字符s.charAt(i)s.charAt(j)显然有两种状态:要么相等,要么不等。

选择。

  • 相等:则dp[i][j] = dp[i + 1][j - 1] + 2
  • 不相等:则dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1])

在遍历的时候,一定要注意,外层的循环即i应该从大到小进行更新。此外,在更新的过程中,记录寻找最大的dp值。

java 复制代码
/**
 * 最长回文子序列
 * @param s
 * @return
 */
public int longestPalindromeSubseq(String s) {
    if (s.length() <= 1) return s.length();

    int n = s.length();
    int maxLen = 1;

    // 初始化 dp 数组
    int[][] dp = new int[n][n];
    for (int i = 0; i < n; i++) {
        dp[i][i] = 1;
    }

    // 更新 dp 数组
    for (int i = n - 1; i >= 0; i--) {
        for (int j = i + 1; j < n; j++) {
            if (s.charAt(i) == s.charAt(j)) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
            }
            maxLen = Math.max(dp[i][j], maxLen);
        }
    }

    return maxLen;
}

计算复杂度分析

  • 时间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2)
  • 空间复杂度 : <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2)
相关推荐
遗憾皆是温柔3 分钟前
19. 重载的方法能否根据返回值类型进行区分
java·开发语言·面试·学习方法
修仙的人8 分钟前
【开发环境】 VSCode 快速搭建 Python 项目开发环境
前端·后端·python
FinalLi10 分钟前
SpringBoot3.5.0项目使用ALLATORI JAVA混淆器
后端
bobz9651 小时前
用于服务器测试的 MCP 开发工具
后端
SimonKing1 小时前
流式数据服务端怎么传给前端,前端怎么接收?
java·后端·程序员
Laplaces Demon1 小时前
Spring 源码学习(十)—— DispatcherServlet
java·后端·学习·spring
BigYe程普1 小时前
出海技术栈集成教程(一):域名解析与配置
前端·后端·全栈
这里有鱼汤1 小时前
如何用‘资金视角’理解短线交易?这篇讲透了!
后端
扶风呀1 小时前
负载均衡详解
运维·后端·微服务·面试·负载均衡
uhakadotcom1 小时前
在nodejs之中, userUuid !== '' 和 userUuid != ''是一样的吗?
前端·javascript·面试