--算法恩仇录-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数组的求解,如果需要获取最大长度,自行遍历即可~

 

 

    

posted @ 2020-08-29 20:58  小虾米在code江湖  阅读(189)  评论(0编辑  收藏  举报