算法学习:manacher
【没有前置知识】
【解决的问题】
大多数都和回文串有关
例如:
字符串中长度最长的回文串
【算法学习】
manacher优秀的地方在于,他能够在线性时间内求出每个字符所对应的以其自身为中心的最长回文串长度
(请牢记这个目的)
先来讲朴素算法:
从1~n,对每个字符都从其自身开始,向两边递推,如果左右两边字符相同,范围+1,长度+2
这样的复杂度是n^2的
而 manacher 的优化方式和 kmp 有所类似,都是利用之前已经匹配过和得到过的信息,简化当前的运算
回文串有两种,一种是奇数长度一种是偶数长度,显然他们是需要被区别对待的
但是在理解了manacher的本质之后,我们只需要做微小改动就能够同时解决这两个问题
首先引入 l 和 r ,代表当前已经找到的回文串中,右端点最右的回文串的左右端点,初始为 0 和 1
设数组d为我们所需要求取的数组,即每个字符所对应的以其自身为中心的最长回文串长度
假设我们现在要求d [ i ] 的值,而 d [ i ] 之前的值已经求出来了
当我们现在所求的 i < r 时,即现在这个字符是在一个回文串中的时候
我们就可以利用这个信息,来处理他
求在(l,r)这个回文串中和他位置对称的 d [ j ]
因为在回文串中,所以 d [ i ] 和 d[ j ] 两边一定长度内的字符是相同的
而这个一定长度就是,以 s [ j ] 为中心的回文串的长度,可能超过了(l,r)的范围
所以不能看作回文串,只有在这个范围内才能看作回文串
所以我们能够借用这个因素得到的 d [ i ] 两边的回文串长度就是min( d [ j ] , j + 1 - r )
得到这个长度之后,我们为了得到最终答案,还需要继续扩展 以 s [ i ] 为中心的回文串
所以用朴素算法继续扩展
在扩展过程中,如果以 s [ i ] 为中心的回文串的最右边端点超过了 r
我们就需要更新 l , r ,保证他们所代表的意义不变
然后我们在来讨论之前没有谈论的两种回文串的问题
常见的方法是在每两个字符间都插入一个不可能出现的字符,这样就能够将所有的奇偶字符转化统一
也还有另外一种方法
通过访问双倍长度,枚举每一种情况
实际上和插入字符类似,只不过是把字符插入这种操作
通过数字下标的形式取代
建议画图理解
【复杂度】
实际上我们可以看出来,至始至终每次都是 r 在不断的右移,并没有出现重复,所以最后的复杂度和字符串的长度等同
1 void manacher(char *s,int n,int *d) 2 { 3 d[0] = 1; 4 for (int i = 1,j = 0; i < (n << 1) - 1; i++) 5 { 6 int p = i >> 1, q = i - p; 7 int r = ((j + 1) >> 1) + d[j] - 1; 8 if (r < q) 9 d[i] = 0; 10 else 11 d[i] = min(r - q + 1, d[(j << 1) - i]); 12 while (0 <= p - d[i] && q + d[i] < n && s[p - d[i]] == s[q + d[i]]) 13 d[i]++; 14 if (q + d[i] - 1 > r) 15 j = i; 16 } 17 }
void manacher(char *s,int n,int *d) { d[0] = 1; for (int i = 1,j = 0; i < (n << 1) - 1; i++) { int p = i >> 1, q = i - p; int r = ((j + 1) >> 1) + d[j] - 1; if (r < q) d[i] = 0; else d[i] = min(r - q + 1, d[(j << 1) - i]); while (0 <= p - d[i] && q + d[i] < n && s[p - d[i]] == s[q + d[i]]) d[i]++; if (q + d[i] - 1 > r) j = i; } }