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算法我暂时还没有看,后面弄懂了以后再补充。