Manacher算法详解
Manacher算法是用来求最长回文长度的一个算法,不同于暴力方法的O(n*n)的时间复杂度,manacher算法可以达到O(n)是时间复杂度。
对于暴力求解一个字符串中最长的回文长度,也是有着技巧的,因为要考虑字符串为奇数偶数两种情况,所以暴力求解一般需要对字符串做一个优化,即是给每个字符之间插入一个标志符。
例如对于字符串str1 : abcdcbeee
假如我们用#来作为标识符,经过优化后的字符串str2为#a#b#c#d#c#b#e#e#e#,我们求str2每个字符的回文长度,而后将所求的最大一个回文长度除以2,得到的结果即是该字符串最长的回文长度。
如何将str1 变成str2 代码如下:
public static char[] manacherString(String str){ char[] chars = str.toCharArray(); char[] res = new char[chars.length*2+1]; int index = 0; for (int i=0;i<res.length;i++){ res[i] = (i & 1) == 0 ? '#' : chars[index++]; System.out.print(res[i]+" "); } System.out.println(); return res; }
上述代码将所有下标为偶数的位置置为#,而奇数位依次填充原数组的值。
manacher算法是如何做到时间复杂度为O(n)的呢,它充分利用了回文对称的性质。
我们首先定义两个变量c和r,c代表回文中心,r代表c的回文右边界。这里需要注意的是c并不是当前回文的位置,而是r对应的回文中心。而r是最右的回文边界。
这里要做到的一件事就是r永不回退,意思就是r只能往右边走,不能往左边退,当当前位置i的右回文半径超过r时,我们更新c为i,更新r为当前回文中心的右边界。
最开始,我们将c和r初始化为-1,而后我们遍历整个字符串数组,对于没有在r范围内的i,我们暴力扩,求其r,而后更新r和c。
对于i在r中的情况,又具体分为三种情形,第一种i关于c对称的i‘的回文半径在r中,那么i的回文半径就等于i’的回文半径。
第二种i关于c对称的i‘的回文半径在r外,则i的回文半径为r-i。这两种情况的时间复杂度都为O(1)。
第三种i关于c对称的i‘的回文半径在r上,这时就需要继续暴力比较了。
关于算法的证明,简单在草稿纸上画一画,就很容易看出这四种情况,这里不做赘述。
代码如下:
public static int maxLcpsLenth(String str){ char[] chars = manacherString(str); int[] m = new int[chars.length]; int r = -1; int c = -1; int max = 0; for(int i =0 ;i<chars.length;i++){ m[i] = r > i ? Math.min(m[2*c-i],r-i):1; while (i-m[i] >-1 && i+m[i] < chars.length){ if(chars[i+m[i]] == chars[i-m[i]]){ m[i]++; }else { break; } } if(i+m[i]>r){ r = i+m[i]; c = i; } max = Math.max(max,m[i]); } return max -1 ; }
这里定义的一个int型的数组,用来存储每一个坐标的回文半径长度,由于我们的chars数组是原数组经过扩展边长得到的,所以最后求得的回文长度直接用回文半径减一便可以得到。