Manacher(马拉车)

作为一个计算机科学技术学院的大学生,在决定自己要跟着走学校举办的ACM时,Manacher可以说是第一个比较用心研究的算法了,在这之前和一位学姐聊天,发现不知不觉都大二了,有些东西是时候该学学了,好了话不多说,开始我们今天的正题 ————马拉车
这个马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,由一个叫 Manacher 的人在 1975 年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性,这是非常了不起的。对于回文串想必大家都不陌生,就是正读反读都一样的字符串,比如 “bob”, “level”, “noon” 等等,那么如何在一个字符串中找出最长回文子串呢,可以以每一个字符为中心,向两边寻找回文子串,在遍历完整个数组后,就可以找到最长的回文子串。但是这个方法的时间复杂度为 O(n*n),并不是很高效,下面我们来看时间复杂度为 O(n)的马拉车算法。
**由于回文串的长度可奇可偶,**比如 “bob” 是奇数形式的回文,“noon” 就是偶数形式的回文,马拉车算法的第一步是预处理,做法是在每一个字符的左右都加上一个特殊字符,比如加上 ‘#’,那么bob --> #b#o#b#noon --> #n#o#o#n# **这样做的好处是不论原字符串是奇数还是偶数个,处理之后得到的字符串的个数都是奇数个,这样就不用分情况讨论了,而可以一起搞定。接下来我们还需要和处理后的字符串t等长的数组p,其中 p[i] 表示以 t[i] 字符为中心的回文子串的半径,若 p[i] = 1,则该回文子串就是 t[i] 本身,那么我们来看一个简单的例子:# 1 # 2 # 2 # 1 # 2 # 2 #
1 2 1 2 5 2 1 6 1 2 3 2 1为啥我们关心回文子串的半径呢?看上面那个例子,以中间的 ‘1’ 为中心的回文子串 “#2#2#1#2#2#” 的半径是6,而未添加#号的回文子串为 “22122”,长度是5,为半径减1。这是个普遍的规律么?我们再看看之前的那个 “#b#o#b#”,我们很容易看出来以中间的 ‘o’ 为中心的回文串的半径是4,而 "bob"的长度是3,符合规律。再来看偶数个的情况 “noon”,添加#号后的回文串为 “#n#o#o#n#”,以最中间的 ‘#’ 为中心的回文串的半径是5,而 “noon” 的长度是4,完美符合规律
。所以我们只要找到了最大的半径,就知道最长的回文子串的字符个数了。**只知道长度无法定位子串,我们还需要知道子串的起始位置。 我们还是先来看中间的 ‘1’ 在字符串 “#1#2#2#1#2#2#” 中的位置是7,而半径是6,貌似 7-6=1,刚好就是回文子串 “22122” 在原串 “122122” 中的起始位置1。那么我们再来验证下 “bob”,“o” 在 “#b#o#b#” 中的位置是3,但是半径是4,这一减成负的了,肯定不对。所以我们应该至少把中心位置向后移动一位,才能为0啊,那么我们就需要在前面增加一个字符,这个字符不能是#号,也不能是s中可能出现的字符,所以我们暂且就用美元号吧,毕竟是博主最爱的东西嘛。这样都不相同的话就不会改变p值了,那么末尾要不要对应的也添加呢,**其实不用的,不用加的原因是字符串的结尾标识为 ‘\0’,等于默认加过了。**那此时 “o” 在 “KaTeX parse error: Expected 'EOF', got '#' at position 1: #̲b#o#b#" 中的位置是4,…#1#2#2#1#2#2#” 中的位置是8,而半径是6,这一减就是2了,而我们需要的是1,所以我们要除以2。之前的 “bob” 因为相减已经是0了,除以2还是0,没有问题。再来验证一下 “noon”,中间的 ‘#’ 在字符串 “$#n#o#o#n#” 中的位置是5,半径也是5,相减并除以2还是0,完美。可以任意试试其他的例子,都是符合这个规律的,最长子串的长度是半径减1,起始位置是中间位置减去半径再除以2
那么下面我们就来看如何求p数组,需要新增两个辅助变量 mx 和 id,**其中 id 为能延伸到最右端的位置的那个回文子串的中心点位置,mx 是回文串能延伸到的最右端的位置,需要注意的是,这个 mx 位置的字符不属于回文串,所以才能用 mx-i 来更新 p[i] 的长度而不用加1,由 mx 的更新方式 mx = i + p[i] 也能看出来 mx 是不在回文串范围内的,**这个算法的最核心的一行如下:
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
以上结论转载于博客园的Grandyang作品下面是链接 感兴趣的可以去看看原创作者https://www.cnblogs.com/grandyang/p/4475985.html
但是他后面关于情况的讨论 由于我当时看的他的博客他说是配图但是我看的时候却没有,可能是我领悟力不够,各位大能可以自己试试。
但是呢总体代码我还是会采用Grandyang的,在这里我仅仅给大家尽我的能力说一下我的理解,可能更适合小白来看:
讲这个题目的三种情况之前大家要明白 上面说的 马拉车的好处在于他把复杂度为O(nn)的算法变成了O(n)的复杂度。
要是大家以前从来没接触过这个马拉车不明白他这个改变算法复杂度的原理我想理解起来还是比较困难的,我就从一个小白的角度给大家分享一下这个理解
首先就是求最长的回文字符串,我们复杂度为O(n
n)的一般方法就是直接 遍历数组 暴力求解 以最长字串的中心为中心 俩边都需要求出(这部分是我自己理解。可能不是太官方,也可能不是很对,但是他原理应该是没问题的) 所以为O(n*n),而马拉车呢就是通过对称点,求出一个,直接确定另一个所以只需要求对称点的一边就可以了 那么复杂度就变成了O(n)。
那接下来我们讨论三种情况
(1)以j为中心的回文串有一部分在以id为中心的回文串之外。因为mx是以id为中心的最长回文的右边界,所以以i为中心的回文串不可能会有字符在以id为中心的回文串之外;否则mx就不是以id为中心的最长回文的右边界。所以,在这种情况下,p[i]=mx–i。下附图!
第一种情况

得出结论 第一种情况是 p[i]=mx–i,大家先记住这个情况。
(2)以j为中心的回文串全部在以id为中心的回文串的内部,则p[i]=p[j],而且p[i]不可能再增加。第二种
当第二种情况时 mx - i > P[j] ,以 S[j] 为中心的回文子串包含在以 S[id] 为中心的回文子串中,由于 i 和 j 对称,以 S[i] 为中心的回文子串必然包含在以 S[id] 为中心的回文子串中,所以必有 P[i] = P[j],其中 j = 2id - i,因为 j 到 id 之间到距离等于 id 到 i 之间到距离,为 i - id,所以 j = id - (i - id) = 2id - i。
此时的结论是 p[i]=p[j]=p[2*id - i] 在记住这个结论
(3)以j为中心的回文串的左端正好与以id为中心的回文串的左端重合。则p[i]=p[j]或p[i]=mx–i,并且p[i]还有可能会继续增加即while (t[i + p[i]] == t[i - p[i]]) ++p[i];(t是添加过#和$之后的新字符串)基本第三种情况和上面俩种类似 ,就不手画图片了经过这三种情况的讨论 得出Grandyang博客中说的,马拉车最核心的一句语言如下
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;(如果大家看不懂这条语句的话可以去查一下?:运算符的使用,我就不在这里给大家赘述了)
最后给出大家Grandyang博客中代码
#include
#include
#include

using namespace std;

string Manacher(string s) {
// Insert ‘#’
string t = “$#”;
for (int i = 0; i < s.size(); ++i) {
t += s[i];
t += “#”;
}
// Process t
vector p(t.size(), 0);
int mx = 0, id = 0, resLen = 0, resCenter = 0;
for (int i = 1; i < t.size(); ++i) {
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
while (t[i + p[i]] == t[i - p[i]]) ++p[i];
if (mx < i + p[i]) {
mx = i + p[i];
id = i;
}
if (resLen < p[i]) {
resLen = p[i];
resCenter = i;
}
}
return s.substr((resCenter - resLen) / 2, resLen - 1);
}

int main() {
string s1 = “12212”;
cout << Manacher(s1) << endl;
string s2 = “122122”;
cout << Manacher(s2) << endl;
string s = “waabwswfd”;
cout << Manacher(s) << endl;
}
vector
iostream
string
这是开头的三个头文件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
马拉车算法是一种常用于字符串匹配的算法,其核心思想是利用回文串的对称性来减少比较次数。Java中可以通过以下步骤实现马拉车算法: 1. 预处理字符串,将字符串中的每个字符用一个特殊字符隔开,如:将字符串"abc"变成"#a#b#c#" 2. 维护一个数组P,其中P[i]表示以i为中心的最长回文子串的半径长度。具体实现时,可以使用一个中心点center和右边界right来维护,其中center表示当前已知的最长回文子串的中心点,right表示该回文子串的右边界。根据回文串的对称性,可以利用已知回文串的左侧字符的对称点来推出右侧字符的回文半径。 3. 遍历字符串,根据P数组更新center和right,并记录最长回文子串的起始位置和长度。 以下是Java代码实现示例: ```java public class ManacherAlgorithm { public static String longestPalindrome(String s) { if (s == null || s.length() == 0) { return ""; } StringBuilder sb = new StringBuilder(); sb.append("#"); for (int i = 0; i < s.length(); i++) { sb.append(s.charAt(i)); sb.append("#"); } String str = sb.toString(); int[] P = new int[str.length()]; int center = 0, right = 0; int start = 0, maxLen = 0; for (int i = 0; i < str.length(); i++) { if (i < right) { P[i] = Math.min(right - i, P[2 * center - i]); } while (i - P[i] - 1 >= 0 && i + P[i] + 1 < str.length() && str.charAt(i - P[i] - 1) == str.charAt(i + P[i] + 1)) { P[i]++; } if (i + P[i] > right) { center = i; right = i + P[i]; } if (P[i] > maxLen) { start = (i - P[i]) / 2; maxLen = P[i]; } } return s.substring(start, start + maxLen); } public static void main(String[] args) { String s = "babad"; System.out.println(longestPalindrome(s)); } } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值