子序列篇
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题[最长连续递增子序列]
题目描述
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l
和 r
(l < 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题[最长重复子数组]
题目描述
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
css
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
我的思路[暴力匹配]
- 遍历
nums1
中的每个元素,从nums2
中寻找和nums1[i]
相等的元素nums2[j]
- 分别从
i
和j
开始匹配,记录当前匹配到的长度 - 更新最大长度
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 - 1
到1
此时,nums1
和nums2
相交部分的索引分别为[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题[最长公共子序列]
题目描述
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 **是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
ini
输入: text1 = "abcde", text2 = "ace"
输出: 3
解释: 最长公共子序列是 "ace" ,它的长度为 3 。
我的思路
- 定义
int[][] dp
为子串i
和j
的情况下,公共子序列的最大值
状态转移方程怎么求呢?
对于指针i
和j
,其所指的字符有两种情况
第一种:指针所指的两个字符相等 ,即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题[不相交的线]
题目描述
在两条独立的水平线上按给定的顺序写下 nums1
和 nums2
中的整数。
现在,可以绘制一些连接两个数字 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题[判断子序列]
题目描述
给定字符串 s
和 t
,判断 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();
}
计算复杂度分析
- 时间复杂度:从代码中可以看出,我们分别遍历了
s
和t
中的每一个字符串数组,因此计算复杂度为 <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题[两个字符串的删除操作]
题目描述
给定两个单词 word1
和 word2
,返回使得 word1
和 word2
**相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 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题[编辑距离]
题目描述
给你两个单词 word1
和 word2
, 请返回将 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 - 1
到0
去更新。
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)