算法题目总结 -- 最长回文子串

题目描述

给定一个字符串,找出其中最长的回文子串

解题思路

暴力法

最容易想到的就是暴力破解,求出每一个子串,之后判断是不是回文,找到最长的那个

求每一个子串时间复杂度O(N^2), 判断子串是不是回文O(N),两者是相乘关系,所以时间复杂度为O(N^3)。

动态规划O(N ^ 2)

设状态dp[j][i]表示索引j到索引i的子串是否是回文串。则转移方程为:
在这里插入图片描述
则dp[j][i]为true时表示索引j到索引i形成的子串为回文子串,且子串起点索引为j,长度为i - j + 1。

算法时间复杂度为O(N ^ 2)。

中心扩展法O(N ^ 2)

假设一个字符串str是回文串,那么它一定是左右对称的,所以该字符串以某个字符str[mid]为中心的前缀和后缀一定是相同的。例如,对于回文串"aba",以’b’为中心,它的前缀和后缀都是’a’。故,可以枚举字符串的每个字符作为回文串的中心位置,左右扩展得到回文串,然后比较得到最长的长度。

注意:这里需要考虑到一个特殊情况,即存在"YXaaXY"这样长度为偶数的特殊子串,这里可以将"aa"视为’a’。
具体见代码:

public static String getLongestPhraseByExpMid(char[] s)
{
    int len = s.length;
    if (len == 1 || len == 0)
        return String.valueOf(s);

    int max_len = 1, start = -1;    // 最长回文string的起始位置,max_len是最长回文长度
    for (int i = 0; i < len; i++)
    {
        int l = i - 1, r = i + 1;       // left, right
        // 把一个字符作为中心
        while (l >= 0 && r <= len-1 && s[l] == s[r]){
            --l; ++r;
        }
        if (r-l-1 > max_len)
        {
            max_len = r-l-1;
            start = l + 1;
        }

        if (i != len-1 && s[i] == s[i+1])
        {
            l = i-1;
            r = i+2;
            // 两个相同字符作为中心
            while (l >= 0 && r <= len - 1 && s[l] == s[r]){
                --l; ++r;
            }
            if (r-l-1 > max_len)
            {
                max_len = r-l-1;
                start = l+1;
            }
        }
    }

    if(start == -1)     // 当没有回文子串的时候,随便返回一个就行,为保证不会越界,返回第一个就行
        return "" + s[0];

    return String.valueOf(s).substring(start, start+max_len);

}

Mancher法

首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。下面举一个例子:
在这里插入图片描述

Len数组简介与性质

Manacher算法用一个辅助数组Len[i]表示以字符T[i]为中心的最长回文字串的最右字符到T[i]的长度,比如以T[i]为中心的最长回文字串是T[l,r],那么Len[i]=r-i+1。

Len数组中表示的是字符串对应位置最长回文子串的半径。

对于上面的例子,可以得出Len[i]数组为:

在这里插入图片描述

Len数组的性质:Len[i]-1就是该回文子串在原字符串S中的长度。

证明:

字符串T由字符串S转换而来

=》在字符串T中,所有的回文子串都为奇数

=》在字符串T中,以T[i]为中心的最长回文子串,其长度为2*Len[i]-1

且 字符串T中所有回文子串中,分隔符#的数量一定比其他字符的数量多1

=》字符串T中所有回文子串中,分隔符#的数量为Len[i],其余Len[i]-1个字符来自原字符串S

=》在S中以T[i]为中心的回文子串长度为Len[i]-1

求解思路

我们再引入一个辅助变量MaxRight,表示当前访问到的所有回文子串,所能触及的最右一个字符的位置。另外还要记录下MaxRight对应的回文串的对称轴所在的位置,记为pos,它们的位置关系如下。
在这里插入图片描述

我们从左往右地访问字符串来求RL,假设当前访问到的位置为i,即要求RL[i],在对应上图,i必然是在po右边的(obviously)。但我们更关注的是,i是在MaxRight的左边还是右边。我们分情况来讨论。

当i在MaxRight的左边

情况1)可以用下图来刻画:
在这里插入图片描述

我们知道,图中两个红色块之间(包括红色块)的串是回文的;并且以i为对称轴的回文串,是与红色块间的回文串有所重叠的。我们找到i关于pos的对称位置j,这个j对应的RL[j]我们是已经算过的。根据回文串的对称性,以i为对称轴的回文串和以j为对称轴的回文串,有一部分是相同的。这里又有两种细分的情况。

以j为对称轴的回文串比较短,短到像下图这样。

在这里插入图片描述

这时我们知道RL[i]至少不会小于RL[j],并且已经知道了部分的以i为中心的回文串,于是可以令RL[i]=RL[j]。但是以i为对称轴的回文串可能实际上更长,因此我们试着以i为对称轴,继续往左右两边扩展,直到左右两边字符不同,或者到达边界。

以j为对称轴的回文串很长,这么长:

在这里插入图片描述

这时,我们只能确定,两条蓝线之间的部分(即不超过MaxRight的部分)是回文的,于是从这个长度开始,尝试以i为中心向左右两边扩展,,直到左右两边字符不同,或者到达边界。

不论以上哪种情况,之后都要尝试更新MaxRight和pos,因为有可能得到更大的MaxRight。

具体操作如下:

step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i)
            // 2 * postion - i => j
            // rad[postion]-i+postion => MaxRight - i

step 2: 以i为中心扩展回文串,直到左右两边字符不同,或者到达边界。
step 3: 更新MaxRight和pos

2)当i在MaxRight的右边
在这里插入图片描述
遇到这种情况,说明以i为对称轴的回文串还没有任何一个部分被访问过,于是只能从i的左右两边开始尝试扩展了,当左右两边字符不同,或者到达字符串边界时停止。然后更新MaxRight和pos。

public static int getLongestPhraseByManacher(String string){
    // 1.构造新的字符串
    // 为了避免奇数回文和偶数回文的不同处理问题,在原字符串中插入'#',将所有回文变成奇数回文
    StringBuilder newStr = new StringBuilder();
    newStr.append("#");
    for (int i=0;i<string.length();i++){
        newStr.append(string.charAt(i));
        newStr.append("#");
    }

    // rad[i]表示以i为中心的回文的最大半径,i至少为1,即该字符本身
    int [] rad = new int[newStr.length()];
    // maxRight表示已知的回文中,最右的边界的坐标
    int maxRight = -1;
    // postion表示已知的回文中,拥有最右边界的回文的中点坐标
    int postion = -1;

    // 2.计算所有的rad
    // 这个算法是O(n)的,因为right只会随着里层while的迭代而增长,不会减少。
    for (int i=0;i<newStr.length()-1;i++){
        // 2.1.确定一个最小的半径
        int r=1;
        if (i<=maxRight){
            // 2 * postion - i => j
            // rad[postion]-i+postion => MaxRight - i
            r = Math.min(rad[postion]-i+postion, rad[2 * postion - i]);
        }

        // 2.2.尝试更大的半径
        while (i-r >= 0 && i+r < newStr.length()
                && newStr.charAt(i-r) == newStr.charAt(i+r)){
            r++;
        }

        // 2.3.更新边界和回文中心坐标
        if (i + r - 1 > maxRight){
            maxRight = i + r - 1;
            postion = i;
        }

        rad[i] = r;
    }

    // 3.扫描一遍rad数组,找出最大的半径
    int maxLength = 0;
    for (int r : rad) {
        if (r > maxLength) {
            maxLength = r;
        }
    }
    return maxLength - 1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值