[算法入门笔记] 11. Manacher

提示:Manacher是解决最长回文子串问题的较优解~

[问题]
给定一个字符串str,返回str最长回文子串的长度

1. Manacher预处理

把每个字符开头结尾和中间插入一个特殊字符'#'来得到新的字符串数组

  • 对于奇数回文,不处理也能找到
  • 对于偶数回文,不处理是得不到的

2. 辅助变量含义

数组pArr

  • pArr[i]的意义是以i位置字符作为回文中心的情况下,扩出去得到的最大回文半径长度

  • 整个过程是从左到右遍历的过程中,一次计算每个位置的最大回文半径

对于"#c#a#b#a#c#"来说 pArr[0..10] [ 1 , 2 , 1 , 2 , 1 , 6 , 1 , 2 , 1 , 2 , 1 ] [1,2,1,2,1,6,1,2,1,2,1] [1,2,1,2,1,6,1,2,1,2,1]

整数pR

  • 表示之前遍历的所有字符的所有回文半径中,最右即将到达的位置

对于"#c#a#b#a#c#"来说 ,还没遍历前初始化为 − 1 -1 1

charArr[0]=='#'的回文半径是 1 1 1,所以目前回文半径只能扩到 0 0 0位置,回文半径最右即将到达的位置变为 1 1 1pR=1)

在这里插入图片描述

charArr[1]=='c'的回文半径是 2 2 2,此时所有的回文半径向右能扩到位置 2 2 2,所以回文半径最右即将到达的位置变为 3 3 3pR=3)

在这里插入图片描述

charArr[2]=='#'的回文半径是 1 1 1,位置 2 2 2向右只能扩到位置 2 2 2,回文半径最右即将到达的位置不变(pR=3)

在这里插入图片描述

charArr[3]=='a'的回文半径是 2 2 2,位置 3 3 3向右能扩展到位置 4 4 4,回文半径最右即将到达的位置变为 5 5 5pR=5)

在这里插入图片描述

charArr[4]=='#'的回文半径是 1 1 1,位置向右只能扩展到位置 4 4 4,回文半径最右即将到达位置不变(pR=5)

在这里插入图片描述

charAee[5]=='b'的回文半径是 6 6 6,所以位置 4 4 4向右能扩展到位置 10 10 10,回文半径最右即将到达的位置变为 11 11 11pR=11)

在这里插入图片描述

已经到达数组末尾,之后的pR不再变化

pR是遍历过的所有字符串中向右扩出来的最大右边界,只要右边界更往右,pR就更新

整数index

表示最近更新pR时,回文中心的位置

遍历到charArr[0]pR更新,index就更新为 0 0 0,…

3. 更新pArr

假设现在计算到i位置的字符charArr[i],在i之前位置的计算中,都会不断更新pRindex,即位置i之前的index这个回文中心扩出了一个目前最右的回文边界pR

  • 如果pR-1位置没有包住当前的i位置

在这里插入图片描述

比如计算到charArr[i]='c'时,pR=1,右边界在 1 1 1位置, 1 1 1位置是最右回文半径即将到达但还没有到达的位置,

当前pR-1没有包住当前i位置

从i字符开始,向左右两侧扩出去检查,此时扩的过程没有优化

  • 如果当前pR-1位置包住当前的i位置

在这里插入图片描述

比如计算到charArr[6..10]时,pR都为11,此时pR-1包住了位置6~10,这种情况下可以进行优化

在这里插入图片描述

位置i是计算回文半径(pArr[i])的位置。pR-1位置此时是包住位置i的,indexpR更新时的回文中心位置

回文半径数组pArr是从左到右计算的,位置i之前的所有位置都已经计算过回文半径

假设位置iindex为中心向左对称过去的位置记为i',位置i'的回文半径是计算过的,以i’为中心的最大回文串大小只有三种情况

  • 情况1

左小和右小完全在左大和右大内部,即以i’为中心的最大回文串完全在以index为中心的最大回文串的内部

在这里插入图片描述

a'是左小位置的前一个字符,b'右小位置的后一个字符,b右小'左小'a是以index为中心的对称字符

此情况下,以i为中心的最大回文串可以直接确定,即右小'左小'

  1. 左小右小是以index为中心,对应过去是右小'左小'这段,那么右半部分的[右小'左小']是左半部分[左小右小]的逆序
  2. 由于左半部分[左小右小]是回文串,那么右半部分的[右小'左小']也是回文串
  3. 以i’为中心的回文串是左半部分[左小右小],那么a'≠b',所以以i为中心的最大回文串是右半部分的[右小'左小']

在这里插入图片描述

  • 情况2

在这里插入图片描述

a左大的前一个字符,d右大的后一个字符

以i为中心的最大回文串是从右大’到右大

  1. 右大'右大左大左大'的逆序
  2. 左小右小是以i'为中心的回文串,那么左大左大'也是回文串,则右大’右大也是回文串
  3. a=bb=c左大右大没有扩,说明a!=d,所以c!=d,所以以i为中心的回文串就是右大'右大,而不会扩大

在这里插入图片描述

  • 情况3

在这里插入图片描述

左小左大是一个位置,即以i'为中心的最大回文串压在以index为中心的最大回文边界上

i为中心最大回文串是右大'右大,但可能扩充更大

  1. 右大‘右大这段是左小右小index为中心对称过去的,两段互为逆序,左小右小是回文串,所以右大'右大也是回文串

  2. 扩出去过程可以优化,但无法避免扩出去的检查

在这里插入图片描述

总结

在这里插入图片描述

4. 找最大回文半径

假设i位置的回文半径最大,pArr[i]=max,但max只是charArr的最大回文半径,还得对应原字符串,求出最大回文半径(其实就是max-1)

比如字符串”121“,处理成charArr后变成"#1#2#1#"。在charArr中位置3的回文半径最大,最大值为4,pArr[3]=4,对应原字符串的最大回文子串长度为4-1=3

5. 时间复杂度分析

时间复杂度关键在于扩出去的行为发生的数量,原字符串在处理后由N变为2N,从更新pArr来看,要么计算一个位置的回文半径完全不需要扩出去检查(比如情况1、2,可以直接获得位置i的回文半径长度),要么每次扩出去检查都会导致pR变量的更新(情况3,扩出去检查时会让回文半径到达更右的位置,会使pR更新)

pR最多从-1更新到2N(右边界),并且从来不减小,扩出去的检查是O(N级别)

因此Manacher时间复杂度为 O ( N ) O(N) O(N)

6. 伪代码实现

public static int[] manacher(String s) {
    //1221 -> #1#2#2#1
    s -> 处理串 str
    char[] str;
    int[] pArr = new int[str.length];
    int R = ?;
    int C = ?;
    for(int i = 0; i < str.length; i++) {
        if(i在R外部) {
                从i开始往两边暴力扩;R变大;
        } else {
            if(i' 的回文区域在彻底在L..R内) {
                pAAr[i] = 某个O(1)表达式;
            } else if(i'回文区域有部分在L..R外) {
                pArr[i] = 某个O(1)表达式;
            } else { //i' 回文区域和L..R左边界压线R之外的字符开始,往外扩,然后确定pArr[i]的答案;
                若第一步扩失败了,R不变,
                否则,R变大
            }
        }
    }
}

7. 算法实现

/**
 * 预处理字符串
 * @param str
 * @return
 */
public static char[] manacherString(String str) {
    char[] charArr = str.toCharArray();
    char[] res = new char[str.length() * 2 + 1];
    int index = 0;
    for (int i = 0; i != res.length; i++) {
        res[i] = (i & 1) == 0 ? '#' : charArr[index++];
    }
    return res;
}
public static int maxLcpsLength(String str) {
    if (str == null || str.length() == 0) {
        return 0;
    }
    //121-->#1#2#1#
    char[] charArr = manacherString(str);
    //回文半径数组
    int[] pArr = new int[charArr.length];
    //最近一次更新pR时回文中心的位置
    int index = -1;
    //之前遍历的所有字符的所有回文半径中,最右即将到达的位置
    int pR = -1;
    //扩出去的最大值
    int max = Integer.MIN_VALUE;
    //每个位置都求回文半径
    for (int i = 0; i != charArr.length; i++) {
        /**
         * pArr[i]表示以i位置上的字符,作为回文中心的情况下,扩出去的最大回文半径
         * pR>i 表示i在pR内部
         * i在pR外部 如果pR-1位置没有包住当前的i位置,自己和自己构成回文,返回1
         * i在pR内部
         * pR-i表示 i到pR的距离
         * 2 * index - i 表示i'的位置
         */
        pArr[i] = pR > i ? Math.min(pArr[2 * index - i], pR - i) : 1;
        /**
         * 1.扩
         * 2.三情况
         * 情况1、2不扩
         * 情况3 扩
         */

        //左边界和有边界有效情况下
        //i + pArr[i] 右大的右边一个
        //i - pArr[i] 左大的左边一个
        while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
            //右大的右边一个元素==左大的左边一个元素,扩半径
            if (charArr[i + pArr[i]] == charArr[i - pArr[i]])
                pArr[i]++;
            else {//否则不变
                break;
            }
        }
        if (i + pArr[i] > pR) {
            //更新pR(之前遍历的所有字符的所有回文半径中,最右即将到达的位置)
            pR = i + pArr[i];
            //更新回文中心
            index = i;
        }
        max = Math.max(max, pArr[i]);
    }
    return max - 1;
}

8. 进阶问题

给定一个str,想通过添加字符串的方式使得str整体变成回文字符串,要求只能str的末尾添加字符,返回str后面添加的最短字符串

在字符串的最后添加最少字符,使得整个字符串成为回文串,其实就是查找必须包含最后一个字符的情况下,最长的回文子串是什么,那么之前不是最长回文子串部分逆序后便是应该添加的部分

比如”abcd123321“,在必须包含最后一个字符的情况下,最长回文子串是”123321“,之前不是回文字串的部分是”abcd“,所以末尾添加”dbca“

算法思想

从左到右计算回文半径时关注回文半径最右即将到达的位置pR,一旦发现到达最后pR==charArr.length,说明必须包含最后一个字符的最长回文半径已找到,直接退出检查过程,返回添加的字符串即可

/**
 * 给定一个str,想通过添加字符串的方式使得str整体变成回文字符串,
 * 要求只能在str的末尾添加字符,返回str后面添加的最短字符串
 *
 * @param str
 * @return
 */
public static String shortestEnd(String str) {
    if (str == null || str.length() == 0) {
        return null;
    }
    //121-->#1#2#1#
    char[] charArr = manacherString(str);
    //回文半径数组
    int[] pArr = new int[charArr.length];
    //最近一次更新pR时回文中心的位置
    int index = -1;
    //之前遍历的所有字符的所有回文半径中,最右即将到达的位置
    int pR = -1;
    int maxContainsEnd = -1;
    for (int i = 0; i != charArr.length; i++) {
        //每个位置都求回文半径
        /**
         * pArr[i]表示以i位置上的字符,作为回文中心的情况下,扩出去的最大回文半径
         * pR>i 表示i在pR内部
         * i在pR外部 如果pR-1位置没有包住当前的i位置,自己和自己构成回文,返回1
         * i在pR内部
         * pR-i表示 i到pR的距离
         * 2 * index - i 表示i'的位置
         */
        pArr[i] = pR > i ? Math.min(pArr[2 * index - i], pR - i) : 1;
        /**
         * 1.扩
         * 2.三情况
         * 情况1、2不扩
         * 情况3 扩
         */

        //左边界和有边界有效情况下
        //i + pArr[i] 右大的右边一个
        //i - pArr[i] 左大的左边一个
        while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
            //右大的右边一个元素==左大的左边一个元素,扩半径
            if (charArr[i + pArr[i]] == charArr[i - pArr[i]])
                pArr[i]++;
            else {//否则不变
                break;
            }
        }
        if (i + pArr[i] > pR) {
            //更新pR(之前遍历的所有字符的所有回文半径中,最右即将到达的位置)
            pR = i + pArr[i];
            //更新回文中心
            index = i;
        }
        if (pR == charArr.length) {
            maxContainsEnd = pArr[i];
            break;
        }
    }
    char[] ans = new char[str.length() - maxContainsEnd + 1];
    for (int i = 0; i < ans.length; i++) {
        ans[ans.length - 1 - i] = charArr[2 * i + 1];
    }
    return String.valueOf(ans);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan Chau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值