如何O(1)判断一个子串是否是回文串
先只考虑奇数长度的字符串(以s中的字符为对称中心),若是暴力算法,i遍历字符串s,以i为中心向两边延申,计算两边对称位置字符是否相同,复杂度为O(n*n),
i 0 1 2 3 4 5 6 7 8 9 10 11 12
s c d a b c b a b c b a d e
仍然是只考虑奇数长度的字符串情况,假设知道了以某个字符为中心的最长回文串长度l,那么以该字符为中心长度小于l的奇数长度字符串肯定也是回文串,比如上面以i=6的字符a为中心的最长回文串长度为11,对应"dabcbabcbad",那么其子串“bcbabcb”依然是回文的,也就是说只要知道了以i为中心的最长回文串长度,就能判断以i为中心的任意子串是否回文,假如该字串长度<=最长回文串长度,那么它就回文。
定义一个奇回文串的回文半径hl=(长度+1)/2,即保留回文中心,去掉一侧后的剩余字符串的长度。接着考虑怎么得到字符串s以每个字符为中心对应的最长回文串的回文半径数组halflen。
i 0 1 2 3 4 5 6 7 8 9 10 11 12
s c d a b c b a b c b a d e
maxlen 1 1 1 1 5 1 11 1
假设当前遍历到了i=8的位置,所以要求的是以i=8为中心的最长回文串长度,可以发现前面在i=6位置时对应得最长回文串右边界(i=11)最大,也就是在1-11范围内的字符都关于i=6对称,所以若要求i=8时的最长回文串长度,可以等价于找以i=4对应字符为中心的最长回文串长度,因为区间8-11的字符都可以通过i=6这个镜像对称到1-4,区间5-8同理对称到4-7,所以i=8时左右两边的情况大致和i=4时相似,不同之处在于i=12的位置,因为该位置在之前遍历过程中一直没被遍历到,遍历到的最大右边界为i=11。
i 0 1 2 3 4 5 6 7 8 9 10 11 12
s c b a b c b a b c b a b e
maxlen 1 1 5 1 7 1 11 1
假设i=11和i=5也相等的情况,也就是i=4时的maxlen=7时,此时可以知道以i=8为中心的最长回文串长度至少为7,因为i=12没有可以对称的位置,所以需要继续暴力判断i=12和i=4的位置是否相等,若相等,长度就变为了9,此时遍历到的最大右边界就可以更新到i=12了,对应的回文字符串中心就是i=8的位置。可以看出假设每个位置得到的答案还跟右边界有关,假设右边界到当前i的位置长度小于其对称位置的回文半径,那么应该取右边界到当前i的位置长度,然后继续遍历右边无法对称的区域更新答案;否则i到右边界距离更大,说明右边区域都可以对称,就取对称位置的maxlen作回文半径,但仍然可能需要继续遍历。
上面都是在讨论奇数长度的回文子串,若**为偶数长度,可以变换为奇数情况**,在字符之间加上特殊符号即可,以这特殊符号为中心的回文串的情况就是偶数长度回文串的情况:
s: "cabac" -> t: "#c#a#b#a#c#"
以i为中心向两端延申的过程中,可能会出现越界的情况,所以可以在两端再加上不同的字符,这样就可以不用担心越界情况发生。
t: "^#c#a#b#a#c#$"
halflen[i]表示的就是以t\[i]为中心的最长回文串半径,显然halflen[0]没意义,halflen[1]=1,最后两个'#$'其实也没必要算,所以halflen的长度可以为t.size()-2
这样,就需要考虑s区间[l,r]子串到t字符串中区间的区间转换和回文串长度转换:
若s字符串长度为n,t字符串长度就是2*n+3,找规律得到s中每个的i对应t中的2\*i+2的位置,也就是s区间[l,r]对应t区间[2*l+2,2*r+2],回文中心就是i=l+r+2。那么就可以知道以i为中心的最长回文半径halflen[l+r+2]。
根据回文半径即可判断是否是回文串:
t: "#c#a#b#a#c#"
字符b对应回文半径长度为"b#a#c#"的长度,也就是6,对应的回文串"cabac",长度为5,可以推出最大长度回文串长度 = 最长回文半径 - 1,而当前区间\[l,r]的子串长度为r-l+1,若r-l+1<=halflen\[l+r+2]-1,则该子串就是回文子串。
int main() {
//构造t
string t = "^"; //防止越界
for(char c:s){
t += '#';
t += c;
}
t += "#$";
vector<int> halflen(t.size()-2);
halflen[1] = 1;
int boxR = 0; //最大右边界
int boxM = 0; //最大右边界对应的回文串中心位置
//从t[2]开始遍历
for(int i=2;i<halflen.size();i++){
int hl = 1; //以i为中心的回文半径
if(i<boxR){ //若i位置可以关于中心对称,对称位置为2*boxM-i
//取i到边界距离和对称位置的最长回文半径的最小值
hl = min(halflen[2*boxM-i],boxR-i);
}
//继续暴力遍历无法通过对称判断的部分,也就是右边界之后的部分
while(t[i+hl]==t[i-hl]){
hl++;
//更新右边界和中心位置
boxM = i;
boxR = i+hl;
}
//以i为中心的最长回文半径就是hl
halflen[i]=hl;
}
[5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) **模板题**
[647. 回文子串](https://leetcode.cn/problems/palindromic-substrings/) 计算回文子串个数
[214. 最短回文串](https://leetcode.cn/problems/shortest-palindrome/)
在字符串s前添加字符使s变为回文串,返回求得的最小回文串长度
结合蓝桥杯19718 回文字符串,可知这类在之前字符串前添加字符成为回文字符串的题核心就是找s的前缀回文串,若找到了回文前缀,那么把剩余的后缀反转和s连接就能形成回文串。解题方法也有多种:
1. 可以用字符串哈希,正序逆序哈希值应当相等。
2. manacher找最长回文前缀(要考虑回文子串何时是回文前缀)
3. 若找最长回文前缀,可以用KMP
若s前缀为s1,s反转为~s,s1反转为~s1,那么考虑到 s1 是一个回文串,因此 s1=~s1,s1 同样是 ~s的后缀。这样一来,我们将 s 作为模式串, ~s作为查询串进行匹配。当遍历到~s的末尾时,如果匹配到s 中的第 i 个字符,那么说明 s 的前 i 个字符与 ~s的后 i 个字符相匹配(即相同),s 的前 i 个字符对应 s1,~s后i个字符对应~s1,因为s1=~s1,所以s1是回文串,且这样匹配得到的是最长的回文前缀。