目录
5. 最长回文子串
给你一个字符串
s
,找到s
中最长的回文子串
。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
示例 1:
输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd" 输出:"bb"
提示:
1 <= s.length <= 1000
s
仅由数字和英文字母组成
中心扩散
1.
首先对字符串进行处理,例如str="abcd",处理后str="#a#b#c#d#"。
2.
记录处理后字符串中最长的回文串的区间,最长回文串最左边的元素一定是"#",最右边的元素也一定是"#"。
3.
在处理后的字符串中,如果字符是原字符串中复制过来的,下标对应关系i/2就是原字符串对应的下标。
class Solution {
public:
// 定义函数寻找最长回文子串
string longestPalindrome(string s) {
// 调用manacherString函数处理原始字符串,插入特殊字符'#'以方便回文查找
string str = manacherString(s);
// 初始化最长回文子串的起始和结束位置
int start = 1, end = 0;
// 遍历处理后的字符串
for (int i = 0; i < str.size(); i++) {
// 初始化当前位置的左右指针
int left = i - 1, right = i + 1;
// 当左右指针在字符串范围内且对应字符相等时,向两边扩展
while (left >= 0 && right < str.size() && str[left] == str[right]) {
left--, right++;
}
// 扩展结束后,调整左右指针位置
left++, right--;
// 如果新找到的回文长度大于之前记录的最长回文长度,更新最长回文的起始和结束位置
if (end - start + 1 < right - left + 1) {
start = left, end = right;
}
}
// 计算原字符串中回文子串的起始位置和长度
int start1 = (start + 1) / 2;
int end1 = (end - 1) / 2;
int length = end1 - start1 + 1;
// 从原字符串中截取最长回文子串并返回
return s.substr(start1, length);
}
// 定义函数生成处理后的字符串,其中每两个字符间插入'#',首尾也为'#'
string manacherString(string s) {
// 创建新字符串,长度为原字符串的两倍加一
string str(2 * s.size() + 1, '\0');
int index = 0;
// 遍历新字符串,插入'#'和原字符串的字符
for (int i = 0; i < str.size(); i++) {
// 判断索引奇偶,决定插入'#'或原字符串字符
str[i] = (i & 1) == 0 ? '#' : s[index++];
}
// 返回处理后的字符串
return str;
}
};
Manacher
r是遍历过的最右边的回文边界,r本身不在回文区间内,而是最右边的元素下标+1。
class Solution {
public:
// 定义查找最长回文子串的函数
string longestPalindrome(string s) {
// 通过manacherString函数转换原始字符串,插入'#'以便处理
string str = manacherString(s);
// 创建一个数组pArr,存放每个字符作为中心时的最大回文半径
vector<int> pArr(str.size());
// r表示当前遍历过的回文右边界,c表示对应的中心位置
int r = -1, c = -1;
// maxc记录最大回文半径对应的中心位置,maxr记录最大回文半径
int maxc = INT_MIN, maxr = INT_MIN;
// 遍历处理后的字符串
for (int i = 0; i < str.size(); i++) {
// 初始化pArr[i],如果当前位置i在r内,利用对称性,否则初始化为1
pArr[i] = i < r ? min(r - i, pArr[2 * c - i]) : 1;
// 向两边扩展以检查更长的回文
while (i - pArr[i] >= 0 && i + pArr[i] < str.size()) {
if (str[i - pArr[i]] == str[i + pArr[i]]) {
pArr[i]++;
} else {
break;
}
}
// 如果通过扩展更新了右边界,同时更新中心位置
if (i + pArr[i] > r) {
r = i + pArr[i];
c = i;
}
// 更新记录的最大回文半径及其中心位置
if (pArr[i] > maxr) {
maxr = pArr[i];
maxc = c;
}
}
// 计算最长回文子串在处理后的字符串中的起始位置和结束位置
int left = maxc - pArr[maxc] + 1;
int right = maxc + pArr[maxc] - 1;
// 转换回原字符串的索引
int leftindex = (left + 1) / 2;
int rightindex = (right - 1) / 2;
// 计算长度
int length = rightindex - leftindex + 1;
// 返回原字符串中的最长回文子串
return s.substr(leftindex, length);
}
// 函数用于生成处理后的字符串,插入'#'以方便处理
string manacherString(string s) {
// 创建新字符串,长度为原字符串的两倍加一
string str(2 * s.size() + 1, '\0');
int index = 0;
// 在新字符串中插入'#'和原始字符
for (int i = 0; i < str.size(); i++) {
str[i] = (i & 1) == 0 ? '#' : s[index++];
}
// 返回处理后的字符串
return str;
}
};
214. 最短回文串(最长前缀回文串)
给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为
回文串
。找到并返回可以用这种方式转换的最短回文串。
示例 1:
输入:s = "aacecaaa" 输出:"aaacecaaa"
示例 2:
输入:s = "abcd" 输出:"dcbabcd"
提示:
0 <= s.length <= 5 * 10(4)
s
仅由小写英文字母组成
中心扩散(超时)
class Solution {
public:
// 主函数,用于找到并返回能构成最短回文的字符串
string shortestPalindrome(string s) {
if(s.size()==0||s.size()==1) return s; // 如果字符串为空或者长度为1,直接返回原字符串
string str = getStr(s); // 将原字符串转换为新的格式,便于处理
int maxlen = 0; // 初始化最长回文子串的长度为0
for (int i = 0; i < str.size(); i++) { // 遍历新格式的字符串
int left = i - 1, right = i + 1; // 初始化左右指针
while (left >= 0 && right < str.size()) { // 当左右指针没有越界时
if (str[left] == str[right]) { // 如果左右字符相等
left--, right++; // 向外扩展
} else {
break; // 否则停止当前扩展
}
}
if (left == -1) { // 如果左指针到达了字符串的最左边
left++, right--; // 修正左右指针
left = left / 2; // 将字符位置转换回原始字符串的位置
right = (right - 1) / 2; // 同上
if (right - left + 1 > maxlen) // 如果找到了更长的回文子串
maxlen = right - left + 1; // 更新最长回文子串的长度
}
}
string str1 = s.substr(maxlen); // 从原始字符串截取非回文部分
reverse(str1.begin(), str1.end()); // 翻转非回文部分
s = str1 + s; // 将翻转后的字符串添加到原始字符串前面
return s; // 返回结果
}
// 辅助函数,用于将原字符串转换为特殊格式(在每个字符之间插入'#')
string getStr(string s) {
string str(2 * s.size() + 1, '\0'); // 创建一个新字符串,长度为原长度的两倍加一
int index = 0; // 初始化索引
for (int i = 0; i < str.size(); i++) { // 遍历新字符串的每个位置
str[i] = (i & 1) == 0 ? '#' : s[index++]; // 偶数位置放'#',奇数位置放原字符串的字符
}
return str; // 返回转换后的字符串
}
};
Manacher
1.
如何用Manacher寻找最长前缀回文子串?
只需要从右往左计算parr数组即可,当l表示回文串左边界越界的时候,此时的c就是最长前缀回文子串的中心,parr[c]就是此时对应的回文半径。
注意,c和parr都是ManacherString的回文中心和回文半径,需要转化到原字符串中还需要一些操作。
2.
ManacherString特点:
-
回文中心c,c+parr[c]是右边界,c+parr[c]-1是回文串中最右边的元素下标。
-
回文中心c,c-parr[c]是左边界,c-parr[c]+1是回文串中最左边的元素下标。
-
回文串中最左边和最右边的元素一定是"#"。
-
回文串中如果是原字符串str复制过来的字符,下标对应关系是index/2,对应的是原字符串的下标。
-
((c+parr[c]-1)-1)/2是原字符串回文区间最左边的下标。
-
((c-parr[c]+1)+1)/2是原字符串回文区间最右边的下标。
class Solution {
public:
// 主函数,用于找到并返回能构成最短回文的字符串
string shortestPalindrome(string s) {
if(s=="") return ""; // 如果字符串为空,直接返回空字符串
string str=manacherString(s); // 对原始字符串进行转换,方便后续处理
vector<int> parr(str.size()); // 初始化存储回文半径的数组
int l=str.size(),c=-1; // 初始化回文左边界l和回文中心c
// Manacher算法核心部分,用于寻找最长前缀回文子串
for(int i=str.size()-1;i>=0;i--){
parr[i]=i>l?min(i-l,parr[2*c-i]):1;
while(i+parr[i]<str.size()&&i-parr[i]>=0){
if(str[i+parr[i]]==str[i-parr[i]]){
parr[i]++;
}else{
break;
}
}
if(i-parr[i]<l){
l=i-parr[i];
c=i;
}
if(l==-1){
break;
}
}
// 计算回文前缀的起始位置和结束位置
int left=((c-parr[c]+1)+1)/2;
int right=((c+parr[c]-1)-1)/2;
string substr(s.size()-(right-left+1),'\0'); // 初始化存储非回文部分的字符串
int index=0;
for(int i=s.size()-1;i>right;i--){
substr[index++]=s[i]; // 提取非回文部分
}
s=substr+s; // 将非回文部分与原始字符串拼接
return s; // 返回结果
}
// 辅助函数,用于将原字符串转换为特殊格式(在每个字符之间插入'#')
string manacherString(string s){
string str(s.size()*2+1,'\0'); // 创建新字符串,长度为原字符串长度的两倍加一
int index=0;
for(int i=0;i<str.size();i++){
str[i]=(i&1)==0?'#':s[index++]; // 偶数位置插入'#',奇数位置插入原字符
}
return str; // 返回转换后的字符串
}
};
KMP
KMP算法怎么计算前缀最长的回文子串?
回文串的逆序是自己本身,str逆序,则A==B,A==B可以理解为KMP找子串。
把str当作是模式串,把str逆序当作是搜索串。
y对应模式串匹配下标,x对应搜索串匹配下标。
x不回退,y会回退。
因此只需要x越界当作是结束条件即可。此时y一定是str前缀最长回文串后一个位置的下标,也就是回文串的长度。
class Solution {
public:
string shortestPalindrome(string s) {
// 如果字符串长度为0或1,它本身就是回文,直接返回
if (s.size() == 0 || s.size() == 1)
return s;
// 将原始字符串反转
string re_s(s);
reverse(re_s.begin(), re_s.end());
// next数组用于KMP算法中,记录字符串的部分匹配表
vector<int> next(s.size());
next[0] = -1, next[1] = 0; // 初始化next数组的前两个值
int i = 2, cn = 0; // i表示当前正在处理的位置,cn表示最长相等前后缀的长度
while (i < next.size()) {
if (s[i - 1] == s[cn]) {
next[i] = cn + 1; // 前后缀匹配,增加长度
i++, cn = cn + 1;
} else if (cn != 0) {
cn = next[cn]; // 前后缀不匹配,回退到前一个可能的匹配位置
} else {
next[i] = 0; // 没有匹配,设置为0
i++;
}
}
int x = 0, y = 0; // x遍历反转后的字符串,y遍历原始字符串
while (x < re_s.size()) {
if (re_s[x] == s[y]) {
x++, y++; // 字符匹配,同时前进
} else if (y != 0) {
y = next[y]; // 不匹配,使用next数组跳转到可能的匹配位置
} else {
x++; // y为0时,只能让x前进
}
}
// 从不匹配的点将剩余的字符串取出,反转后加到原始字符串的前面,形成回文
string str1 = s.substr(y);
reverse(str1.begin(), str1.end());
s = str1 + s; // 将反转后的字符串拼接到原始字符串前,形成最短回文
return s;
}
};
回文串逆序等于本身(超空间)
class Solution {
public:
string shortestPalindrome(string s) {
// 如果字符串长度为0或1,它本身就是回文,直接返回
if(s.size()==0||s.size()==1) return s;
// 将原始字符串反转
string s_ = s;
reverse(s_.begin(), s_.end());
// 初始化最大回文长度为1
int length = 1;
int maxlen=INT_MIN; // 用来记录发现的最长回文子串的长度
// 通过逐渐增加长度来检查最大回文子串
while (length <= s.size()) {
// 如果原始字符串的前缀与反转后的字符串的相应后缀相同
if (s.substr(0, length) == s_.substr(s.size() - length, length)){
// 更新找到的最大长度
if(length>maxlen) maxlen=length;
}
length++; // 增加比较的长度
}
// 如果整个字符串都是回文,则直接返回
if(maxlen==s.size())return s;
// 从最长回文子串结束位置到字符串末尾的部分,反转后添加到字符串前面形成回文
string str = s.substr(maxlen);
reverse(str.begin(), str.end());
s = str + s; // 将反转后的部分拼接到原字符串前面,形成最短的回文串
return s;
}
};
459. 重复的子字符串
给定一个非空的字符串
s
,检查是否可以通过由它的一个子串重复多次构成。示例 1:
输入: s = "abab" 输出: true 解释: 可由子串 "ab" 重复两次构成。
示例 2:
输入: s = "aba" 输出: false
示例 3:
输入: s = "abcabcabcabc" 输出: true 解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
提示:
1 <= s.length <= 10(4)
s
由小写英文字母组成
结论
如果一个字符串可以由子串重复多次构成,那么用两个字符串拼接,一定可以在内部找到该字符串的子串。
内部找到子串的意思是这个子串下标不能从0开始,结尾元素不能是最后一个元素位置。
也就是不能是A或者B。
string内置函数find
class Solution {
public:
bool repeatedSubstringPattern(string s) {
return (s + s).find(s, 1) != s.size();
}
};
KMP
class Solution {
public:
bool repeatedSubstringPattern(string s) {
// 如果字符串长度为1,则不能由更小的子串重复组成
if (s.size() == 1)
return false;
// 使用vector来构建KMP算法的部分匹配表(next数组)
vector<int> next(s.size());
next[0] = -1, next[1] = 0; // 初始化next数组的前两个元素
int cn = 0; // cn用于记录当前匹配的位置
int i = 2; // i用于遍历字符串s构建next数组
while (i < next.size()) {
if (s[i - 1] == s[cn]) {
// 当前字符匹配成功,更新next数组
next[i] = cn + 1;
i++, cn = cn + 1;
} else if (cn != 0) {
// 匹配失败,通过next数组回退
cn = next[cn];
} else {
// 匹配失败,且无法回退,则设置为0,继续向前遍历
next[i] = 0;
i++;
}
}
// 将字符串s自身拼接一次
string str = s + s;
int x = 1, y = 0; // x从1开始,因为我们不需要比较第一个字符
while (x < str.size() - 1 && y < s.size()) {
if (str[x] == s[y]) {
// 当前字符匹配,同时向前移动x和y
x++, y++;
} else if (y != 0) {
// 不匹配时,通过next数组回退y
y = next[y];
} else {
// y无法回退时,只移动x
x++;
}
}
// 如果y等于s的长度,表示s可以由自身的一个子串重复构成
if (y == s.size())
return true;
else
return false;
}
};
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!