1.字串转换
1.1 转换 s ——> s’
转换的目的主要是避免奇偶讨论,两端的@和¥避免越界。
e.g.
1 2 3 2 1 -—-> @#1#2#3#2#1#¥
1 2 2 1 ——> @#1#2#2#1#¥
1.2 性质
定义:字串“1”半径为0,“#1#”半径为1,“#1#2#”半径为2
半径的定义不同,则性质表达式略有偏差
s 的长度 = s’ 的半径
s 的半径 = (s’ 的半径+1) / 2
2. 主算法思路
A. 定义一个大小与 s’ 长度相同的数组 r,作用第3步介绍。
B. 主循环:从左至右遍历 s’ 中各个字符(不包括两端),分别将 s’[i] 作为子串的中心点。
C. 循环内部:计算 r[i]。 r[i] 用于记录以 s’[i] 为中心点的最大回文子串的半径。
D. 得到 r 之后,无论是求最长回文子串还是回文子串总数等都很容易,不再详细展开。
3. 核心步骤
算法的核心步骤是计算r[i]
3.1 右端最靠右的最长回文子串 s_right
当进行完第 i 步迭代之后,s’[i]之前的每一个字符作为中心均对应有一个最长回文子串,其半径已经得出,分别为 r[0], r[1], …, r[i-1]。在这些最长回文子串中,我们在后面的计算中需要利用右端最靠右的那个最长回文子串,暂且命名为s_right。
于是定义两个变量 center, right,用于记录 s_right 的中心位置和最右端位置。每完成一步迭代之后(即计算完一个 r[i])进行一次更新。
3.2 分情况计算r[i]
3.2.1利用s_right对称性
A. 当 i < right
需要利用 s_right 的对称性。
不妨令 s’[i] 关于 center 对称的字符为 s’[j],其中 j = 2 * center - i。
a. 当 right - i >= r[j]
s’[j] 对应的最长回文子串完全在 s_right 内部。
根据 s_right 的对称性, r[i] >= r[j]
r[i] 的准确值还无法确定。
注:个人认为s’[i]对应的最长回文子串并不一定完全在 s_right 内部,很多博客上说 r[i] = r[j] 是不准确的,以s’[i]对应的最长回文子串右端完全可能越过right
b. 当 right - i < r[j]
s’[j] 对应的最长回文子串部分在 s_right 内部。
根据 s_right 的对称性, r[i] >= right - i。
r[i] 的准确值还无法确定。
B. 当 i >= right
无法利用 s_right 的对称性
3.2.2 常规扩展判断
利用完对称性后,还需要采用常规方法,向中心点两侧扩展,并进行判断。
4. C++程序
计算回文子串个数总和
class Solution {
public:
int countSubstrings(string s) {
// s transfer to str
string str = "@";
for(auto a : s){
str += '#';
str += a;
}
str += "#$";
// r[i] record the max radius when str[i] as the center
vector<int> r(str.size(), 0);
int center = 0, right = 0; // 记录当前s_right的中心位置和最右端位置
// 以str[i](从i = 1至i = r.size() - 2)作为中心点,计算r[i]
for(int i = 1; i <= r.size() - 2; ++i){
// 利用对称性减少一部分两端比较的操作,核心步骤
if(i < right){
r[i] = min(right-i, r[2*center-i]);
}
// 常规两端比较
while(str[i+r[i]+1] == str[i-r[i]-1]){
++r[i];
}
// update center,right
if(i+r[i] > right){
center = i;
right = i + r[i];
}
}
// 计算回文子串之和
int sum = 0;
for(auto a : r){
sum += (a + 1) / 2;
}
return sum;
}
};