回文子串

回文子串

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串,如aabaac中的aba和aabaa是两个回文子串。

思路一:中心拓展

  1. 回文串分两种:
    • 奇数长度:中间的字母不用管,两侧的各自相等。
    • 偶数长度:没有中间的字母,两侧的各自相等。
  2. 针对每个字母,作为奇数长度的回文串的中间的字母,进行扩散检查
  3. 针对相邻的两个字母,作为偶数长度的回文串,进行扩散检查

这样便可以在一次遍历中进行扩散检查,对不符合的情况立刻剪枝。
在这里插入图片描述

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算法

  1. 先对字符串进行预处理,两个字符之间加上特殊符号#
  2. 然后遍历整个字符串,用一个数组来记录以该字符为中心的回文长度,为了方便计算右边界,我们在数组中记录长度的一半(向下取整)。这一次不通过中心扩展全部取完,后面再说
  3. 每一次遍历的时候,如果该字符在已知回文串最右边界的覆盖下,那么就**计算其相对最右边界回文串中心对称的位置,**得出已知回文串的长度
    len[i] = min(mx - i ,len[2 * id - i])
  4. 判断该长度和右边界,如果达到了右边界,那么需要进行中心扩展探索。如果第3步该字符没有在最右边界的范围下,则直接进行中心扩展探索。进行中心扩展探索的时候,同时又更新右边界。
  5. 最后得到最长回文后,去掉其中的特殊符号即可。
//马拉车算法
//首先我们在每个字符中间和字符串前后添加'#'
//然后定义数组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;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值