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

子序列篇

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)
相关推荐
浪遏9 分钟前
面试官😏: 讲一下事件循环 ,顺便做道题🤪
前端·面试
uhakadotcom12 分钟前
Pandas入门:数据处理和分析的强大工具
后端·面试·github
Asthenia041215 分钟前
Json里面想传图片(验证码图)-Base64编码来助你!
后端
服务端技术栈1 小时前
MySQL 索引:数据库查询的“加速器”
后端
Asthenia04121 小时前
Redis与MySQL协同:旁路缓存机制
后端
hamburgerDaddy11 小时前
golang 从零单排 (一) 安装环境
开发语言·后端·golang
Asthenia04121 小时前
线程的生命周期状态你烂熟于心,那线程池呢?Running/ShutDown/Stop/Tidying/Terminated
后端
小画家~1 小时前
第本章:go 切片
开发语言·后端·golang
浪遏1 小时前
面试官:字符串反转有多少种实现方式 ?| 一道题目检测你的基础
前端·面试
小画家~1 小时前
第五章:go 的数据类型 及go语言拼接字符串有哪些方式
开发语言·后端·golang