--算法恩仇录-01-小虾米回忆马拉车--(Manacher回文串算法)
是日,小虾米于门中无事,闲来下山走走。路经一处山脚,此山巍峨耸立,远远望去山尖似乎有一高塔。小虾米心中好奇,不禁询问上山之人:“兄台,敢问此为何处?”,那位无面之人轻笑答道:“此山乃算法山脉一脉,名为阿狗丽泽山(algorithm),此山最高峰名为阿狗丽泽峰,至于那座宝塔,自然是闻名于江湖的力扣塔(LeetCode)!”
闻名江湖?!
小虾米暗暗心惊,此有名之物我怎么不知道。一时也顾不得闲游,忙追上先前之人的步伐,继续询问有关力扣塔一事。
原来,这宝塔中闻名于世,不知其高,无人知晓其层数几何。但其宝塔中,每层都关卡重重,江湖中人,任何人都可前去闯关,可从任意一层开始,每层难易未知,但难以挑战的关卡也有高手破关过,简单关卡也有江湖新手通过。于是江湖中就流传出力扣榜,记录每位闯关者闯过大大小小的关卡,挑战次数,战胜几何,闯关数排名均有记录。
如此宝地,小虾米岂能不前去一看?
少几,小虾米来到塔前,有一层名为“最短回文串”。
咦?小虾米心中一疑,不禁回想起当前在修武堂所学... ...
Manacher算法
俗称‘马拉车’,用于处理回文字符串的一种针对字符串使用的算法。
1.什么是回文串?
回文串,是一类字符串。这类字符串 从左至右的字符排列 和 从右至左的字符排列 一致。
例如:‘abcba’,就是一个回文串。
马拉车,则是解决找寻一个正常字符串中的最大回文串的长度。
如果我们使用暴力枚举,对每一个字符进行 “同时向该字符两边进行遍历匹配操作的话”,其时间复杂度为O(n^2)。(同时还要考虑 字符串的长度奇偶 的问题,偶数长度字符串与奇数长度字符串的遍历起始点取值稍有不同)
而马拉车这能将时间复杂度完成为O(n)。
如何实现?请往下看。
2.马拉车的实现
1.处理原字符串
对于原字符串,我们首先需要对其进行一个处理。对于每个字符中间需要加上非匹配的字符。在字符串首尾添加另一类字符。
如: 原字符串:‘aaaba’
处理后:‘$a#a#a#b#a#*’
其意义在于,将原字符串 偶数长度、奇数长度的字符串 全部转换为 奇数字符串 进行处理。
2.思路
开始讲解马拉车的思路之前,先考虑下一个问题,这对算法的理解有很大帮助。
问题:如果要优化暴力枚举遍历字符串的步骤使其复杂度降下来,应该如何优化?或者说应该在哪一步进行优化?
暴力枚举的解决办法为:
遍历每个点,再由每个点向两端遍历。
那么在这过程中会产生一个问题,例如字符串:‘abcdcba’ 中,我们先遍历了'd' 字符,然后再遍历了第二个'c'字符,可我们已经知道了'd'的最大回文长度为7,知道了d是左右对称的回文串,总长度长度为7,那么我们还要重新遍历'c'的回文长度,岂不是进行了没必要的操作?
‘c’是已经在‘d’的遍历中判断过了,依据‘d’的长度还可以知道,在‘d’的左边也有一个‘c’,那么我们完全可以将第一个‘c’的状态赋给第二个‘c’的状态,省去对第二个‘c’的遍历。
马拉车的优化就在于此。
对于每一个字符,都会将其位置i与之前字符的最大回文进行一个比较。
(假设当前遍历到i字符串,将i之前遍历到的最大回文中心点cpoint的最远距离记为rpoint)
这时候就有两种状态:
1.i < rpoint ,即当前点i处于之前遍历过的最大回文中,属于已经遍历过的部分。
那么此时,我们需要找到i以cpoint为轴进行对称的点j。(j<i,j点由于已经遍历过,因此j点的回文状态是已知的),因此我们可以借由j的状态得到i的状态。
但此时又会出现一个问题:如果j点的回文长度会延伸到cpoint的回文长度之外,我们如果直接获取j的状态,会将未匹配过的字符交付给i。
因此还需要一个len[j]与rpoint-i (最长回文已匹配的长度)的比较。取其最小值即可。
2.i > rpoint, 即当前点i处于之前还未遍历到的位置。
此时,需要自己重新进行遍历,为后续点进行更新状态。
这就是马拉车的核心部分。
因此,我们需要一个存储状态的数组len,以及记录最大回文中心点cpoint和最大回文最远点rpoint。
模板代码如下:(JS编写)
1 console.log('Code Running ...'); 2 console.log('This is Manacher Algorithm...') 3 const initString = function(str0){ 4 str = '$#'; 5 6 for(let i = 0; i<str0.length; i++){ 7 str += str0[i]; 8 str += '#'; 9 } 10 // str += '*'; 11 return str; 12 } 13 const getLen = function(str){ 14 str = initString(str); 15 let len=[]; //初始化len数组 16 let rpoint=0, cpoint=-1;//rpoint为最右端的点,cpoint为回文串的中心点 17 for(let i=1; i<=str.length; i++){ 18 //当前点是否在已经匹配过的回文串中 19 //是,则取 i关于cpoint对称点的len值 和 rpoint与i的差值 20 if(rpoint > i){ 21 len[i] = Math.min(len[2*cpoint - i], rpoint - i); 22 } else { 23 //否则直接为1 24 len[i] = 1; 25 } 26 //遍历回文 27 //如果为1,说明len还未匹配这么远,重新对该点遍历 28 for(;str[i - len[i]] == str[i + len[i]];len[i]++){} 29 if(len[i] + i > rpoint) rpoint = len[i] + i,cpoint = i; 30 } 31 //返回得到的len数组 32 return len; 33 } 34 let str0 = 'aaaba'; 35 const len = getLen(str0); 36 console.log(len);
PS:代码部分主要是对len数组的求解,如果需要获取最大长度,自行遍历即可~