回文子串
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串,如aabaac中的aba和aabaa是两个回文子串。
思路一:中心拓展
- 回文串分两种:
- 奇数长度:中间的字母不用管,两侧的各自相等。
- 偶数长度:没有中间的字母,两侧的各自相等。
- 针对每个字母,作为奇数长度的回文串的中间的字母,进行扩散检查
- 针对相邻的两个字母,作为偶数长度的回文串,进行扩散检查
这样便可以在一次遍历中进行扩散检查,对不符合的情况立刻剪枝。
class Solution {
public:
int countSubstrings(string s) {
int ans = 0;
for (int i = 0; i < s.size(); i++) {
ans += check(s, i, i);
if (i == s.size() - 1) continue;
ans += check(s, i, i + 1);
}
return ans;
}
int check(string& s, int l, int r) {
int cnt = 0;
while (l >= 0 && r < s.size() && s[l] == s[r]) {
l--;
r++;
cnt++;
}
return cnt;
}
};
复杂度分析
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
思路二:Manacher算法
- 先对字符串进行预处理,两个字符之间加上特殊符号#
- 然后遍历整个字符串,用一个数组来记录以该字符为中心的回文长度,为了方便计算右边界,我们在数组中记录长度的一半(向下取整)。这一次不通过中心扩展全部取完,后面再说
- 每一次遍历的时候,如果该字符在已知回文串最右边界的覆盖下,那么就**计算其相对最右边界回文串中心对称的位置,**得出已知回文串的长度
len[i] = min(mx - i ,len[2 * id - i]) - 判断该长度和右边界,如果达到了右边界,那么需要进行中心扩展探索。如果第3步该字符没有在最右边界的范围下,则直接进行中心扩展探索。进行中心扩展探索的时候,同时又更新右边界。
- 最后得到最长回文后,去掉其中的特殊符号即可。
//马拉车算法
//首先我们在每个字符中间和字符串前后添加'#'
//然后定义数组p,记录以每个中心点向两边走的最大步数以达到最大回文长度
//p[i]对应修改后的数组的最大步数,对应原数组的最大回文长度.
//我们更新最用的下标maxRight, 和中心点center.
class Solution {
//求出走的步数
int getStep(string& str, int left, int right) {
while (left >= 0 && right < str.size() && str[left] == str[right]) {
--left;
++right;
}
return (right - left - 2) >> 1;
}
public:
int countSubstrings(string s) {
int center = -1, maxRight = -1;
int ans = 0;
string str = "#";
for (auto& ch : s) {
str += ch;
str += "#";
}
int size = str.size();
vector<int> p(size);
for (int i = 0; i < size; ++i) {
int step = -1;
if (i < maxRight) {
int val = 2 * center - i;
int minStet = min(p[val], maxRight - i);
step = getStep(str, i - minStet, i + minStet);
} else {
step = getStep(str, i, i);
}
p[i] = step;
if (i + p[i] > maxRight) {
maxRight = i + p[i]; //更新最右边的下标
center = i; //更新中心值
}
ans += (step + 1) >> 1; //step为原始字符串的回文长度,+1除以2即为回文字符串的个数
}
return ans;
}
};
最长回文子串
给你一个字符串s,找到s中最长的回文子串。
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
思路一:双指针
首先确定回文串,就是找中心然后想两边扩散看是不是对称的就可以了。
class Solution {
public:
int left = 0;
int right = 0;
int maxLength = 0;
string longestPalindrome(string s) {
int result = 0;
for (int i = 0; i < s.size(); i++) {
extend(s, i, i, s.size()); // 以i为中心
extend(s, i, i + 1, s.size()); // 以i和i+1为中心
}
return s.substr(left, maxLength);
}
void extend(const string& s, int i, int j, int n) {
while (i >= 0 && j < n && s[i] == s[j]) {
if (j - i + 1 > maxLength) {
left = i;
right = j;
maxLength = j - i + 1;
}
i--;
j++;
}
}
};
思路二:Manacher
- 最左边的箭头上指向的a这个字符,它可以向左、向右扩展1位,也就是变成#a#这样的回文串,这个字符串总长度是3,以a为中心可以向两边扩展1位(不包含a字符本身),所以这个字符的回文半径是1.
- 原始字符是xabbcbbay,它的长度为9。
而经过处理后的字符串#x#a#b#b#c#b#b#a#y#,它的长度为19,也就是说 原字符串长度 * 2 + 1 就等于 处理后的字符串长度。
现在计算回文半径时,这个长度不包括箭头指向的字符本身。那么计算出来的回文半径最大值,就等于原始字符串中最长回文串的长度,即上图中间那个箭头指向的字符c其回文半径长度为7(对应的数组下标是9),而原始字符串中最长回文串为abbcbba,其长度也是7。
于是我们遍历处理后的字符串,当遍历到下标9时,就会得到计算出最长回文半径7,通过下标和回文半径就可以更新start(原始字符最长回文串的起始位置),因为题目要求要把子串打印出来。
i:当前遍历到的数组下标
armLen:以i为中心,计算出的回文半径
start = (i - armLen) / 2
maxLen = armLen
等字符串全部遍历完,我们根据start和maxLen这两个变量,就可以从原始字符串s中截取出最长回文子串:
s[start : start + maxLen]
由于每遍到一个字符,都需要往左/右进行探测,故这种方式的时间复杂度是O(N^2)
空间复杂度是O(1)。
上面的代码在计算过程中,没有用到辅助空间,计算#x#a#b#b#c#b#b#a#y#中每个字符的回文半径时,都需要往两边扩展一点点计算。
实际上,我们可以将之前计算过的回文半径保存到一个数组中,后面再计算某个字符的回文半径时,就可以利用到之前已经计算过的值。
分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例:
输入: “aab”
输出:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]
思路:回溯法
class Solution {
private:
vector<vector<string>> result;
vector<string> path; // 放已经回文的子串
void backtracking (const string& s, int startIndex) {
// 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
if (startIndex >= s.size()) {
result.push_back(path);
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是回文子串
// 获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else { // 不是回文,跳过
continue;
}
backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经填在的子串
}
}
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
public:
vector<vector<string>> partition(string s) {
result.clear();
path.clear();
backtracking(s, 0);
return result;
}
};