Manacher算法详解

Manacher算法

马拉车算法解决最长回文字串的问题

时间复杂度:O(n)

传统思想:以一个字符为中心,向两边扩大,就能返回最长回文字串,但是长度为偶数的字符串是不成立的,需要分情况讨论,而且算法时间复杂度 o(n^2) ,也可以使用动态规划,时间复杂度仍为o(n^2)

解决办法:在传统方法的基础上将每个字符两边添加字符 # ,然后再向两边扩展,这样奇数和偶数的回文串都找到了!

问题:添加的字符是不是要求字符串中没出现的字符?

  • 不是,随便添加,因为无论什么时候都是实际上有的的和有的比较,新添加的和新添加的比较

Manacher算法实现

讲解:

  1. 首先沿用了暴力解法的中心扩展思想,从第一个字符向后遍历,为了解决偶数字符串问题,在每个字符周围插入字符 ‘#’,例如字符串“ababa”变为“#a#b#a#b#a#”,新字符串长度变为``2 * str.length() + 1`

  2. 算法的优点在于对向后遍历过程中优化了回文串判断的逻辑,不同于暴力算法,每个字符都从该字符开始从中心扩散,而Manacher算法类似于KMP将之前比较过的结果利用起来了,避免了反复的重复判断

  3. 算法核心讲解:

    1. 定义变量:R:最右回文右边界,C:中心点位置,arr[str.length]:用来保存每个位置为中心的回文半径(从中心点开始向边缘计数)长度
    2. R的意思是在整个字符串的遍历过程中保存每次i位置为中心的回文串的最右侧位置构成一个回文区间,在区间内的字符可以通过回文区间的特性,缩减判断步骤,甚至直接不用判断
    3. C的意思是,R为右边界的回文串的中心位置,左边则对应L
    4. 每次判断i位置的字符的回文串的时候都保留一下到达的最右侧位置,把最右侧位置给R,这样就能不停的扩展这个回文串,更新会文串时要同时更新中心点C
    5. 大概的图解
    6. 一共有四种情况,分为两大种
    7. (1)、当前遍历的i位置的字符不在回文区间中,那么这种情况就不会右优化的空间,之间按照暴力方法向后遍历
    8. (2)、当前遍历的i位置的字符回文区间中(三种情况)
      1. 以i为中心的回文串(L....iil....ii....iir....c....il....i....ir....R)整个都在R以内,根据回文特性,i位置的回文串的长度和ii位置是完全相同的。所以arr[i] == arr[ii],i和ii是关于对称的,所以ii = 2 * c - i,所以arr[i] = arr[2 * c - i],这种情况完全不需要判断
      2. 以i为中心的回文串一部分在LR这个区间之外(iil..L..ii..iir..c..il..i..R..ir)那么就认为i到R这之间是不需要判断的即R - i这个长度是一定是回文的,但是R之后是否是回文还是需要判断的,同时伴随着判断结束的R的更新,一旦R右侧仍有字符构成回文,那么,R和C将同时更新
      3. 以i为中心的回文串压了LR的线。这种情况就是arr[i] = arr[ii] = R - i
    9. 至此4种情况梳理结束,循环结束后arr数组中最大值 - 1 就是最长回文子串长度(因为字符串是被我加过#字符扩展的,新字符串长度为2 * len + 1,而arr[i]是最大回文半径长度,减一以后刚好是原串中最大回文字串的长度例如“#a#b#a#”,arr[i]最大为4,串长为7,实际上串长为3,最大回文字串长度为3)
  4. 代码实现

    package Manacher;
    
    
    /**
     * @author 孙东宇
     * 创建时间:2022/04/17
     * 介绍:Manacher算法,解决回文串问题
     */
    public class Manacher {
        public static void main(String[] args) {
            String s = "babab";
            manacher(s);
        }
    
        public static int manacher(String s) {
            if (s == null || s.length() == 0) {
                return 0;
            }
            // 将 字符串添加‘#’字符
            char[] str = manacherString(s);
            // 定义回文"半径"数组
            int[] arr = new int[str.length];
            // 回文串中心
            int c = -1;
            // 回文右边界再往右一个位置(为了方便实现代码)
            int r = -1;
            // 扩展字符串的最大回文字串长度
            int max = Integer.MIN_VALUE;
            // 对每个位置求回文半径,原理和暴力方法相同都是以i为中心
            // 向两边扩展
            for (int i = 0; i < arr.length; i++) {
                // 先确定每个位置最小确定的不用判断的区域,先给arr[i]赋值,减少判断量
                // 解读:
                // 1. r > i  是判断i是否再r之内,如果不在就是第一种情况
                // 2.第二种情况右3种小情况,
                // 2.1,i所在的区域都在r之内,那么就是arr[2 * r - c]对称过去的i
                // 2.2,如果有的部分不在,那么就是 r - i 为回文半径长度(但是需要判断是否需要扩大)
                // 2.3,如果压线,那么arr[2 * r - C] 和 r - i 是相等的
                // 所有情况都是围绕arr[2 * i - c] 和 r - i 所以总结起来就是在两个之间取最小值
                arr[i] = r > i ? Math.max(r - i, arr[2 * i - c]) : 1;
                // 将 r 向两边扩展
                // 虽然只有 2.3 这一种情况需要将r向两边扩展更新,但是为了缩减代码长度,将所有都进行判断扩展
                // 不能扩展到数组越界
                while (i + arr[i] < str.length && i - arr[i] > -1) {
                    if (str[i - arr[i]] == str[i + arr[i]]) {
                        arr[i]++;
                    } else {
                        break;
                    }
                }
                //更新右边界R
                if (i + arr[i] > r) {
                    // r的定义、最右侧再往右一个位置
                    r = 1 + arr[i];
                    // 更新中心点
                    c = i;
                }
                max = Math.max(max, arr[i]);
            }
            return max - 1;
        }
    
        // 插入字符‘#’
        private static char[] manacherString(String s) {
            char[] charArr = s.toCharArray();
            char[] res = new char[charArr.length * 2 + 1];
            int index = 0;
            for (int i = 0; i < res.length; i++) {
                res[i] = (i & 1) == 0 ? '#' : charArr[index++];
            }
            return res;
        }
    }
    
posted @ 2022-04-17 21:32  ふじさんのゆき  阅读(49)  评论(0编辑  收藏  举报