5.最长回文子串

题目地址: https://leetcode.cn/problems/longest-palindromic-substring/

解法1 - 中心枚举法

思路

有两种回文串

  • 长度为偶数:若中心点坐标为i,左右两坐标起点是i - 1, j + 1

  • 长度为奇数:若中心点坐标为i,左右两坐标起点是i, j + 1

将字符串中的每个字符视为中心,尝试向两边延伸,最后枚举完所有字符后,结果就是最长字符

枚举到左右两边字符不相等,此时s[l + 1] ~ s[r - 1]就是最长回文串

时间复杂度

假定每个字母都向左右试探n步

0号字母走0步,
1号走1步,
2号走两步
...
n/2走n/2步
...
n-2走2步
n-1走1步
n走0步

\(0+1+2+...+\frac{n}{2} + ... + 2 + 1 + 0\)

根据等差数列公式

$ = \frac{n^2}{4}$

所以是\(N^2\)级别的

代码

//substr的两个参数分别是(起始位置,字符串长度)
class Solution {
public:
    string longestPalindrome(string s) {
        string ans;
        for(int i = 0; i < s.size(); i ++) {
          	// 枚举奇数长度回文串
            int l = i - 1, r = i + 1;
            while(l >= 0 && r < s.size() && s[l] == s[r]) --l, ++r;
            if(ans.size() < r - l - 1) ans = s.substr(l + 1, r - l - 1);
            
          	// 枚举偶数长度回文串
            l = i, r = i + 1;
            while(l >= 0 && r < s.size() && s[l] == s[r]) --l, ++r;
            if(ans.size() < r - l - 1) ans = s.substr(l + 1, r - l - 1);
        }
        return ans;
    }
};

思路2 二分 + Hash

思路

对字符串正序和逆序分别求Hash值,

  • 枚举每个字母作为中点,

  • 二分测出半径

    如果两侧字符串Hash值相等,那就可以继续延长,如果不等,那就缩短

其他需要注意的点

  1. 当回文串长度为奇数和偶数时,枚举坐标不同,为了方便处理,在每两个字符串之间添加一个#,这样偶数长度回文串也会变成奇数,比如 abba --> a#b#b#a,假设原长度是x,那么添加的#的长度就是x + 1,两者之和等于\(2x + 1\),必然是奇数

  2. 后面求出回文串长度后,如何确定原字符串长度?

    可查看某一端点的字符是#还是字符,如果是#,那就是#比字符多一个,假设半径是mid,总长度就是mid

    比如#a#b#a#, mid = 3,字符总数就是3

    如果端点值是字符,那就是字符比#多一个,假设半径是mid,总长度就是mid + 1

    比如a#b#a, mid = 2,字符总数就是2 + 1 = 3

  3. 字符串相反, 位置对应问题

    0 1 2 3 4 5 6 7
    0 7 6 5 4 3 2 1
    n = 7, 假设此时i = 4, mid = 2
    则应该对比[2,3] 和 [5,6]
    [2,3]的坐标就是[i-mid, i-1]
    [5,6]的坐标在反方向是[2,3],即[n-i-(mid-1), n-i]
    

时间复杂度

枚举n个字母 n,假定每个字母左右都有n个字母,每次耗时\(O(\log^N)\),总的就是\(O(N\log^N)\)级别的

代码

typedef unsigned long long ULL;
class Solution {
public:
    static const int N = 2010;
    char str[N];
    int base = 131;
    ULL p[N], hl[N], hr[N];
    ULL get(ULL h[], int l, int r)
    {
        return h[r] - h[l - 1] * p[r - l + 1];
    }

    string longestPalindrome(string s) {
        strcpy(str + 1, s.c_str());
        int n = s.size() * 2;
        for(int i = n; i >= 1; i-=2)
        {
            str[i] = str[i/2];
            str[i - 1] = 'z' + 1;
        }

        p[0] = 1;
        for(int i = 1, j = n; i <= n; i++, j--)
        {
            p[i] = p[i - 1] * base;
            hl[i] = hl[i - 1] * base + str[i];
            hr[i] = hr[i - 1] * base + str[j];
        }
        
        int st = 0, radius = 0;
        for(int i = 1; i <= n; i++)
        {
            int l = 0, r = min(i - 1, n - i);
            while(l < r)
            {
                int mid = (l + r + 1) >> 1;
                if(get(hl, i - mid, i - 1) == get(hr, n - i - mid + 1, n - i)) l = mid;
                else r = mid - 1;
            }
            if(l >= radius)
            {
                st = i;
                radius = l;
            }
        }
        string ans;
        for(int i = st - radius; i <= st + radius; i++)
            if(str[i] <= 'z') ans += str[i];
        return ans;
    }
};

Q&A

1. 二分必须使用

int mid = (l + r + 1) >> 1;
if(get(hl, i - mid, i - 1) == get(hr, n - i - mid + 1, n - i)) 
  l = mid;
else 
  r = mid - 1;

不能使用

int mid = (l + r) >> 1;
if(get(hl, i - mid, i - 1) != get(hr, n - i - mid + 1, n - i)) 
  r = mid;
else 
  l = mid + 1;

以cbabb为例,在加上#字符后变为#c#b#a#b#b,当i = 5,即字符a时,二分的结果错误

原因是第二种二分,在等号成立时,只能保证这个长度的字符串相等, 而l = mid+1,扩大了相等的范围,把不相等的字符包括进来了

posted @ 2022-11-08 11:56  INnoVation-V2  阅读(14)  评论(0编辑  收藏  举报