Manacher算法

Manacher算法

问题场景

假设字符串str长度为N,想返回最长回文子串的长度

回文即是指字符串具有对称性,从头到尾遍历和从尾到头遍历结果相同 如“123321” 或 “abc1cba”就是典型的回文子串

解决思路

前置知识

暴力解决的方式,就是从字符串str的第一个元素开始,向两边扩张,到第i个元素时,如果str[i-1] == str[i+1] 那么说明符合回文条件,可以继续扩张。这么做的问题是,可以解决“12321”这种字符串,但是遇到“123321”这种中间是两个数的就会出问题。

为了解决上述问题,初步的解决方式是在元素的空档加入辅助元素,例如可以这样来处理,将原有的字符串"123321"变为"#1#2#3#3#2#1#"

//"123321" -> "#1#2#3#3#2#1#"
public static char[] manacherString(String str) {
   char[] charArr = str.toCharArray();//字符串变字符数组
   char[] res = new char[str.length() * 2 + 1];//定义新字符串的字符数组 长度为2N+1
   int index = 0;
   for (int i = 0; i != res.length; i++) {//移动新数组下标
      //新数组中插元素 下标奇数插'#',下标偶数插原有数组元素并移动原有数组下标
      res[i] = (i & 1) == 0 ? '#' : charArr[index++];
   }
   return res;
}

这样通过之前的扩张思路处理manacherString方法得到的新数组,将会得到包括"#"在内的最大回文子串,时间复杂度为O(N2)(因为最坏的情况每个元素都需要扩张到最左边或者最右边,最终会得到一个等差数列的求和,复杂度为N2)

Manacher算法的引出

在之前暴力解决的过程中,其实不难发现,在比较过程中,有一些操作是重复进行的,比如在字符串"aba23232aba"中共有11个元素,其中下标为1的“b”和下标为9的“b"的最大回文子串长度是一样的,但是暴力解决时会不分青红皂白开始扩张,显然在浪费资源,那么有更好的方法吗?

几个概念

回文半径 :”123321“ 回文半径为3 ”12321“ 回文半径为3

​ ( 看一眼就懂的概念,感觉比文字介绍好很多)

回文半径数组pArr就是把每个元素的回文半径存到数组里

回文最右边界R,中心点C比如经扩张处理后,得到的最长回文子串是“#1#2#2#1#”,R就是最右边的#,也就是str[8],C点就在两个2中间的“#”,即str[4]

当前元素i ,以及对称元素i` 两元素关于中心点C对称

条件划分

  • i在R外 暴力扩张

  • i在R内(包括元素 i 的下标与 R 的下标相同) 此时元素 i 在中心点C右边,中心点C左边必有相同元素 i’ ,根据 i’ 的回文区域细分

    • i’ 回文区域在左边界L之内

      元素 i 的回文区域与 i’ 的回文区域完全相同

    • i’ 回文区域在左边界L之外

      元素 i 的回文区域右边界与当前回文最右边界R重合

    • i’ 回文区域左边界与当前回文最左边界L重合 即压线的情况

      元素 i 的回文区域从当前回文最右边界R开始继续向右扩张

代码实现

public static int manacher(String s) {
   if (s == null || s.length() == 0) {
      return 0;
   }
   char[] str = manacherString(s);//"12132" -> "#1#2#1#3#2#"
   int[] pArr = new int[str.length];// 定义回文半径数组 用于统计各元素的回文半径
   int C = -1;//中心点 起始点为-1 回文右边界R更新 C就更新为当前回文区域的中心点
   int R = -1;// 定义回文最右边界 初始值为-1 含义是扩张成功的最右边界的下个位置
   int max = Integer.MIN_VALUE;
   for (int i = 0; i != str.length; i++) {// i位置扩出来的答案,i位置扩的区域,至少是多大。
      //R < i时 即i在R外或者压线,回文半径为1 即只包含自身 继续暴力扩张
      //R == i 时,回文半径为1,因为i跟R压线,无论i'回文区域如何,i回文区域均为自身或者从R开始扩张
      //R > i时,即i在R内,回文半径取对称点i'的回文半径,即i'回文区域在左边界L之内或取R-i,即i’回文区域在左边界压线和最左边界L重合,i回文区域从R开始继续向右扩张或者是i'回文区域在左边界L之外,即i的回文区域只能到R
      //取min的原因就是i’回文区域在L内时 pArr[i’] < L-i' = R-i
      pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
      while (i + pArr[i] < str.length && i - pArr[i] > -1) {
         //判断是否满足回文条件 进行扩张
         if (str[i + pArr[i]] == str[i - pArr[i]])
            pArr[i]++;
         else {
            break;
         }
      }
       //更新R 更新C
      if (i + pArr[i] > R) {
         R = i + pArr[i];
         C = i;
      }
      max = Math.max(max, pArr[i]);//比较得到最大回文子串的回文半径
   }
   return max - 1;//由于有特殊符号#的原因,最大回文子串的回文半径 - 1 就是原字符串的最大回文子串长度 
}

PS:关于条件划分的一些图,因为是手工画的原因,较丑,就不往上粘了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值