『力扣题解』最长回文子串、子序列问题

79 阅读2分钟

回文系列的典型问题共有三道,分别是:

其中前两题求子串,即字符得是连续的;第三题子序列,字符可以不连续。所以在做法上,前两题也是比较类似的。

三道题都可以用动态规划,都适用二维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;
};