带上 POJ 3974 Palindrome , 谈谈我对Manacher算法的理解(注释版代码)

    xiaoxiao2022-07-07  188

    原题:http://fastvj.rainng.com/problem/21720/origin

    题意: Manacher模板题

    在B站有人上传了Kuangbin的直播录像,正好有讲Manacher算法的,在这里安利一下

    ------我对Manacher的理解:

    首先定义两个数列,ma和mp

    ma数组(manacher的简称):用于存储修改后的字符串。 为什么需要修改呢,原因是字符串有奇有偶处理起来不方便,于是可以统一将其变成奇数,做法就是在每一个字符前后都加入一个该字符串从来没有出现过的字符,根据惯例,选用‘#’,然后在最开头加上另外一个没出现过的字符,作为边界判断,同样惯例采用’$’,至于为什么要特地加这个而不进行下标判断呢,原因是使用这个会为代码带来极大的方便,后文有讲。

    如123   就变成了 $#1#2#3#

    mp数组(这个我就不知道是啥简称了): 用于储存以ma中每一个元素为中心的最长回文串的半径,1起步(自己都算一个)

    如 #1#  则 mp[0]=1    mp[1]=2

    众所周知, 假如需要在O(n)的时间内查找一个字符串的最长回文子串,那么一定需要在遍历的时候一边处理,也预示着需要用之前所处理出来的结果。那么这时候我们就开始顺利成章的假设,假设我已经知道了 mp[0~i](不包括i),那如何利用这一已知条件呢?

    先看看代码:

    for (int i = 0; i < l; i++) { // i: 表示现在正在处理的那个子串中心 mp[i] = mx > i ? min(mp[id*2 - i], mx - i) : 1; //假如子串存在于最大半径之内,那么直接就可以利用之前匹配到的信息,否则只能从新来了 while (ma[i + mp[i]] == ma[i - mp[i]]) mp[i]++; if (i + mp[i] > mx) {//更新最大覆盖到的半径 mx = i + mp[i]; id = i; } maxx = maxx < mp[i] ? mp[i] : maxx; }

    为了最大化利用已知条件,我们引入两个变量,id和mx,mx指的是0~i中最大的(id+mp[id]),指的是我已知回文串最能够得着右边的。

    因为id的回文串是已经知道的,那么假如i在id的回文串半径之内,我们就可以在id的另外一边对称的地方,也就是2*id-i这个地方得到mp[i]的一部分回文串信息,我们就不需要再重复匹配这一部分了,可以直接让mp[i]=mp[2*id-i],但是此时需要注意,假如i+mp[i]超过了 (id+mp[id]),也就是我已知回文串最能够得着右边的那个地方,由于再往右就已经是未知了,不能保证依然是对称,所以这个时候需要将mp[i]赋值为mx-i ,然后再继续往下找

    假如i不在id的回文串半径内呢,那么只能朴素匹配了,将mp[i]赋值为1慢慢找吧。

    噢对了最后说一下为什么开头加入'$'会方便一点吧,可以看到查找回文串的代码是:

    while (ma[i + mp[i]] == ma[i - mp[i]]) mp[i]++;

    也就是在开头加入一个整个字符串都不会出现的字符那么程序必然在这里跳出循环,就不需要每次都判断下标(作为一个小小的加速吧)

    最后是这道题的代码:

    #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn = 2e6 + 7; char s[maxn]; char ma[maxn]; // 马拉车字符串 int mp[maxn]; // 记录最长回文串 这个数组-1就是答案 void Manacher(int cnt) { int maxx = 0; int l = 0; // l:新数组长度 ma[l++] = '$'; ma[l++] = '#'; int end = strlen(s); for (int i = 0; i < end; i++) { //scanf("%c", &ma[l++]); ma[l++] = s[i]; ma[l++] = '#'; } ma[l++] = 0; int mx = 0, id = 0; // mx:最大半径能到的地方 id:最大半径中心 for (int i = 0; i < l; i++) { // i: 表示现在正在处理的那个子串中心 mp[i] = mx > i ? min(mp[id*2 - i], mx - i) : 1; //假如子串存在于最大半径之内,那么直接就可以利用之前匹配到的信息,否则只能从新来了 while (ma[i + mp[i]] == ma[i - mp[i]]) mp[i]++; if (i + mp[i] > mx) {//更新最大覆盖到的半径 mx = i + mp[i]; id = i; } maxx = maxx < mp[i] ? mp[i] : maxx; } printf("Case %d: %d\n", cnt, maxx-1); } int main() { int cnt = 0; while (scanf("%s", s)) { //if (s == "END") break; if (s[0] == 'E') break; else { Manacher(++cnt); } } return 0; }

     

    最新回复(0)