leetcode上关于子序列和字串问题有很多,大部分都可以用动态规划来实现。首先,先说一下这两个名词的区别,字串一定是连续的,而子序列不一定是连续的。下面对这些常见的问题做一个总结归纳。
子序列问题
300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
来源:leetcode-cn.com/problems/lo…
这是一个比较经典的问题,可以用动态规划实现,也可以用二分法来实现。
动态规划
var lengthOfLIS = function(nums) {
// dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
let len = nums.length;
if(!len) return 0;
let dp = new Array(len).fill(1);
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
return Math.max(...dp);
};
516. 最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
輸入:"bbbab"
輸出:4
一个可能的最长回文子序列为 "bbbb"。
來源:leetcode-cn.com/problems/lo…
var longestPalindromeSubseq = function(s) {
let dp = Array.from(new Array(s.length), () => new Array(s.length).fill(0));
for (let i = 0; i < s.length; i++) {
dp[i][i] = 1;
}
for (let i = s.length - 1; i >= 0; i--) {
for (let j = i + 1; j < s.length; j++) {
if(s[i] === s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1])
}
}
}
return dp[0][s.length - 1];
};
1143. 最长公共子序列
最長公共子序列问题是非常经典的面试题目,一般采用二维DP实现。大部分比较困难的问题和这个问题都是一个套路,非常值得掌握。
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
來源:leetcode-cn.com/problems/lo…
var longestCommonSubsequence = function(text1, text2) {
//dp[i][j] 的含义是:对于 text1[1..i] 和 text2[1..j],它们的 LCS 长度是 dp[i][j];
let len1 = text1.length, len2 = text2.length;
let dp = Array.from(new Array(len1 + 1), () => new Array(len2 + 1).fill(0));
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
if(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]);
}
}
}
return dp[len1][len2];
};
72. 编辑距离
该题是最长公共子序列问题的变种。
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
来源:leetcode-cn.com/problems/ed…
var minDistance = function(word1, word2) {
let l1 = word1.length;
let l2 = word2.length;
if(!l1) return l2;
if(!l2) return l1;
let dp = [];
dp[0] = [0];
for (let i = 1; i <= l1; i++) {
dp[i] = [];
dp[i][0] = i;
}
for (let j = 1; j <= l2; j++) {
dp[0][j] = j;
}
for (let i = 0; i < l1; i++) {
for (let j = 0; j < l2; j++) {
if(word1[i] === word2[j]) {
dp[i + 1][j + 1] = dp[i][j];
continue;
}
let insert = dp[i + 1][j] + 1;
let remove = dp[i][j + 1] + 1;
let modify = dp[i][j] + 1;
dp[i + 1][j + 1] = Math.min(insert, remove, modify);
}
}
return dp[l1][l2];
};
115. 不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)
题目数据保证答案符合 32 位带符号整数范围。
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
来源:leetcode-cn.com/problems/di…
var numDistinct = function(s, t) {
// dp[i][j]代表t前i字符串可以由s中j字符串组成最多个数
let n = s.length, m = t.length;
let dp = Array.from(new Array(m + 1), () => new Array(n + 1).fill(0));
for (let i = 0; i <= n; i++) {
dp[0][i] = 1;
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if(t[i - 1] === s[j - 1]) {
dp[i][j] = dp[i][j - 1] + dp[i - 1][j - 1];
} else {
dp[i][j] = dp[i][j - 1];
}
}
}
return dp[m][n];
};
53. 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
来源: leetcode-cn.com/problems/ma…
贪心算法实现
var maxSubArray = function(nums) {
if (nums == null || nums.length == 0) return 0;
let max = Number.MIN_SAFE_INTEGER;
for (let begin = 0; begin < nums.length; begin++) {
let sum = 0;
for (let end = begin; end < nums.length; end++) {
// sum是[begin, end]的和
sum += nums[end];
max = Math.max(max, sum);
}
}
return max;
};
分治实现
var maxSubArray = function(nums) {
return maxSubArray(nums, 0, nums.length);
function maxSubArray(nums, begin, end) {
if (end - begin < 2) return nums[begin];
let mid = (begin + end) >> 1;
let leftMax = nums[mid - 1];
let leftSum = leftMax;
for (let i = mid - 2; i >= begin; i--) {
leftSum += nums[i];
leftMax = Math.max(leftMax, leftSum);
}
let rightMax = nums[mid];
let rightSum = rightMax;
for (let i = mid + 1; i < end; i++) {
rightSum += nums[i];
rightMax = Math.max(rightMax, rightSum);
}
return Math.max(leftMax + rightMax,
Math.max(
maxSubArray(nums, begin, mid),
maxSubArray(nums, mid, end))
);
}
};
动态规划实现
var maxSubArray = function(nums) {
if (nums === null || nums.length === 0) return 0;
let dp = nums[0];
let max = dp;
for (let i = 1; i < nums.length; i++) {
if(dp <= 0) {
dp = nums[i];
} else {
dp = dp + nums[i];
}
max = Math.max(dp, max);
}
return max;
};
子串问题
5. 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
来源:leetcode-cn.com/problems/lo…
var longestPalindrome = function(s) {
let len = s.length;
if(len < 2) return s;
let maxL = 1, begin = 0;
let dp = Array.from(new Array(len), () => new Array(len).fill(0));
for (let j = 1; j < len; j++) {
for (let i = 0; i < j; i++) {
if(s[i] !== s[j]) {
dp[i][j] = false;
} else {
if(j - i < 3) {
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
if(dp[i][j] && j - i + 1 > maxL) {
maxL = j - i + 1;
begin = i;
}
}
}
return s.substring(begin, begin + maxL);
};
647. 回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
输入:"aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
来源:leetcode-cn.com/problems/pa…
var countSubstrings = function(s) {
let len = s.length;
let dp = Array.from(new Array(len), () => new Array(len).fill(0));
let ans = 0;
for (let i = 0; i < len; i++) {
for (let j = 0; j <= i; j++) {
if(s[j] === s[i]) {
if(i - j < 2 || dp[j+1][i-1]) {
dp[j][i] = true;
ans++;
}
}
}
}
return ans;
};