本文主要针对LeetCode上关于回文字符串的算法题进行总结。
总体而言,LeetCode上关于回文字符串的算法题可以分为以下几类:
(1)在已知的字符串串找到最长的回文字符串
(2)修改现有的字符串得到回文字符串
(3)分割现有的字符串得到回文字符串
涉及题目:
5. 最长回文子串
131. 分割回文串
132. 分割回文串 II
214. 最短回文串
516. 最长回文子序列
647. 回文子串
1278. 分割回文串 III
1312. 让字符串成为回文串的最少插入次数
1745. 回文串分割 IV
1771. 由子序列构造的最长回文串的长度
常用方法:中心扩展,动态规划,回溯,Manacher马拉车算法
文章目录
在已知的字符串串找到最长的回文字符串
找到最长的回文子串最基本的就是两种,一则子串连续,二则子串不连续。其他最长回文子串都是关于这两者的变体。
最长回文子串(子串连续)
题目链接:5. 最长回文子串
之前整理过这个题目的4种解法的C++和Python版本,常见的方法有暴力法,中心扩展法,动态规划法,Manacher(马拉车)法。其中中心扩展法最容易理解,也最推荐,动态规划比较经典需要掌握,而马拉车算法效率最高,学有余力的学习。
这里讲解一下中心扩展法和动态规划法
中心扩展法
依次遍历每一位作为中心,然后左右指针往两边走,找到每个中心对应的最长子字符串。
class Solution {
public:
string longestPalindrome(string s) {
string ans = "", tempans;
for(int i = 0; i < s.length(); ++ i){
tempans = expandToCenter(s, i, i);
if(tempans.length() > ans.length()) ans = tempans;
tempans = expandToCenter(s, i, i+1);
if(tempans.length() > ans.length()) ans = tempans;
}
return ans;
}
string expandToCenter(string &s, int left, int right){
while(left >= 0 && right < s.length() && s[left] == s[right]){
-- left;
++ right;
}
return s.substr(left+1, right-left-1);
}
};
动态规划法
首先是容易理解的二维dp
/*
dp[i][j]:对于子字符串s[i...j]是否为回文串
s[i] == s[j] : dp[i][j] = dp[i+1][j-1]
Base: dp[i][i] = true; dp[i][i+1] = (s[i] == s[i+1])
*/
class Solution {
public:
string longestPalindrome(string s) {
int n = s.length();
if(n < 2) return s;
vector<vector<bool>> dp(n, vector<bool>(n, false));
string ans = s.substr(0, 1);
for(int i = 0; i < n; ++ i)
dp[i][i] = true;
for(int i = n-2; i >= 0; -- i){
for(int j = i+1; j < n; ++ j){
if(j == i+1)
dp[i][j] = s[i] == s[i+1];
else if(s[i] == s[j])
dp[i][j] = dp[i+1][j-1];
if(dp[i][j] && j - i + 1 > ans.length())
ans = s.substr(i, j-i+1);
}
}
return ans;
}
};
对二维dp进行状态压缩
class Solution {
public:
string longestPalindrome(string s) {
int n = s.length();
if(n < 2) return s;
vector<bool> dp(n, true);
string ans = s.substr(0, 1);
for(int i = n-2; i >= 0; -- i){
bool pre = true;
for(int j = i+1; j < n; ++ j){
bool temp = dp[j];
if(s[i] == s[j])
dp[j] = pre;
else
dp[j] = false;
if(dp[j] && j - i + 1 > ans.length())
ans = s.substr(i, j-i+1);
pre = temp;
}
}
return ans;
}
};
最长回文子串(子串不连续)
题目链接:516. 最长回文子序列
这道题里面中心扩展法就失效了,主要是动态规划法
动态规划法
首先是容易理解的二维dp
/*
dp[i][j]:对于子字符串s[i...j],最长的回文子序列长度
s[i] == s[j] : dp[i][j] = dp[i+1][j-1] + 2
s[i] != s[j] : dp[i][j] = max(dp[i+1][j], dp[i][j-1])
*/
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.length();
vector<vector<int>> dp(n, vector<int>(n, 0));
dp[0][0] = 1;
for(int i = n-2; 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 dp[0][n-1];
}
};
状态压缩后
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.length();
vector<int> dp(n, 1);
for(int i = n-2; i >= 0; -- i){
int pre = 0;
for(int j = i+1; j < n; ++ j){
int temp = dp[j];
if(s[i] == s[j])
dp[j] = pre + 2;
else
dp[j] = max(dp[j], dp[j-1]);
pre = temp;
}
}
return dp[n-1];
}
};
应用
这道题其实就在基础问题二的一个变体,把word1和word2合起来找最长不连续回文子序列,只不过这里要保证word1和word2里面至少取了一个字符。那我们直接对于word1中某个字符c,从右开始找word2中第一个c的位置,然后求这两个位置之间的不连续回文子序列。
有两点优化,一则word1种每种字符只需要针对左边第一个出现的位置找;二则用一个minn保留c出现的最右边的位置,如果当前字符c2第一个出现的位置在minn之前,那么得到的子字符串肯定比之前小,这种情况也不用计算。
class Solution {
public:
int longestPalindrome(string word1, string word2) {
int n = word1.length(), m = word2.length();
string word = word1 + word2;
int minn = n-1, maxn = 0;
vector<bool> visit(26, false);
for(int i = 0; i < n; ++ i){
if(visit[word[i] - 'a']) continue;
int j = n+m-1;
while(j > minn && word[i] != word[j]) -- j;
if(j == minn) continue;
minn = j;
maxn = max(maxn, longestPalindromeSubseq(word, i, j));
}
return maxn;
}
int longestPalindromeSubseq(string s, int left, int right) {
int n = right - left + 1;
vector<int> dp(n, 1);
for(int i = n-2; i >= 0; -- i){
int pre = 0;
for(int j = i+1; j < n; ++ j){
int temp = dp[j];
if(s[i+left] == s[j+left])
dp[j] = pre + 2;
else
dp[j] = max(dp[j], dp[j-1]);
pre = temp;
}
}
return dp[n-1];
}
};
题目链接:214. 最短回文串
表面上看这道题是修改,但是研读一下发现只要首部插入,其实变相就是求原字符串最长的回文前缀(连续)。
这道题用动态规划和中心扩展都会超时,这里用马拉车算法。这块自己还没完全搞懂,等后面消化了再把马拉车好好理一下,先贴一个我觉得讲得最透彻的题解。
具体参考:最短回文串的Manacher解法
题目链接:647. 回文子串
求所有回文子串的个数,这题是问题一(连续回文子串)的应用,在原有的基础上加上计数即可。这里只写一个最方便的中心扩展法。
class Solution {
public:
int ans = 0;
int countSubstrings(string s) {
for(int i = 0; i < s.length(); ++ i){
expandToCenter(s, i, i);
expandToCenter(s, i, i+1);
}
return ans;
}
void expandToCenter(string &s, int left, int right){
while(left >= 0 && right < s.length() && s[left] == s[right]){
-- left;
++ right;
++ ans;
}
}
};
修改现有的字符串得到回文字符串
常见的修改主要包括插入、删除、替换。
而上一节中的找不连续子串其实是删除操作,同理插入也是动态规划,只不过动态规划方程不太一样。
替换没有找到对应题目,或者说如果要求某个字符串需要替换多少个才能构成回文串其实是很简单的,只要左右指针遍历,记录不同的个数即可。第3节的回文串分割3中涉及到替换操作,用dp记录各个子串需要替换的次数,具体参考1278. 分割回文串 III。
这里修改主要讲一下插入操作。
任意位置插入字符
/*
dp[i][j]:对于子字符串s[i...j],构成回文串的最少插入次数
s[i] == s[j}: dp[i][j] = dp[i+1][j-1]
s[i] != s[j]: dp[i][j] = min(dp[i+1][j], dp[i][j-1])
*/
class Solution {
public:
string shortestPalindrome(string s) {
int n = s.length();
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = n-2; i >= 0; -- i){
for(int j = i + 1; j < n; ++ j){
if(s[i] == s[j])
dp[i][j] = dp[i+1][j-1];
else
dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1;
}
}
return dp[0][n-1];
}
};
// 状态压缩
class Solution {
public:
int minInsertions(string s) {
int n = s.length();
vector<int> dp(n, 0);
for(int i = n-2; i >= 0; -- i){
int pre = 0;
for(int j = i+1; j < n; ++ j){
int temp = dp[j];
if(s[i] == s[j])
dp[j] = pre;
else
dp[j] = min(dp[j], dp[j-1]) + 1;
pre = temp;
}
}
return dp[n-1];
}
};
分割现有的字符串得到回文字符串
这节有点八仙过海各显神通的意味了,主要是回溯和动态规划。这里把回文串分割这个系列具体问题具体分析下。
131. 分割回文串
题目链接:131. 分割回文串
要给出所有的分割方案,用回溯是最佳的
class Solution {
public:
vector<string> tempans;
vector<vector<string>> ans;
void dfs(string &s, int cur){
if(cur == s.length()){
ans.push_back(tempans);
return;
}
for(int j = cur; j < s.length(); ++ j){
//这里判断子串是否为回文串,可以用第一节的找最长连续回文子串中的动态规划优化一下
bool flag = true;
int k = 0;
while(k+cur < j-k){
if(s[k+cur] != s[j-k]){
flag = false;
break;
}
++ k;
}
if(flag){
tempans.push_back(s.substr(cur, j-cur+1));
dfs(s, j+1);
tempans.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
dfs(s, 0);
return ans;
}
};
132. 分割回文串 II
题目链接:132. 分割回文串 II
这题理论上也可以用131的回溯法,然后找到最短分割次数,但是这种方法势必会带来重复运算,比如对于最少分割次数中,有一段为s[left,right],但是回溯还是会把s[left,right]中所有的分割方法都揪出来,且不大容易剪枝。因此这里考虑动态规划, 对子串[left,right]已经满足回文要求的不再分割。
/*
dp[i]: s[0...i]分割成回文子串的最短次数
s[0...i]为回文子串:dp[i]=0
s[0...i]不是回文子串:dp[i] = min(dp[i], dp[j]+1), 其中s[j+1...i]为回文串
*/
class Solution {
public:
int minCut(string s) {
int n = s.length();
//这里判断子串是否为回文串,用第一节的找最长连续回文子串中的动态规划优化一下
vector<vector<bool>> ispalindrome(n, vector<bool>(n, false));
for(int i = 0; i < n; ++ i)
ispalindrome[i][i] = true;
for(int i = n-2; i >= 0; -- i){
for(int j = i+1; j < n; ++ j){
if(j == i+1)
ispalindrome[i][j] = s[i] == s[i+1];
else if(s[i] == s[j])
ispalindrome[i][j] = ispalindrome[i+1][j-1];
}
}
vector<int> dp(n, INT_MAX);
for(int i = 0; i < n; ++ i){
if(ispalindrome[0][i]) dp[i] = 0;
else{
for(int j = 0; j < i; ++ j)
if(ispalindrome[j+1][i])
dp[i] = min(dp[i], dp[j]+1);
}
}
return dp[n-1];
}
};
1278. 分割回文串 III
题目链接:1278. 分割回文串 III
这题有点特殊,涉及到修改字符。如果不考虑修改字符的话,本题即判断字符串能否分割成k个不为空的回文子串,容易想到动态规划。
/*
dp[i][k]: s[0...i]能否分割成k个不为空的字符串
dp[i][k] = dp[j][k-1] 其中s[j+1...i]为回文子串
*/
进一步想,要求最小的修改次数的话,用cost(s, j+1, i)表示将s[j+1…i]改为回文子串的修改次数。这个cost函数其实就是修改中的替换操作,可以用动态规划提前记录下所有的cost
/*
dp[i][k]: s[0...i]能否分割成k个不为空的字符串需要修改的次数
dp[i][k] = min(dp[j][k-1]+cost(s,j+1,i))
*/
class Solution {
public:
int palindromePartition(string s, int k) {
int n = s.size();
vector<vector<int>> cost(n, vector<int>(n, 0));
vector<vector<int>> dp(n+1, vector<int>(k+1, INT_MAX));
for(int i = n-2; i >= 0; -- i){
for(int j = i+1; j < n; ++ j){
if(s[i] == s[j]) cost[i][j] = cost[i+1][j-1];
else cost[i][j] = cost[i+1][j-1] + 1;
}
}
dp[0][0] = 0;
for(int i = 1; i <= n; ++ i){
for(int j = 1; j <= min(k,i); ++ j){
if(j == 1)
dp[i][j] = cost[0][i-1];
else
for(int r = j-1; r < i; ++ r)
dp[i][j] = min(dp[i][j], dp[r][j-1]+cost[r][i-1]);
}
}
return dp[n][k];
}
};
1745. 回文串分割 IV
题目链接:1745. 回文串分割 IV
判断原字符串能否分割成3个回文子串,动态规划+暴搜,在5. 最长回文子串得到各个子字符串是否为回文串的基础上,暴力搜索两个分隔线p和q即可
class Solution {
public:
bool checkPartitioning(string s) {
int n = s.length();
vector<vector<bool>> dp(n+1, vector<bool>(n+1, false));
for(int i = 0; i < n; ++ i)
dp[i][i] = true;
for(int i = n-2; i >= 0; -- i){
for(int j = i+1; j < n; ++ j){
if(j == i+1)
dp[i][j] = s[i] == s[i+1];
else if(s[i] == s[j])
dp[i][j] = dp[i+1][j-1];
}
}
for(int p = 0; p < n-1; ++ p){
if(!dp[0][p]) continue;
int q = p + 1;
while(q < n){
if(dp[p+1][q] && dp[q+1][n-1])
return true;
++ q;
}
}
return false;
}
};