【五十五】【算法分析与设计】5. 最长回文子串,214. 最短回文串(最长前缀回文串),459. 重复的子字符串,Manacher算法应用,中心扩散,KMP算法求前缀最长回文串

目录

5. 最长回文子串

中心扩散

Manacher

214. 最短回文串(最长前缀回文串)

中心扩散(超时)

Manacher

KMP

回文串逆序等于本身(超空间)

459. 重复的子字符串

结论

string内置函数find

KMP

结尾


 

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;
    }
};

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

妖精七七_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值