Bootstrap

LeetCode回文串问题汇总

1、最长回文子串 (LC-5)

题目要求在已知字符串中找到最长的连续回文子串。必须掌握的解题方法有动态规划法和中心扩展法。
详细题解

解法1:动态规划法

状态转移方程为:P(i, j) = P(i+1, j−1) ∧ (Si == Sj);边界条件为:P(i, i) = true,P(i, i+1) = (Si == Si+1)。

class Solution {
public:
    string longestPalindrome(string s) {
        int n = (int) s.size();
        vector<vector<int>> dp(n, vector<int>(n));
        string ans;
        // 子串长度为l+1,l从1-n
        for (int l = 0; l < n; ++l) {
            for (int i = 0; i + l < n; ++i) {
                int j = i + l;
                if (l == 0) {
                    dp[i][j] = 1;
                } else if (l == 1) {
                    dp[i][j] = (s[i] == s[j]);
                } else {
                    dp[i][j] = (dp[i + 1][j - 1] && (s[i] == s[j]));
                }
                if (dp[i][j] && l + 1 > ans.size()) {
                    ans = s.substr(i, l + 1);
                }
            }
        }
        return ans;
    }
};

解法2:中心扩展法

边界情况对应的子串实际上就是我们扩展出的回文串的「回文中心」。枚举所有的「回文中心」并尝试扩展,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。

class Solution {
public:
    pair<int, int> expandAroundCenter(string& s, int left, int right) {
        while (left >= 0 && right < s.size() && s[left] == s[right]) {
            --left;
            ++right;
        }
        return {left + 1, right - 1};
    }
    string longestPalindrome(string s) {
        int start = 0, end = 0;
        for (int i = 0; i < s.size(); ++i) {
            auto [left1, right1] = expandAroundCenter(s, i, i);
            auto [left2, right2] = expandAroundCenter(s, i, i + 1);
            if (right1 - left1 > end - start) {
                start = left1;
                end = right1;
            }
            if (right2 - left2 > end - start) {
                start = left2;
                end = right2;
            }
        }
        return s.substr(start, end - start + 1);
    }
};

2、最长回文子序列 (LC-516)

题目要求在已知字符串中找到最长的回文子序列。由于回文子序列可能不连续,这道题不能用中心扩展法求解,只能考虑用动态规划法求解。
详细题解

解法:动态规划法

定义dp[i][j]表示在子串s[i…j]中最长回文子序列的长度。状态转移方程为:如果s[i] == s[j],则dp[i][j] = dp[i+1][j-1] + 2;如果s[i] != s[j],则s[i]和s[j]不可能同时出现在s[i…j]的最长回文子序列中,dp[i][j] = max(dp[i+1][j], dp[i][j-1])。边界条件为:dp[i][i] = 1,dp[i][j] = 0(i > j)。注意要根据状态转移方程确定遍历顺序。

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = (int) s.size();
        // 将dp数组全部初始化为0,就不用处理i>j的情况
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for (int i = 0; i < n; ++i) {
            dp[i][i] = 1;
        }
        // 反向遍历
        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                // 状态转移方程
                if (s[i] == s[j]) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][n - 1];
    }
};

3、回文子串 (LC-647)

题目要求计算已知字符串中共有多少个回文子串,返回回文子串的个数。本题要求找出所有的回文子串,而第5题要求找出最长的回文子串,思路差不多,必须掌握的解题方法有动态规划法和中心扩展法。
详细题解

解法1:动态规划法

用状态dp[i][j]表示字符串s在[i,j]区间的子串是否是回文串。注意遍历顺序,计算dp[i][j]需要知道dp[i-1][j+1],因此将外层循环设置为列循环,保证计算当前列时前一列已经确定。

class Solution {
public:
    int countSubstrings(string s) {
        int ans = 0;
        int n = (int) s.size();
        vector<vector<bool>> dp(n, vector<bool>(n, false));
        for (int j = 0; j < n; ++j) {
            for (int i = 0; i <= j; ++i) {
                if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    ans++;
                }
            }
        }
        return ans;
    }
};

解法2:中心扩展法

总共有2*len-1个中心点,包括len个单字符中心点和len-1个双字符中心点。可以通过center来确定left和right的初始位置下标,即left=center/2,right=left+center%2。当center%2=0时,left和right指向同一位置,回文串的长度为奇数;当center%2=1时,left和right指向不同位置,回文串的长度为偶数。

class Solution {
public:
    int countSubstrings(string s) {
        int ans = 0;
        int n = (int) s.size();
        for (int center = 0; center < 2 * n - 1; ++center) {
            int left = center / 2;
            int right = left + center % 2;
            while (left >= 0 && right < n && s[left] == s[right]) {
                ans++;
                left--;
                right++;
            }
        }
        return ans;
    }
};

稍稍修改上述算法,就可以用于第5题。

class Solution {
public:
    string longestPalindrome(string s) {
        string result = "";
        int len = (int) s.size();
        for (int center = 0; center < 2 * len - 1; ++center) {
            int left = center / 2;
            int right = left + center % 2;
            while (left >= 0 && right < len && s[left] == s[right]) {
                if (right - left + 1 > result.size()) {
                    result = s.substr(left, right - left + 1);
                }
                left--;
                right++;
            }
        }
        return result;
    }
};

4、由子序列构造的最长回文串的长度 (LC-1771)

题目要求在两个字符串中分别取非空子序列构建最长回文子串。可以等价为将两个字符串拼成一个大字符串,并对其求解最长回文子序列的问题,难点在于要保证从word1和word2中取的子序列都非空。
详细题解

解法:动态规划法

我们再来回顾一下求解最长回文子序列的DP算法,当s[i]==s[j]时,s[i]和s[j]会同时出现在子串s[i…j]的最长回文子序列中,此时需要更新最长长度;当s[i]!=s[j]时,s[i]和s[j]不会同时出现在子串s[i…j]的最长回文子序列中,此时不需要更新最长长度。本题只需要增加一个约束条件,即在每一次更新最长长度时都保证s[i]和s[j]分别来自word1和word2,就能保证从word1和word2中取的子序列都非空。

class Solution {
public:
    int longestPalindrome(string word1, string word2) {
        int res = 0;
        int m = (int) word1.size();
        string word = word1 + word2;
        int n = (int) word.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for (int i = 0; i < n; ++i) {
            dp[i][i] = 1;
        }
        for (int i = n - 1; i >= 0; --i) {
            for (int j = i + 1; j < n; ++j) {
                if (word[i] == word[j]) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                    if (i < m && j >= m) {
                        res = max(res, dp[i][j]);
                    }
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return res;
    }
};

5、分割回文串 (LC-131)

给你一个字符串s,请你将s分割成一些子串,使每个子串都是回文串。返回s所有可能的分割方案。
详细题解

解法1:回溯算法+双指针法判断回文串

要找到所有可能的解,就要使用回溯算法,用DFS来实现。此外,用双指针法判断划分的每一个子串是否是回文串。时间复杂度为O(N⋅2N)。

class Solution {
private:
    // 双指针:判断子串是否是回文串
    bool checkPalindrome(string s, int left, int right) {
        while (left < right) {
            if (s[left] != s[right]) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
    void dfs(string s, int index, int len, vector<string>& path, vector<vector<string>>& res) {
        if (index == len) {
            res.push_back(path);
            return;
        }
        for (int i = index; i < len; ++i) {
            if (!checkPalindrome(s, index, i)) {
                continue;
            }
            path.push_back(s.substr(index, i - index + 1));
            dfs(s, i + 1, len, path, res);
            path.pop_back();
        }
    }
public:
    vector<vector<string>> partition(string s) {
        int len = (int) s.size();
        vector<vector<string>> res;
        if (len == 0) {
            return res;
        }
        vector<string> path;
        dfs(s, 0, len, path, res);
        return res;
    }
};

解法2:回溯算法+动态规划法预先判断回文串

用动态规划法预先判断所有子串是否是回文串的时间复杂度为0(N2),所以总的时间复杂度为O(N2+2N) = O(2N)。

class Solution {
private:
    void dfs(string s, int index, int len, vector<vector<bool>>& dp, vector<string>& path, vector<vector<string>>& res) {
        if (index == len) {
            res.push_back(path);
            return;
        }
        for (int i = index; i < len; ++i) {
            if (dp[index][i]) {
                path.push_back(s.substr(index, i - index + 1));
                dfs(s, i + 1, len, dp, path, res);
                path.pop_back();
            }
        }
    }
public:
    vector<vector<string>> partition(string s) {
        int len = (int) s.size();
        vector<vector<string>> res;
        if (len == 0) {
            return res;
        }
        // 动态规划:预先计算所有子串是否是回文串
        vector<vector<bool>> dp(len, vector<bool>(len, false));
        for (int right = 0; right < len; ++right) {
            for (int left = 0; left <= right; ++left) {
                if (s[left] == s[right] && (right - left <= 2 || dp[left + 1][right - 1])) {
                    dp[left][right] = true;
                }
            }
        }
        vector<string> path;
        dfs(s, 0, len, dp, path, res);
        return res;
    }
};

6、分割回文串 II (LC-132)

给你一个字符串s,请你将s分割成一些子串,使每个子串都是回文串。返回符合要求的最小分割次数。如果直接在131题的基础上找出最短路径并减1,会超出时间限制;求最小的分割次数,还是考虑使用动态规划法。
详细题解

解法1:动态规划法+双指针法判断回文串

用dp[i]表示将前缀子串s[0,i]分割成若干个回文串所需要的最小分割次数。
如果s[0,i]本身就是回文串,则dp[i]=0,否则dp[i]=min(dp[j]+1 for j in range(i) if s[j+1,i]是回文串);dp[0]=0。

class Solution {
public:
    // 如果s[0,i]本身就是回文串,则dp[i]=0,否则dp[i]=min(dp[j]+1 for j in range(i) if s[j+1,i]是回文串);dp[0]=0
    // dp[i]表示将前缀子串s[0,i]分割成若干个回文串所需要的最小分割次数
    int minCut(string s) {
        int len = (int) s.size();
        if (len < 2) {
            return 0;
        }
        vector<int> dp(len);
        // 初始化为最多分割次数
        for (int i = 0; i < len; ++i) {
            dp[i] = i;
        }
        for (int i = 1; i < len; ++i) {
            if (checkPalindrome(s, 0, i)) {
                dp[i] = 0;
                continue;
            }
            for (int j = 0; j < i; ++j) {
                if (checkPalindrome(s, j + 1, i)) {
                    dp[i] = min(dp[i], dp[j] + 1);
                }
            }
        }
        return dp[len - 1];
    }
private:
    bool checkPalindrome(string s, int left, int right) {
        while (left < right) {
            if (s[left] != s[right]) {
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
};

解法2:动态规划法+动态规划法预先判断回文串

用动态规划法预先判断所有子串是否是回文串,时间复杂度为0(N2),这样在DFS中就可以通过O(1)的时间复杂度判断一个子串是否是回文串。

class Solution {
public:
    int minCut(string s) {
        int len = (int) s.size();
        if (len < 2) {
            return 0;
        }
        vector<int> dp(len);
        // 初始化为最多分割次数
        for (int i = 0; i < len; ++i) {
            dp[i] = i;
        }
        vector<vector<bool>> check(len, vector<bool>(len, false));
        for (int right = 0; right < len; ++right) {
            for (int left = 0; left <= right; ++left) {
                if (s[left] == s[right] && (right - left <= 2 || check[left + 1][right - 1])) {
                    check[left][right] = true;
                }
            }
        }
        for (int i = 1; i < len; ++i) {
            if (check[0][i]) {
                dp[i] = 0;
                continue;
            }
            for (int j = 0; j < i; ++j) {
                if (check[j + 1][i]) {
                    dp[i] = min(dp[i], dp[j] + 1);
                }
            }
        }
        return dp[len - 1];
    }
};

7、让字符串成为回文串的最少插入次数 (LC-1312)

本题是第516题的子题,可以转化为求解字符串中不属于最长回文子序列的字符个数;只要插入这些字符,就能使整个字符串成为回文串,因此用总长度减去最长回文子序列长度就是最少插入次数。
详细题解

解法:动态规划法

参考516题的解法一,注意遍历顺序,最后返回n-dp[0][n-1]。

class Solution {
public:
    int minInsertions(string s) {
        int n = (int) s.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        for (int i = n - 1; i >= 0; --i) {
            dp[i][i] = 1;
            for (int j = i + 1; j < n; ++j) {
                if (s[i] == s[j]) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return n - dp[0][n - 1];
    }
};

总结

对于回文串问题就先整理到这里,LC上相关的问题还有很多。总的来说,如果题目不要求返回所有可能的解,一般优先考虑动态规划法,如果是求连续的回文子串,则还可以使用中心扩展法。Manacher算法我暂时还没有看,后面弄懂了以后再补充。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;