动态规划 最少添加字符构成回文字符串

116 阅读3分钟

题目

4175f6c0378b1360b4b176fedf428ebe.png

  • 遍历模型为:遍历每一个范围的子串[i,j]

  • 如果 i = j,此时子串长度为1,不需要添加字符;

  • 如果 [i,i+1] ,即子串长度为2,如果两个字符相等,不需要添加字符,如果不相等,只需要添加其中1字符就能构成回文

  • 如果是其余情况,进行可能性分析,分为3种情况

    • [i,j] 范围,先让 [i,j-1] 范围构成回文,然后在 i 的左边添加 j 位置的字符构成回文, dp[i][j-1]+1
    • [i,j] 范围,先让 [i+1,j] 范围构成回文,然后在 j 的右边添加 i 位置的字符构成回文,dp[i+1][j]+1
    • [i,j] 范围,如果 i、j 位置字符相等,那么只需 [i+1,j-1] 范围构成回文即可,dp[i+1][j-1]
  • 当填完 dp 表,得到整个字符串最小需要添加的字符数量后,根据 dp 表开始回退路径,从右上角开始(右上角为最少需要添加的字符数答案)

  • 如果答案来自左边,则说明是[i,j]范围内, [i,j-1] 范围构成回文,然后在 i 的左边添加 j 位置的字符构成回文这种情况,所以数组的首尾字符为 j 位置字符,然后首位指针向右左锁进

  • 来到上一步确立答案来源的位置,继续判断来自可能性分析中的哪种情况,然后填写左右指针位置的字符,直到遍历到起始位置

  • 如果是要求所有添加字符的情况,那在遇到比如可以选左也可以选右情况时,进行深度优先遍历走完所有情况统计答案

2704028bd356edd7453ac25c350d8370.png

15a97b92b018ba48fdc2ed5fed07ad0b.png

function process(str) {
  let dp = [];
  let row = str.length,
    col = str.length,
    res = Infinity;

  // 区间长度为1时,已经是回文,不用添加
  for (let i = 0; i < row; i++) {
    dp[i][j] = 0;
  }
  // 当区间长度为2时,如果字符相等不用添加,字符不等只需要添加两个字符中其中1个就能构成回文
  for (let i = 1; i < row; i++) {
    if (str[i] === str[i - 1]) {
      dp[i][i - 1] = 0;
    } else {
      dp[i][i - 1] = 1;
    }
  }

  // 其余情况
  // 从下往上,从右往左填
  for (let row = str.length - 3; row >= 0; row--) {
    for (let col = str.length - 1; cor >= 2; col--) {
      // 只填写上半区中间对角线和中间对角线右边的对角线以上的区域
      if (row + 2 > col) {
        break;
      }

      res = Math.min(res, dp[row][col - 1] + 1);
      res = Math.min(res, dp[row + 1][col] + 1);

      if (str[row] === str[col]) {
        res = Math.min(res, dp[row + 1][col - 1]);
      }
    }
  }
  // 返回 0 - (字符串长度-1) 的范围就是整个字符串变成回文的最少添加情况
  const minNum = dp[0][row - 1];

  redo(str, minNum);

  // 根据 dp 表还原路径
  function redo(str, minNum) {
    const len = str.length + minNum;
    const arr = Array(len);
    let left = 0,
      right = len - 1,
      row = 0,
      col = str.length - 1;
    
    // 终止情况也可以是抵达对角线上时
    while (left < right) {
      let temp = dp[row][col];

      let left = dp[row][col - 1];
      let down = dp[row + 1][col];
      // 说明 [i,j] 范围内,是 [i,j-1] 构成回文,在最左边添加 j 位置字符构成回文的情况
      if (temp + 1 == left) {
        arr[left] = str[col];
        arr[right] = str[col];
        row = row;
        col = col - 1;
      }
      // 说明 [i,j] 范围内,是 [i+1,j] 构成回文,在最右边添加 i 位置字符构成回文的情况
      if (temp + 1 == down) {
        arr[left] = str[row];
        arr[right] = str[row];
        row = row + 1;
        col = col;
      }

      // 说明是 i、j 字符相等,[i+1,j-1] 构成回文的情况
      if (str[row + 1] === str[col - 1]) {
        arr[left] = str[row];
        arr[right] = str[row];
        row = row + 1;
        col = col - 1;
      }
      left++, right--;
    }
  }
}