回文系列的典型问题共有三道,分别是:
其中前两题求子串,即字符得是连续的;第三题子序列,字符可以不连续。所以在做法上,前两题也是比较类似的。
三道题都可以用动态规划,都适用二维dp数组,在遍历顺序上一致,即从下向上,从左往右。因为想要判断[i, j]区间是否构成回文子串,在s[i] == s[j]的基础上,还需要看[i + 1, j - 1]是不是回文子串,i控制区间左部,j控制区间右部。由于i依赖i + 1,j依赖j - 1,所以i从后向前遍历,j从前向后遍历。
回文子串
子串的两道题目,dp[i][j]表示区间[i, j]是否构成回文子串,即值是true/false,在for循环的判断逻辑中,在s[i] == s[j]的基础上,以下三种情况都满足回文子串:
- i == j
- j - i == 1
- dp[i + 1][j - 1] == true
前两种情况合起来写,是j - i <= 1。
如果判断dp[i][j]等于true了,就更新回文子串的数目。
// @lc code=start
/**
* @param {string} s
* @return {number}
*/
var countSubstrings = function (s) {
const len = s.length;
const dp = Array(len)
.fill(false)
.map(() => Array(len).fill(false));
let ans = 0;
for (let i = len - 1; i >= 0; i--) {
for (let j = i; j < len; j++) {
if (s[i] === s[j]) {
if (j - i <= 1 || dp[i + 1][j - 1]) {
dp[i][j] = true;
ans++;
}
}
}
}
return ans;
};
最长回文子串
和上一道题基本一致,只是当判断dp[i][j]等于true后,计算当前回文子串的长度,然后去更新最大长度,以及此时的左右指针。
/**
* @param {string} s
* @return {string}
*/
// 中心扩散法
var longestPalindrome = function (s) {
const len = s.length;
let max = 0;
let left = 0;
let right = 0;
for (let i = 0; i < len; i++) {
let l = i;
let r = i;
while (l > 0 && s[l] === s[l - 1]) {
l--;
}
while (r < len && s[r] === s[r + 1]) {
r++;
}
while (l > 0 && r < len && s[l - 1] === s[r + 1]) {
l--;
r++;
}
if (r - l + 1 > max) {
max = r - l + 1;
left = l;
right = r;
}
}
return s.slice(left, right + 1);
};
// dp
var longestPalindrome = function (s) {
const len = s.length;
let max = 1;
let left = 0;
let right = 0;
const dp = Array(len)
.fill(false)
.map(() => Array(len).fill(false));
for (let i = len - 1; i >= 0; i--) {
for (let j = i; j < len; j++) {
if (s[i] === s[j]) {
if (j - i <= 1 || dp[i + 1][j - 1]) {
dp[i][j] = true;
if (j - i + 1 > max) {
max = j - i + 1;
left = i;
right = j;
}
}
}
}
}
return s.slice(left, right + 1);
};
最长回文子序列
序列和子串稍有不同,虽然这题和上一题一样,都是求“最大长度”,但是由于子串是连续的,即使不在dp数组中保存最大长度,也能通过 j - i + 1 计算出来,所以dp中保存布尔值。而子序列,dp数组保存[i, j]中最长回文子序列的长度。
dp数组要初始化,for循环中的逻辑也不一样,j从i + 1开始。因为如果j从i开始,会直接进入s[i] == s[j]的判断,出现错误。至于else里的Math.max(...),则是子序列问题的“传统艺能”,不再解释。
// @lc code=start
/**
* @param {string} s
* @return {number}
*/
var longestPalindromeSubseq = function (s) {
const len = s.length;
const dp = Array(len)
.fill(0)
.map(() => Array(len).fill(0));
let ans = 1;
for (let i = 0; i < len; i++) {
dp[i][i] = 1;
}
for (let i = len - 1; i >= 0; i--) {
for (let j = i + 1; j < len; j++) {
if (s[i] === s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
ans = Math.max(ans, dp[i][j]);
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return ans;
};