Leetcode5.最长回文子串 - 三种方法

Leetcode5.最长回文子串 - 三种方法_第1张图片

5.最长回文子串 - 三种方法

    • 食用指南:
    • 题目描述:
    • 题目分析:
    • 算法模板:
    • 代码实现:
      • 法一:暴力双指针32ms
      • 法二:动态规划636ms
      • 法三:马拉车算法688ms
    • 注意点:

食用指南:

Leetcode专栏开启了,由于博主闭关期末,所以每日只能一题
尽量做到一题多解,先说思路,之后代码实现,会添加必要注释
语法或STL内容会在注意点中点出,新手友好
欢迎关注博主神机百炼专栏,内涵算法基础详细讲解和代码模板

题目描述:

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

    1 <= s.length <= 1000
    s 仅由数字和英文字母组成

  • 代码背景

class Solution {
public:
    string longestPalindrome(string s) {

    }
};
  • 题目来源:https://leetcode.cn/problems/longest-palindromic-substring/

题目分析:

  • 字符串长度才1000,时间复杂度在O(n2)左右都能通过

  • 法一:暴力双指针

    枚举每个字符作为回文串的中间字符

    中间字符分为两种情况:

    1. 奇数长度的回文串,中间字符无对称元素
    2. 偶数长度的回文串,中间字符和下一字符对称

    假设字符长度为n,则枚举n轮
    每轮奇数长度回文串最多枚举n/2种子串
    每轮偶数长度回文串也最多枚举n/2种子串
    最坏时间复杂度O(n2),106平稳度过

  • 法二:动态规划

    填表:枚举起点arr[i],枚举终点arr[i][j]
    dp填表都是自下而上

    arr[i][j]子情况有两种:

    1. s[i] == s[j],则arr[i][j]是否回文需要看上一层arr[i+1][j-1]
      但当arr[i][j]所表示的串长度为2时,
      arr[1][2] -> arr[2][1],陷入死锁。
      但是既然s[i] == s[j],那么i~j必然是回文
    2. s[i] != s[j],则arr[i][j]必然不是回文串

    dp表的基石:所有单个字符都回文,arr[i][i] = 1;

    时间复杂度:起点n个,终点n个,填表O(n2),查表找最长O(n2)

    可否压缩?不可
    找最长子串需要遍历所有arr[i][j],保证arr[i][j] == 1 同时找最大j-i+1

  • 法三:马拉车算法(manacher)

    不论回文串长度为奇数/偶数,插空补#号或其他非字符串内符号,
    奇数长度则有偶数个空,偶数长度则有奇数个空
    则最终连带#得到的回文串都是奇数长

    字符串哈希O(n)完成打表,之后O(1)判断两串是否相等

    枚举每一点作为奇数长回文串中间字,O(n)

    二分查找该点为奇数长回文串中间字时回文串的最大长度,最差O(log(n/2))

    总时间复杂度:O(2*n + n + nlog(n/2)) = O(n(3+log(n/2)))
    最高可以解决串长n = 106

算法模板:

  • 法一:
    双指针
    单调双指针
  • 法二:
    一维前缀和
    二维前缀和
    字符串前缀哈希
    整数二分

代码实现:

法一:暴力双指针32ms

class Solution {
public:
    string longestPalindrome(string s) {
        string res;
        res += s[0];
        int maxx = 0;
        int len = s.size();
        for(int k = 0; k<len; k++){
            int i = k-1, j = k+1;
            while(i>=0 && j<len && s[i] == s[j]){
                if (j-i+1 > maxx){
                    maxx = max(maxx, j-i+1);
                    res = s.substr(i, j-i+1);
                }
                i--, j++;
            }
            i = k, j = k+1;
            while(i>=0 && j<len && s[i] == s[j]){
                if (j-i+1 > maxx){
                    maxx = max(maxx, j-i+1);
                    res = s.substr(i, j-i+1);
                }
                i--, j++;
            }
        }
        return res;
    }
};

法二:动态规划636ms

  • 数组越界问题很头疼
    看了一眼答案发现只要枚举长度,右端点越界时就break
    这个写法不用考虑枚举边界,好方法!Get it.
class Solution {
public:
    string longestPalindrome(string s) {
        int len = s.size();
        vector<vector<int>> dp(len, vector<int>(len));
        for (int i = 0; i < len; i++) dp[i][i] = 1;
        int maxx = 0;
        string res;
        res += s[0];	//至少单个字符是回文子串
        for (int L = 2; L <= len; L++) {
            for (int i = 0; i < len; i++) {
                int j = L + i - 1;
                if (j >= len) break;

                if (s[i] != s[j])    dp[i][j] = false;
                else if (L == 2) dp[i][j] = true;
                else dp[i][j] = dp[i + 1][j - 1];

                if (dp[i][j] && L > maxx) {
                    maxx = L;
                    res = s.substr(i, L);
                }
            }
        }
        return res;
    }
};
  • int **的二维数组写法:
int ** dp = new int*[len];
for(int i=0; i<len; i++){
	dp[i] = new int[len];
}
for(int i=0; i<len; i++){
	for(int j=0; j<len; j++){
		if (i == j)	dp[i][j] = 1;
		else dp[i][j] = 0;
	}
}

法三:马拉车算法688ms

  • manacher算法应用范围非常窄,目前我见过只有最长回文子串可以用
  • 但是字符串哈希是完全可以替代KMP的存在,应用范围非常广
class Solution {
public:
    typedef unsigned long long ULL;
    static const int N = 2e6 + 1;
    const int p = 131;
    ULL P[N], Hash1[N], Hash2[N];
    char ss[N];
    ULL get(ULL h[] , int l , int r){
        return h[r] - h[l - 1] * P[r-l+1];
    }
    string longestPalindrome(string s) {
        P[0] = 1;
        int len = s.size();
        int k = 1;
        //1,奇偶统一
        for(int i=0; i<len; i++){
            ss[k++] = s[i];
            if (k/2 < len)  //奇数位是原串,偶数位是补空,补的空数永远比位数少1
            ss[k++] = '#';
        }
        k--;
        //2.打表前缀和 & 打表对称后缀和
        for(int i=1,j=k; i<=k; i++,j--){
            P[i] = P[i-1]*p;
            Hash1[i] = Hash1[i-1]*p + ss[i];
            Hash2[i] = Hash2[i-1]*p + ss[j];
        }
        //3.枚举中点,寻找中点的最长回文
        int maxx = 0;
        string tmp;
        for(int i=1; i<=k; i++){
            //以中点为圆心,半径最小0,最大为1~该点前/该点后~k。二分找最大合适半径
            int l = 0, r = min(k - i , i - 1);
            while(l < r)
            {
                int mid = (l + r + 1) >> 1;
                if(get(Hash1, i - mid, i - 1) == get(Hash2, k + 1 - (i + mid), k + 1 - (i + 1)))  l = mid;
                else r = mid - 1;
            }
            
            if (ss[i-l] == '#'){        //比较时带有=号,因为可能最终结果是单个字母,则半径为0 == 初始maxx
                if(maxx <= r){          //#结尾,则最终有半径个字母
                    maxx = r;
                    tmp.erase(0);
                    for(int j=i-r; j<=i+r; j++) tmp+=ss[j];
                }
            }else{
                if (maxx <= r + 1){
                    maxx = r + 1;       //字母结尾,则最终有半径+1个字母
                    tmp.erase(0);
                    for(int j=i-r; j<=i+r; j++) tmp+=ss[j];
                }
            }
            
        }
        string res;
        for(int i=0; i<tmp.size(); i++)
            if(tmp[i] != '#')
                res.push_back(tmp[i]);
        return res;
    }
};

注意点:

  • 二维向量写法:
vector<vector<int>> dp(n, vector<int>(n));
//默认初始化为0
  • 后缀和数组:

    由于前缀和数组已有固定求区间和公式:
    Leetcode5.最长回文子串 - 三种方法_第2张图片
    距左边远节点 - 距左边近-1节点*P【边长】= 区间和

    后缀和数组想要直接套用该公式是不行的
    因为后缀和数组距离左边近的数值大,远的数值小

    所以现将后缀和数组表示对称过来,
    原本距离右边为1即H[len-1],现表达为距离左边1即H[2]
    所以Hash2[i]表达的串是 len+1-i 到 len的子串
    于是又满足了i大的距离左边界远,值也大,可以直接套用区间和公式

你可能感兴趣的:(Leetcode一题多解,算法,leetcode,数据结构,哈希算法,动态规划)