回文子串
回文分为奇数回文和偶数回文,即字符串的长度为奇数还是偶数。其实只需要讨论奇数的情况,即以某个字符为中心,左右两边的字符按照中心对称的情况(偶数的情况可以通过本文末尾的方法转换成奇数的情况)。
最长回文子串
朴素算法
枚举每个字符,作为中心,向两边扩展判断是否相等,找到最大的可行长度。算法最坏复杂度O(n^2)。
图1Manacher算法
用p[i]表示以第i个字符为中心轴,两边字符轴对称的最大半径。如图所示,p[1]=1,p[2]=2,p[4]=3,p[7]=5。主体思想还是和朴素算法类似,从小到大枚举i,计算p[i]。核心思想在于p[i]的初始值,它不再是1,而是根据之前计算的{p[j] | 1<=j<i}来确定p[i]的初始值。
图2
我们用一个变量ct来保存前面所有字符产生的最大回文子串的“最大右边界”的中心轴位置,“最大右边界”的含义不是p[i]最大,而是i + p[i]最大,如图2中枚举完前8个位置后,得到ct=7,对应的右边界为ct + p[ct] - 1。
然后,我们有了一些初始信息,那么当枚举到第i个字符时,分情况讨论:
1)如图3所示,如果i<ct+p[ct],即第i个字符必然落在以ct为中心轴的最大回文子串之内。那么我们必然可以找到一个和i点按照ct轴对称的点j,满足(i+j)/2=ct,则j = 2*ct-i。
图3
这时候,我们继续分情况讨论,j的位置可能衍生出三种情况,如图4所示:
图4
(a)的情况,p[i]=p[j],而且不可能再长。假设p[i]能够再长,则根据对称性,p[j]也能够再长,但是p[j]已经确定了,所以假设不成立;
(b)的情况,p[i]=p[j],和(a)不同的是,它有可能还可以向两边扩张;
(c)的情况,p[i]=ct+p[ct]-i,而且不可能再长。还是利用反证法,如果p[i]两边可以再扩张一个字符,如图中的y和z部分相等,那么必然x和y部分也相等(根据对称性),而x和w也相等,从而w和z相等,这样说明原本的p[ct]可以再增长,而p[ct]已经确定了,所以假设不成立。
图5
2)i>=ct+p[ct],那么前面的一些计算信息无法为我们所用,于是p[i] = 1;
图6
综合上述情况,可以得到p[i]的初始值如下:
if (i < ct + p[ct]) {
p[i] = min(p[2*ct-i], ct + p[ct]-i);
}else {
p[i] = 1;
}
然后再循环判断第i-p[i]个字符和第i+p[i]个字符是否相等,如果相等则p[i]++;否则退出循环。然后利用新的i+p[i]更新ct。
代码实现
int Manacher(char *str) {
int ct = 0, r = 0, maxLen = 1;
p[0] = 1;
for(int i = 1; str[i]; ++i) {
// 1.计算p[i]初始值
if(i < r) {
p[i] = Min(p[2*ct-i], r-i);
}else {
p[i] = 1;
}
// 2.扩张p[i],以适应达到p[i]最大值
while(i-p[i]>=0 && str[i-p[i]] == str[i+p[i]])
++p[i];
// 3.更新ct
if(p[i] + i > r) {
ct = i;
r = p[i] + i;
}
// 4.更新最长回文
if(2*p[i]-1 > maxLen) {
maxLen = 2*p[i] - 1;
}
}
return maxLen;
}
当所有字符都相等时,该算法达到最坏复杂度,观察可得复杂度O(n)。
奇偶回文转换
以上算法只支持字符串长度为奇数的情况,那么接下来我们通过一种方法,将偶数字符串转换成奇数。如图所示,相邻字符间插入一个不在该字符串的字符集中的字符,比如$。原串长度为s,则转换后的串长度为2*s+1,所以必然为奇数。
图7
并且这么做以后有几个比较明显的性质:
1、任何一个回文子串的两端必然是'$'。
2、以'$'为中心的最长回文子串的半径一定是奇数;反之,一定是偶数。
Manacher
Manacher
Manacher+单调性
Manacher+动态规划
Manacher+枚举
Manacher+树状数组
Manacher+RMQ+枚举
HDU 3068 最长回文
题意:给定一个长度为N(N<=110000)的字符串,求它的最长回文子串的长度。
题解:间隔插入字符'$'后,利用Manacher算法求出每个字符为中心的最长回文子串半径p[i],比较取得最大的p[i]记为Max,则Max-1就是最后的答案。
HDU 3294 Girl's research
题意:在一个长度为N(N<=200000)的字符串中,找到一个最长回文子串,并且输出。
题解:间隔插入字符'$'后,利用Manacher算法求出每个字符为中心的最长回文子串半径p[i],比较取得最大的p[i],输出两端即可。
HDU 4513 吉哥系列故事——完美队形II
题意:在一个长度为N(N<=100000)的整数序列中,找到一个连续的回文序列,且从左到中间保持单调不降。
题解:对于原序列,计算一个辅助数组h,h[i]代表从自己到最左边的单调不增的序列长度。然后在原序列间隔插入-1,利用Manacher算法求出每个数为中心的最长回文子序列的半径p[i],然后对新序列(插入-1的序列)的每个i (i 下标从0开始计) 进行计算求最大值。回文和单调取交集,即满足半径小的那个。
以第i个元素为中心轴,如果这个元素是-1,则两边必然偶对称,min{p[i]-1, 2*h[i/2-1]};否则为奇对称,取值为min{p[i]-1, 2*q[i/2]-1},取所有这些串中的最大值就是答案。
图8
HDU 5677 ztr loves substring
题意:给定N(N<=100)个字符串,要求取出其中K(K<=100)个子串,组成一个长度为L(L <=100)的子串。并且这些子串需要满足的条件是必须回文。可行输出Yes,不可行输出No。
题解:利用Manacher求出每个串的回文子串中长度为 i (i <= 该字符串长度)的个数。然后将这些个数合并。
用dp[i][j][k]表示可以选择的子串长度为1-i的情况下,选择个数为j个,组成长度为k的方案是否可行。然后就是一个O(n)的状态转移,算法最坏复杂度O(n^4)。实际上肯定达不到,因为只要有一个i满足dp[i][K][L]=true,则可以直接输出Yes了。
初始状态dp[0][0][0]=true,其它均为false,然后枚举进行状态转移即可。
HDU 5340 Three Palindromes
题意:给定N(N<=20000)个字符串,求判断是否能够将它切两刀,将它变成三个回文串。
题解:首先还是插入字符'$',求一遍Manacher,然后开始找一些有趣的性质。我们可以分成两种情况讨论:
1)第一种,中间那个串的最长回文子串去掉后,两边的串分别正好是回文串。这种情况比较简单,枚举每个字符作为中心轴,去掉中间那个回文串后,确定两边两个串的中心轴,利用p[i]判断是否满足回文条件即可。
图9
2)第二种,中间那个串的最长回文去掉后,两边无法再构成回文;但是中间串缩短长度后,两边有可能构成回文。如图所示:
图10
这种情况下,我们需要枚举中间那个串的实际长度,这个实际长度最大值为p[i],然后每次减2。由于逐渐减小长度的过程中,必然两个边界是按照中心轴对称的,而边界需要满足分别和原串的两边界对称,所以在减小长度的同时可以比较原串的收尾是否有不相等的情况,一旦不相等就可以退出循环了,复杂度小于O(n^2)。
HDU 5785 Interesting(推荐)
题意:给定一个长度小于等于N(N<=10^6)的字符串S[N],有三元组(i,j,k)满足1<=i<=j<k<len,且S[i...j]和S[j+1...k]均为回文串。现在需要输出所有这些三元组的i*k的和模1000000007。
题解:首先插入字符'$',原串S变成S',然后求一遍Manacher,记录下p[i]。然后就是枚举j,看左边有多少i满足S[i...j]是回文,右边有多少k满足S[j+1...k]是回文。如果用朴素的做法,统计所有i的和以及k的和相乘加和,总的复杂度为O(n^2)。所以我们可以采用树状数组来加速这部分求和。
我们先讨论S[i...j]部分的求和统计。目的是找到以j结尾的所有回文串S[i...j]中i的和。
首先枚举所有非'$'的字符y,然后我们的目的是要找到所有的x<=y,满足g=(x+y)/2,且g+p[g]>y。由于y对应的字符不是'$',根据对称性,x对应的字符也不是'$'。x字符对应原字符串的下标为(x+1)/2。
所以问题转化成求:
图11图12
例如,这个串以第9个字符结尾的回文串的左端点x集合是{1,5,9},经过坐标转换后,原串的所有i求和为sum{ (x+1)/2 | x=1,5,9 }=1+3+5=9。
图13
然而,实际计算的时候,所有i的求和只能通过O(n)的遍历,这样总的复杂度会变成O(n^2),为了能够用树状数组进行统计,我们引入了一个中间变量,即所有这些回文串中心g。
因为g=(x+y)/2,所以x = 2g-y,于是有:
图14
sum{g}代表所有满足条件的回文的中心轴数值的和,cnt代表有多少个回文串。那么,我们可以用两个树状数组分别统计这两个值,如图所示:
图15
维护两个树状数组,一个统计cnt,一个统计sum。计算cnt的时候,对于每个p[g],在g位置+1,g+p[g]位置-1;计算sum的时候,对于每个p[g],在g位置+g,在g+p[g]位置-g。
这样对于每个j,就能计算所有i的和了,用同样方法计算所有k的和,然后相乘取模即可。算法复杂度O(nlogn)。当然还有一个更加高效的算法,每次插入和计算求和的时候都是递增的进行,所以可以不需要树状数组,直接一个O(n)的数组维护前缀和即可,总算法复杂度可以达到O(n)。
HDU 5371 Hotaru's problem
题意:给定一个N(N<=10^5)个元素的数组。定义N-序列为3K个元素,前K个和中间K个完全对称,并且前K个和最后K个元素完全相等。求N个元素中最长的N-序列的长度。
题解:由题意得,前K个和中间K个构成偶回文,且中间K个和最后K个也构成偶回文。
首先,数据量大的情况下,用G++提交利用输入外挂,可以节省500MS时间。然后就是各种乱搞,在相邻元素中间插入-1后跑一边Manacher,然后就是枚举以每个-1为对称轴的回文串,记录最优解Max。
图16
枚举每个-1的对称轴i,然后在p[i]覆盖范围内找右边串的对称轴j,算法复杂度O(n^2)。但是,这里可以做很多优化,比如在当前可能的轴i和轴j之间如果没有一个k满足k-p[k]+1<=i,则不需要枚举j直接跳出循环。这一步可以用RMQ来完成。如果当前情况下能够求得的最优解已经小于等于Max,也不需要继续往下枚举。
还有一个比较依赖数据的剪枝,就是3*(Max+1)>N的时候退出整个循环(用于所有元素均相等的情况)。