当前位置:   article > 正文

动态规划——最长回文子串_最长回文子串动态规划

最长回文子串动态规划

题目描述

给你一个字符串 s,找到 s 中最长的回文子串

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串

示例 1:

 输入:s = "babad"
 输出:"bab"
 解释:"aba" 同样是符合题意的答案。

示例 2:

 输入:s = "cbbd"
 输出:"bb"

解题方法

1、动态规划算法

解题思路

(1)考虑回文串的性质:若一个子串是回文串并且长度大于2,则将其首尾两个字母去除后,该子串仍然是一个回文串,反过来思考,若一个子串是回文串,在其首尾分别添加一个字母,若字母相同,则形成的子串也是回文串。

(2)状态表示:回文串有三个重要的性质:起始字母下标终止字母下标以及长度,其中长度可以由前两个性质推导得出,则使用二维数组dp(i,j)表示,dp(i,j)的值表示的也不再是区间[i,j]的最大回文子串的长度了,而是表示区间 [i,j]是否是回文串

若 dp(i,j)=true,表示Si....Sj是一个回文串,反之则不是一个回文串。

(3)初始化:每个字符本身就是一个回文串

  1. for (int i = 0; i < n; i++)
  2.  {
  3.      dp[i][i] = true;
  4.  }

(4)状态转移:由(1)可得,当j-i>=3时,dp(i,j)的值由两个因素决定:s[i]与s[j]的值dp(i+1,j-1),并且仅当以下两个条件同时成立时,dp(i,j)为true

s[i] == s[j]

dp[i+1][j-1] == true

当j-i<3时,存在两种情况:

①j-i=1,则dp(i,j)一定为true(a、b);

②j-i=2,若此时s[i]==s[j],则dp(i,j)为true(ab、aa);

其中第二种情况可以归纳到一般情况的计算中,只需要添加一个条件判断即可。

因为需要使用到dp(i+1,j-1)的值,递推时从长度较小的字符串向长度较长的字符串进行转移(即先计算较小的区间,再计算较大的区间)。

(5)代码流程:当区间大小为1时(即i==j),一定是回文串,因此只需要讨论区间长度在[2,n]之间的情况。外层循环表示每次判断的区间大小L内层循环固定右边界(即i的大小),j的值可以由L和i推导得到(j=L+i-1),因此不需要再加一层循环。同时需要注意左边界越界问题(j>=n),一旦越界就可以退出当前循环,右边界不存在越界问题。

在循环中,一旦s[i]!=s[j]则表示区间[i,j]不可能形成回文串,置dp(i,j)=false;若s[i]==s[j],当L<=3时不需要判断dp(i+1,j-1)的情况,直接置dp(i,j)=true,当L>3时,需要判断dp(i+1,j-1),当且仅当dp(i+1,j-1)=true时,置dp(i,j)= true

代码实现
  1. class Solution {
  2. public:
  3. string longestPalindrome(string s) {
  4. int n = s.size();
  5. // 特殊情况,直接返回字符串即可
  6. if (n < 2) {
  7. return s;
  8. }
  9. int maxLen = 1;
  10. int begin = 0;
  11. // dp[i][j] 表示 s[i..j] 是否是回文串
  12. vector<vector<int>> dp(n, vector<int>(n));
  13. // 初始化:所有长度为 1 的子串都是回文串
  14. for (int i = 0; i < n; i++) {
  15. dp[i][i] = true;
  16. }
  17. // 递推开始
  18. // 先枚举子串长度
  19. for (int L = 2; L <= n; L++) {
  20. // 枚举左边界,左边界的上限设置可以宽松一些
  21. for (int i = 0; i < n; i++) {
  22. // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
  23. int j = L + i - 1;
  24. // 如果右边界越界,就可以退出当前循环
  25. if (j >= n) {
  26. break;
  27. }
  28. if (s[i] != s[j]) {
  29. dp[i][j] = false;
  30. } else {
  31. // 处理当j-i==2时的特殊情况
  32. if (j - i < 3) {
  33. dp[i][j] = true;
  34. } else {
  35. dp[i][j] = dp[i + 1][j - 1];
  36. }
  37. }
  38. // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文
  39. // 此时记录回文长度和起始位置
  40. if (dp[i][j] && j - i + 1 > maxLen) {
  41. maxLen = j - i + 1;
  42. begin = i;
  43. }
  44. }
  45. }
  46. return s.substr(begin, maxLen);
  47. }
  48. };
复杂度分析

(1)时间复杂度

外层循环n次,内层循环n次,则时间复杂度为O(n^2)

(2)空间复杂度

动态规划数组存储空间为nxn,则空间复杂度为O(n^2)

2、中心扩展算法

解题思路

(1)在动态规划方法中,每一次判断s[i]==s[j]之后,我们都会再判断dp(i+1,j-1)的情况。事实上可以考虑以回文子串为扩展中心,向(i-1,j+1)方向扩展,若s[i-1]==s[j+1]则可以扩展回文子串的长度,一旦不相等停止继续扩展回文子串。

(2)代码流程:先枚举所有回文中心,再尝试向左向右同时进行扩展,最后求出所有扩展回文串的最大值得到最大回文串长度。

注意:因为奇数长度的回文串和偶数长度的回文串结构不太一样,偶数长度的回文串没有中心字符,因此枚举时需要以长度为1的子串作为回文中心(扩展得到奇数长度的回文串)也需要以长度为2的子串作为回文中心(扩展得到偶数长度的回文串)。

(3)标准模板库——pair

作用:

①pair是将2个数据组合成一组数据,当需要这样的需求时就可以使用pair,如stl中的map就是将key和value放在一起来保存。

pair<string, string> anon;

②另一个应用是,当一个函数需要**返回2个数据**的时候,可以选择pair。

  1. pair<int, int> expandAroundCenter(const string& s, int left, int right)
  2. {
  3. // 省略
  4. return {left + 1, right - 1};
  5. }

性质:

①pair中的两个数据可以具有不同的数据类型

pair<int, double> p1(1, 1.2);

②两个数据可以分别用pair的两个公有函数first和second访问

  1. pair<int ,double> p1;
  2. p1.first = 1;
  3. p1.second = 2.5;
  4. cout<<p1.first<<' '<<p1.second<<endl;
代码实现
  1. class Solution {
  2. public:
  3. pair<int, int> expandAroundCenter(const string& s, int left, int right) {
  4. while (left >= 0 && right < s.size() && s[left] == s[right]) {
  5. --left;
  6. ++right;
  7. }
  8. // 返回满足要求的回文串起始索引
  9. return {left + 1, right - 1};
  10. }
  11. string longestPalindrome(string s) {
  12. // 使用变量start和end跟踪最大扩展回文子串
  13. int start = 0, end = 0;
  14. // 枚举所有可能的长度为1或2的回文中心
  15. for (int i = 0; i < s.size(); ++i) {
  16. // 以一个字符作为回文中心进行扩展
  17. auto [left1, right1] = expandAroundCenter(s, i, i);
  18. // 以两个字符作为回文中心进行扩展
  19. auto [left2, right2] = expandAroundCenter(s, i, i + 1);
  20. // 更新记录
  21. if (right1 - left1 > end - start) {
  22. start = left1;
  23. end = right1;
  24. }
  25. if (right2 - left2 > end - start) {
  26. start = left2;
  27. end = right2;
  28. }
  29. }
  30. return s.substr(start, end - start + 1);
  31. }
  32. };
复杂度分析

(1)时间复杂度

回文中心共有n个,每个回文中心最多向外扩展n次,则时间复杂度为O(n^2)

(2)空间复杂度

使用了变量start、end作为跟踪变量(常数),则空间复杂度为O(n)

3、Manacher算法

  1. class Solution {
  2. public:
  3. int expand(const string& s, int left, int right) {
  4. while (left >= 0 && right < s.size() && s[left] == s[right]) {
  5. --left;
  6. ++right;
  7. }
  8. return (right - left - 2) / 2;
  9. }
  10. string longestPalindrome(string s) {
  11. int start = 0, end = -1;
  12. string t = "#";
  13. for (char c: s) {
  14. t += c;
  15. t += '#';
  16. }
  17. t += '#';
  18. s = t;
  19. vector<int> arm_len;
  20. int right = -1, j = -1;
  21. for (int i = 0; i < s.size(); ++i) {
  22. int cur_arm_len;
  23. if (right >= i) {
  24. int i_sym = j * 2 - i;
  25. int min_arm_len = min(arm_len[i_sym], right - i);
  26. cur_arm_len = expand(s, i - min_arm_len, i + min_arm_len);
  27. } else {
  28. cur_arm_len = expand(s, i, i);
  29. }
  30. arm_len.push_back(cur_arm_len);
  31. if (i + cur_arm_len > right) {
  32. j = i;
  33. right = i + cur_arm_len;
  34. }
  35. if (cur_arm_len * 2 + 1 > end - start) {
  36. start = i - cur_arm_len;
  37. end = i + cur_arm_len;
  38. }
  39. }
  40. string ans;
  41. for (int i = start; i <= end; ++i) {
  42. if (s[i] != '#') {
  43. ans += s[i];
  44. }
  45. }
  46. return ans;
  47. }
  48. };

(1)时间复杂度:O(n)

(2)空间复杂度:O(n)

该算法时间复杂度和空间复杂度均较低,但是算法复杂,不在掌握范围内。

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号