Manacher 算法 / 求最长回文串长度 (个人理解 + 板子代码)

Manacher (马拉车)算法:

  大多用于求某一串字符串中最长的回文串的长度;

  回文串:简单来说就是正着写和倒着写相同的字符串,举个栗子:"abcba" 反过来看也是 "abcba" 这种字符串我们就将他称为回文串

 

在说马拉车之前,先了解一下暴力算法如果去解决这个问题,举个栗子我们需要去知道某一个长度为 n 的字符串 S 中最长的回文串的长度;

暴力:

  首先,我们需要去判断这个 n 的奇偶性;

  如果 n 为奇数我们则需要以字符串 S 中下标为 0 ~ n - 1 的字符为中心字符然后分别向左右延伸去判断每一次延伸的两个字符是否相同,如果相同则这个回文串的长度 +2 ;反之说明此时不满足回文串的条件则跳出循环记录此时以 0 ~ n - 1 的字符为中心字符的回文串长度;此时回文串的初始长度应该为 1 ;

  如果 n 为偶数我们则需要在字符串 S 中下标为 0 ~ n - 1  的字符中选取两个相邻且相同的字符作为中心字符,然后再同时向左右延伸,如果相同则这个回文串的长度 +2 ;反之说明此时不满足回文串的条件则跳出循环记录此时以 0 ~ n - 1 的字符为中心字符的回文串长度;此时回文串的初始长度应该为 2 ;

  这里我们发现我们不仅需要去判断字符的奇偶性,在匹配不同位置的回文串的时候有时候匹配的操作与之前的操作重复了!!! 为了使复杂度减小我们需要用到马拉车算法。

  

马拉车:

  首先,为了去除判断奇偶性的操作,在匹配回文串之前我们需要来做一步预处理的操作:在每一个字符的中间都插入一个不同与每一个字符的元素,这里我插入的是 ' # ' (具体插入什么并不重要),同时在首位也插入 ' # ' ,这里为了判断边界还需要在首位分别插入一个不同于任何的元素作为边界处理;这里我对左边界插入的是 ' $ ' ,对右边界插入的是 ' ^ ' ;这里我们只需要讨论为奇数的子串;

举个栗子:原字符串为: " a b a c a c " 转化成: " $ # a # b # a # c # a # c # ^ "

  如果我们单讨论原字符串的子串 " a b a " 在转换后为:" # a # b # a # ",如果我们单讨论原字符串的子串 " b a " 在转化后为:" # b # a # " 这样,我们在转化后就只需要去讨论长度为奇数的子串;

这里我们取回文字符串 " # a # b # a # " 中的子串 " b # a # " 的长度为回文半径,所以这里的回文半径是 4 ,回文长度则为 4 - 1 = 3 (回文串 " a b a " 的长度);

初始化代码:

 1 void init() //原字符串的转化
 2 {
 3     b[idx++] = '$';
 4     b[idx++] = '#';
 5     for (int i = 0; a[i]; ++i)
 6     {
 7         b[idx++] = a[i];// a 为原字符串,b 为转化后的字符串
 8         b[idx++] = '#';
 9     }
10     b[idx++] = '^';
11     n = idx; // n表示转化后字符串的长度
12 }

 

下一步便是整个马拉车算法的精髓了!!!—— 我们该如何去减少之前重复的操作?(可能你会说,欸?哪里来的重复操作啊?别急马上就知道了)

**马拉车匹配回文串的时候只有一个指针 i 从头遍历到尾;

举个栗子:原字符串为: " b a b c b a c a " ,转化之后就变成了: " $ # b # a # b # c # b # a # c # a # ^ " ;除此之外,我们在匹配回文串的过程中还需要去记录一下就匹配到目前为止(回文串的)右边界最靠近文本字符串右边界的回文串的右边界 mr 和中心字符 mid ;同时我们还需要开一个数组 p [ i ] 表示以 i 为中心字符的回文半径;

初始状态如下:

字符串: $    #     b    #  a  #  b  #  c  #  b  #  a  #  c  #  a  #  ^

   下标: 0  1  2  3  4     5    6    7     8     9   10   11   12  13   14  15   16   17  18   

      i 

     mr

——————————————————————————————————————————

下面我们通过一种特殊情况来讨论:

字符串: $    #     b    #  a  #  b  #  c  #  b  #  a  #  c  #  a  #  ^

   下标: 0  1  2  3  4     5    6    7     8     9   10   11   12  13   14  15   16   17  18

                          i 

                                mid            mr

这里我们发现在 i == 8 的时候,得到了新的回文串,它的 mr 下标为 13,它的中心字符下标为 mid = i = 8;

——————————————————————————————————————————

 

字符串: $    #     b    #  a  #  b  #  c  #  b  #  a  #  c  #  a  #  ^

   下标: 0  1  2  3  4     5    6    7     8     9   10   11   12  13   14  15   16   17  18

                               i 

                                mid          mr

当 i == 9 时,我们已知了下标 3 ~ 13 是以段回文串,这里我们以 mid 为中心字符(对称轴)发现下标为 6 的字符 ' b ' 与此时下标为 10 的字符 ' b ' 对称,而在此之前我们算出了 p [ 6 ] 的值,也就是中心字符下标为 6 时的最长回文串:" # b # " 的回文半径 2 ;而回文串里面的下标为 5 的 ' # ' 与下标为 11 的 ' # ' 对称,下标为 7 的 ' # ' 与下标为 9 的 ' # ' 对称,所以此时我们可以得知 p [ 10 ] == p [ 6 ] ;我们就不需要再去判断下标为 6 的字符和下标为 10 的字符是否相同了,这样就会减少很多的重复操作;

——————————————————————————————————————————

字符串: $    #     b    #  a  #  b  #  c  #  b  #  a  #  c  #  a  #  ^

   下标: 0  1  2  3  4     5    6    7     8     9   10   11   12  13   14  15   16   17  18

                                             i 

                                mid            mr

当 i = 12 时,我们发现下标为 4 的 ' a ' 是与下标为 12 的 ' a ' 对称的,但是这里需要注意的是 p [ 4 ] 的回文串 " # b # a # b # " 其中的子串 " # b " 超出了目前记录的回文串范围,我们无法去保证下标 1 与下标 15 对称,下标 2 与下标 14 对称,所以这里我们的 p [ 12 ] 只能取 " # a # " 的回文半径 2 ;

——————————————————————————————————————————

代码如下:

 1 void manacher()//p[i]表示中心字符为 b[i] 的回文串的最大长度
 2 {
 3     int mr = 0, mid;
 4     for (int i = 0; i < n; ++i)
 5     {
 6         if (i < mr)
 7         //判断是否超出目前所记录的回文串的范围
 8             p[i] = min(p[2 * mid - i], mr - i);
 9         else
10             p[i] = 1;
11         while (b[i - p[i]] == b[i + p[i]])
12         //分别向两边延伸判断是否是回文串
13             p[i]++;
14         if (i + p[i] > mr)
15         //匹配完好需要去更新 mr 和 mid 
16         {
17             mr = i + p[i];
18             mid = i;
19         }
20     }
21 }

 

posted @ 2022-07-11 16:59  aYi_7  阅读(120)  评论(0)    收藏  举报