动态规划-最长回文子串

218 阅读2分钟

最长回文子串

最长回文子串的问题描述:

给出一个字符串 S,求 S 的最长回文子串的长度

例子:

字符串 "PATZJUJZTACCBCC" 的最长回文子串为 "ATZJUJZTA",长度为 9

还是先看暴力解法,枚举子串的两个端点 i 和 j,需要判断在 [i, j] 内的子串是否回文,从复杂度上看,枚举端点需要 O(n2),判断回文需要 O(n),总复杂度为 O(n3)。

那我们介绍一下动态规划下时间复杂度为O(n2) 的一种解法。

dp[i][j] 为 S[i] 到 S[j] 是否为回文子串,是则为 1,否则为 0。这样根据 S[i] 是否等于 S[j] ,可以把情况分为两种:

  • 如果S[i] == s[j],那么只要 S[i+1] S[j-1]是回文子串,S[i]S[j] 就是回文子串;如果 S[i+1]至S[j-1] 不是回文子串,则 S[i]至S[j] 也不是回文子串
  • S[i] != S[j] ,那么 S[i]至S[j] 一定不是回文子串

由此写出状态转移方程:

if(S[i] == S[j]) dp[i][j] = dp[i+1][j-1]

if(S[i] != S[j]) dp[i][j] = 0

边界条件: dp[i][i] = 1, dp[i][i+1] = (S[i] == S[i+1]) ? 1 : 0

注意这个时候可能出现某些状态始终转移不到的情况,比如说我们 先固定 i, 然后 j 从 2 开始枚举

  • dp[0][2] => dp[1][1] 初始化过,能够求☑️
  • dp[0][3] => dp[1][2] 初始化过,能够求☑️
  • dp[0][4] => dp[1][3] 未初始化过,不能够求❌

而且不论怎么转移都转移不到这个状态,所以我们必须想出新的枚举方式:

  • 根据递推写法从边界出发的原理,注意到边界表示的是长度为 1 和 2 的子串,且每次转移时都对子串长度减 1,因此不妨按照子串的长度和初始位置进行枚举!
  • 也就是第一遍把所有长度为 3 的子串枚举出来,第二次枚举长度为 4 的子串 ……

先枚举长度 L (L可以取到字符串长度的),再枚举左端点 i,这样右端点 i + L -1 也能直接得到。

代码实现:

#include <cstdio>
#include <cstring>

const int maxn = 1010;
char S[maxn];
int dp[maxn][maxn];

int main()
{
  gets(S);
  int len = strlen(S), ans = 1;
  memset(dp, 0, sizeof(dp));
  // 边界
  for(int i = 0; i < len; i++) {
    dp[i][i] = 1;
    if(i < len - 1) {
      if(S[i] == S[i+1]) {
        dp[i][i+1] = 1;
        ans = 2;	// 初始化时注意当前最长回文子串长度
      }
    }
  }
  // 状态转移方程
  for(int L = 3; L <= len; L++) {	// 枚举长度
    for(int i = 0; i + L - 1 < len; i++) {	// 枚举起始端点
      int j = i + L -1;	// 右端点
      if(S[i] == S[j] && dp[i+1][j-1] == 1) {
        dp[i][j] = 1;
        ans = L;	// 更新
      }
    }
  }
  printf("%d\n", ans);
  return 0;
}