Manacher算法详解
Manacher算法解决的问题:字符串str中,最长回文子串的长度如何求解?如何做到时间复杂度 O ( N ) O(N) O(N)完成
经典解法:遍历str中的字符,以str[i]
为中心,向左右两边扩,求出最长回文子串。
- 但是这种方法无法找到长度为偶数的回文子串。
- 改进:
- 每个字符间隙都插入一个辅助符号。比如
123232
→ \to →#1#2#3#2#3#2#
- 修改后的字符向左右两边扩的时候,就可以找出长度为偶数的回文子串,复杂度为 O ( N 2 ) O(N^2) O(N2)
- 辅助符号是否必须是原字符串中未曾出现的?
- 不是,向左右两边扩的时候,辅助符号只会和辅助符号比较、而原字符也只会和原字符比较,所以不会有影响。
- 每个字符间隙都插入一个辅助符号。比如
学习Manacher之前,先来了解几个概念:
- 回文半径:对于一个回文字符串
12321
,其回文直径就是整个回文字符串的长度5,回文半径就是整个回文字符串一半的长度3. - 回文半径数组:在从左往右遍历数组的过程中,把以每个字符为中心的回文子串的回文半径记录下来。
- 之前所扩的所有位置中所到达的最右回文右边界R:
- 以
#1#2#3#2#3#2#
为例,不论以哪个符号为中心向左右两边扩,只要扩出来的右边界大于R,则更新R - 初始时,
R=-1
- 到
1
时,扩出来的右边界为2,R更新为2。
- 以
- 最右边界中心点C:
- C记录了扩到当前最右边界的中心点,也就是说,如果R更新了,则C就更新为当前的中心符号下标。
Manacher详解:
Manacher整个算法会用到上面所说的四个概念。
指针i
在从左往右遍历数组的过程中,有以下几种情况:
-
i>R
,此时无法优化,只能从i
向左右两边扩,同时更新R
和C
-
C<i<R
,以C
为中心点的最大回文区域为[L,R]
,所以i
会有一个对称点i'
,又会分两种情况- 如果以
i'
为中心的最大回文区域也在[L,R]
中,则i
的最大回文区域和i'
的最大回文区域对称
-
为什么
i
最大回文区域不可能比[z, p]
更大呢?在
C
的最大回文区域中,一定有x = p, y = z
(因为是回文嘛),而前面已经说了[x, y]
是i'
的最大回文区域,说明x!=y
,所以p!=z
,i
的最大回文区域不可能超过[z, p]
-
这样,我们就不需要再去计算
i
的最大回文区域的半径了,直接取i'
的即可。 -
如果以
i'
为中心的最大回文区域越过了[L,R]
的范围,则i
的最大回文区域就是下图的[R', R]
为什么
i
的最大回文区域不能更大呢?- 首先在回文区域
[L, R]
中,L'
和R'
是对称的,所以有L' = R'
- 而在
i'
的最大回文区域[x, y]
中,一定有L = L'
- 以
C
为中心的最大回文区域为[L, R]
,所以L != R
- 综上,
R' != R
,即i
的最大回文区域就是[R', R]
- 首先在回文区域
-
如果以
i'
为中心的最大回文区域恰好没超过[L, R]
(压线,即x = L
),则i
的最大回文区域的右边界至少为R
,R往后的部分仍需判断。
- 如果以
JavaCode:
public class Manacher {
/**
* 求s的最大回文子串的半径长度
*/
public static int maxLcpLength(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] str = manacherString(s);
// pArr: 回文半径数组
int[] pArr = new int[str.length];
// c: 中心, r: 回文右边界的再往右一个位置,最右的有效区是R-1位置
int c = -1, r = -1;
// max: 扩出来的最大值
int max = Integer.MIN_VALUE;
// 每一个位置都求回文半径
for (int i = 0; i < str.length; i++) {
/*
i至少的回文半径,先赋给pArr[i]
情况一:i在r外,此时str[i]本身就构成一个回文串,所以至少为1
情况二:i在r内,i的对称点为i'
1. i'的最大回文区域在[l, r]内,i回文半径就是i'的回文半径
· i' = 2 * c - i
· 此时pArr[2 * c - i] > r - i
2. i'的最大回文区域超过了[l, r],此时r - i就是i的回文半径
· 此时pArr[2 * c - i] < r - i
3. i'的最大回文区域压线,此时pArr[2 * c - i] == r - i是i的回文半径
*/
pArr[i] = r > i ? Math.min(pArr[2 * c - i], r - i) : 1;
// 半径扩充操作,但实际上只有情况一、和情况二.3需要扩充操作,另外两个不会再扩了
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;
}
/**
* 字符串填充: 1221 -> #1#2#2#1#
*/
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;
}
}
LeetCode459.重复的子字符串
原题:459. 重复的子字符串 - 力扣(LeetCode)
难度:Esay
解法一:错位匹配
-
由重复子串构成的字符串一定可以实现如下的匹配:
错位移动的距离就是整个重复子串的长度,移动之后,通过判断重叠部分是否相同来判断是否是满足题意的字符串
-
如何判断重复子串,即错位移动的距离?
- 首先可以肯定,重复子串的首字符一定是整个字符串的首字符
str[0]
,我们就可以根据str[0]
来找到下一个与它相等的字符,这个字符就作为循环结束的标志,当然也不一定,比如abacabac
,循环子串abac
中出现了两次a
,这时候就需要对做好判断
- 首先可以肯定,重复子串的首字符一定是整个字符串的首字符
-
复杂度为 O ( N 2 ) O(N^2) O(N2),边界条件比较麻烦。
class Solution {
public boolean repeatedSubstringPattern(String s) {
if (s.length() == 1) {
return false;
}
char[] str = s.toCharArray();
int i = 1, j; // i: 用于找寻循环前缀
while (i < str.length) {
while (i < str.length && str[i] != str[0]) {
i++;// 找到下一个与首字符相等的字符
}
int temp = i;// 记录当前的位置,此时str[i]=str[0]
if (str.length - i < i) {
return false;// 如果后面字符的长度小于循环前缀的长度,说明不可能有一个完整的循环前缀,直接返回false
}
// 开始错位匹配
j = 0;
int prex = i;
while (i < str.length && str[i] == str[j]) {
i++;
j++;
}
// 匹配停止,如果成功遍历到字符串的末尾,并且剩余的长度是循环前缀的倍数,则表示是满足题意的字符串。
if (i == str.length && str.length % prex == 0) {
return true;
}
// 否则,继续寻找下一个等于str[0]的字符
i = temp + 1;
}
return false;
}
}
解法二、移动匹配
-
对于一个字符串
s
,如果s
是由循环子串构成的,则s+s
的中部一定会包含一个s
,如下图: -
所以我们可以先破坏,前后两个S的完整性,即去掉
s+s
首尾两个字符,然后应用字符串匹配算法,判断剩余字符串中是否有s
class Solution {
public boolean repeatedSubstringPattern(String s) {
String str = s + s;
return str.substring(1, str.length() - 1).contains(s);
}
}
解法三、KMP
- 实际上是对上述解法的优化,因为各类语言库函数中的字符串查找方法(java中是
contains()
)一般都是复杂度较高的方法(暴力解法?),应用KMP能很好的降低复杂度
class Solution {
public boolean repeatedSubstringPattern(String s) {
String str = s + s;
return getIndexOf(str.substring(1, str.length() - 1), s) != -1;
}
public static int getIndexOf(String s, String m) {
if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
return -1;
}
char[] str1 = s.toCharArray();
char[] str2 = m.toCharArray();
int i1 = 0, i2 = 0;
// O(M)
int[] next = getNext(str2);
// O(N)
while (i1 < str1.length && i2 < str2.length) {
if (str1[i1] == str2[i2]) {
i1++;
i2++;
} else if (i2 > 0) {
i2 = next[i2];
} else {
i1++;
}
}
return i2 == str2.length ? i1 - i2 : -1;
}
public static int[] getNext(char[] ms) {
if (ms.length == 1) {
return new int[]{-1};
}
int[] next = new int[ms.length];
next[0] = -1;
next[1] = 0;
int i = 2;
int cn = 0;
while (i < next.length) {
if (ms[i - 1] == ms[cn]) {
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
return next;
}
}