最长回文子串与马拉车算法

最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-medium/xvn3ke/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1.中心扩展

寻找回文子串必须要找到子串并判断他是否成中心对称的,要么检查每一个子串,从两侧向中间遍历检查对称。要么从中心向两侧边检查对称边取得子串。
第一种方法是暴力破解法,显然更耗资源一些,所以可以采用第二种中心扩展法。
注意:要考虑串的对称就要判断一个串的字符数是奇数还是偶数。

设置一个mid遍历整个字符串
设置一个left/right由mid开始向左右扩展比较元素相同否,相同急需扩展
1.使用mid遍历串
2.需要处理中心为偶数的情况,如bb abba,所以left与right在最开始时,需要都判断是否与mid相同,相同也进行移动
3.移动完后left/rigth开始左右扩展
4.每次扩展结束记录长度,并比较是否是最大长度。

 public String longestPalindrome(String s) {
        int mid = 0;
        int left = mid, right = mid;
        int maxLen = 0, maxLeft = 0;
        while(mid < s.length()){

            while(left > 0 && s.charAt(left - 1) == s.charAt(mid)){
                left--;
            }
            while(right < s.length() - 1 && s.charAt(right + 1) == s.charAt(mid)){
                right++;
            }
            while(left > 0 && right < s.length() - 1 && s.charAt(left - 1) == s.charAt(right + 1)){
                left--;
                right++;
            }
            if(maxLen < right - left + 1){
                maxLen = right - left + 1;
                maxLeft = left;
            
            }
            mid++;
            left = mid;
            right = mid;
        }
        return s.substring(maxLeft, maxLeft + maxLen);
    }

2.动态规划

如果思考过中心扩展的逻辑可以知道,上一层子串是回文aa,这一层子串才可能也是回文baab。反之则不会是。也就是说当前状态取决于上一层状态,那么这符合动态规划的核心:状态与状态转移。
1.确定dp数组与状态:dp[i][j]代表从i开始到j的子串是否为回文子串
2.确定状态转移方程:
dp[i][j] = dp[i+1][j-1] && s[i]=s[j]
dp[i][j] = true && i=j
dp[i][j] = dp[i+1][i+1] && s[i]=s[j] && i+1=j
3.无边界条件
4.若dp[i][j]为true考虑是否更新长度

class Solution {
    public String longestPalindrome(String s) {   
        boolean[][] dp = new boolean[s.length()][s.length()];
        int maxLen = 0;
        int begin = 0;
        for(int i = 0; i < s.length(); i++){
            for(int j = 0; j <= i; j++){
                if(i == j){
                    dp[j][i] = true;
                }else{
                    if(s.charAt(j) == s.charAt(i)){
                        if(j + 1 == i){
                            dp[j][i] = true;
                        }else{
                            dp[j][i] = dp[j + 1][i - 1];
                        }
                    }else{
                        continue;
                    }
                }
                if(dp[j][i]){
                    if(i - j + 1 > maxLen){
                        maxLen = i - j + 1;
                        begin = j;
                    }
                }
            }
        }
        return s.substring(begin, begin + maxLen);
    }
}

3.Manacher(马拉车)算法

马拉车算法有点类似KMP算法的思想:对已经比较过的部分不在重复比较,以实现优化。
KMP算法利用比较过后的前后缀结构是相同的进行回溯,来优化对相同结构的比较。对于回文子串来说,马拉车算法优化的是中心扩展方法,在中心扩展的时候,每个元素都要从中心出发向左右扩展。但是,在一个已经检查完的回文子串中,其左侧结构与右侧结构是对称的。若在左侧结构中存在一个元素以他为中心存在回文子串,则右侧结构中必然存在相应位置的元素以他为中心存在回文子串。我们通过对左侧结构中回文子串长度的分析来获得右侧回文子串的最小长度来减少比较次数完成优化。

分析右侧子串长度

1.我们认为已经检查完的回文子串右侧边界为maxRigth。当我们有元素在其右侧超过maxRight的位置出现时,我们无法在左侧找到对应元素。所以只能从该位置进行中心扩展
2.若当前元素在maxRight内,我们找到在左侧对应位置上的元素,再找到以其为中心的子串长度。若长度超过了其左侧结构的边界,那么意味着我们无法在右侧超过边界的区域为当前元素找到检查过的回文结构。所以我们设置当前元素的最小子串长度为maxRight到当前元素的距离,然后进行中心扩展
3.若当前元素在maxRight内,且其中心子串长度小于左侧结构边界。这意味着我们右侧结构中的中心子串和他是完全对称的。那么长度也是相同的,将当前元素长度设置为左侧对应元素的中心子串长度

算法准备

1.马拉车的核心思想容易理解,但是代码实现时的位置关系不好理解。首先我们为了规避奇数串和偶数串长度问题,需要在原串的元素间隔和串的两端加上特殊符号,一般为“#”。这样得到的新串必然为奇数串,方便寻找中心子串。
2.设置一个半径的概念,半径是中心子串由中心元素到左右两端的元素个数。
3.设置中心指针指向子串中心元素。
4.设置一个当前已经检查过的子串中,最右侧元素的下标指针。
5.设置一个数字记录每个元素的子串长度。
6.设置记录最大串的中心元素指针和最大串的半径。

算法实现

0.插入特殊符号构建新串
1.依次遍历串中元素。
2.执行分析右侧子串长度的逻辑。
3.检查当前获得的子串边界是否超过了当前最远的最右侧元素。若是,更新最右侧元素下标,子串中心下标。
4.检查当前获得的子串半径是否大于最大串的半径。
5.遍历结束
6.按最优结果截取子串返回结果。

剩余问题

如何进行分析子串长度已经交代过了,那如何构建新串和根据新串返回原串结果呢?
1.构建新串。分析#a#b#与#a#b#c#两种情况可知,特殊符号的下标永远是偶数。字符在原串中的位置要被特殊符号挤占,原串中前面有n个字符,新串前面就有n+1个特殊符号。所以遍历新串偶数为置‘#’,奇数位为当前下标/2取整得到的结果在原串中的元素。
2.返回结果。现在我们有了最优的子串的半径和中心下标,我们需要的是原串中的子串的起始位置下标和子串长度。
-起始位置:我们只需要用中心位置下标减去其右侧的元素个数就可以得到子串在新串的起始下标。右侧元素个数可以由半径-1得到(半径是中心到两端的元素个数,所以要减掉中心)。由于每一个元素之前都有一个特殊字符#。我们可以认为每一对"#字符"对应原串的一个元素。#a对应a,因此对下标0和1通过除2取整可以得到#a对应的原串下标0。因此,我们得到的子串在新串的起始下标除2取整就可以得到子串在原串的起始下标。
-长度:考虑#a#b#a#与#a#a#的半径分别是4与3。其元素个数分别为3与2。所以半径-1就是子串在原串中的元素个数。

代码如下:
public String longestPalindrome(String s) {
        if(s.length() <= 1){
            return s;
        }
        char[] newArray = new char[2 * s.length() + 1];
        for(int i = 0; i< newArray.length; i++){
            newArray[i] = i % 2 == 0 ? '#' : s.charAt(i / 2);
        }
        int maxCenter = 0, maxRight = 0, resR = 0, resCenter = 0;
        int[] maxR = new int[newArray.length];
        for(int i = 0; i < newArray.length; i++){
            if(i < maxRight){
                if(maxR[2 * maxCenter - i] < maxRight - i){ 
                    maxR[i] = maxR[2 * maxCenter - i];
                }else{
                    maxR[i] = maxRight - i;
                    while(i + maxR[i] < newArray.length && i - maxR[i] >= 0 && newArray[i + maxR[i]] == newArray[i - maxR[i]]){
                        maxR[i]++;
                    }
                }
            }else{
                maxR[i] = 1; //半径包含自己,所以初值为1
                while(i + maxR[i] < newArray.length && i - maxR[i] >= 0 && newArray[i + maxR[i]] == newArray[i - maxR[i]]){
                        maxR[i]++;
                    }
            }
            if(i + maxR[i] > maxRight){
                maxRight = i + maxR[i];
                maxCenter = i;
            }
            if(maxR[i] > resR){
                resR = maxR[i];
                resCenter = i;
            }
        }
        resR -= 1;
        int begin = (resCenter - resR) >> 1;
        return s.substring(begin, begin + resR);
    }
posted @ 2021-10-25 15:23  芝芝与梅梅  阅读(101)  评论(0)    收藏  举报