题目I
分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
输入: “aab”
输出:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]
分析
方法1
使用朴素的递归,对于一个长度为N的字符串,其可以切割的地方有N-1处,因此这个时间复杂度为 o ( 2 n − 1 ) o(2^{n-1}) o(2n−1)
class Solution {
public:
vector<vector<string>> res;
vector<vector<string>> partition(string s) {
vector<string> tmp;//用于存储中间结果
dfs(0, s, tmp);
return res;
}
bool isValid(string s){
int i=0;
int j=s.size()-1;
while(i<j){
if(s[i]!=s[j]) return false;
i++;
j--;
}
return true;
}
void dfs(int start, string s, vector<string> tmp){
if(start==s.size()){
res.push_back(tmp);
return;
}
//选择决策
for(int i=start;i<s.size();i++){
string sub = s.substr(start, i-start+1);
if(isValid(sub)){
tmp.push_back(sub);
dfs(i+1, s, tmp);
tmp.pop_back();
}
}
}
};
方法2 动态规划
验证回文串那里,每一次都得使用“两边夹”的方式验证子串是否是回文子串。于是“用空间换时间”,利用「力扣」第 5 题:最长回文子串 的思路,利用动态规划把结果先算出来,这样就可以以 O(1)的时间复杂度直接得到一个子串是否是回文。
class Solution {
public:
vector<vector<string>> res;
vector<vector<string>> partition(string s) {
//使用朴素的递归,对于一个长度为N的字符串,其可以切割的地方有N-1处,因此这个时间复杂度为$o(2^(n-1))$
//使用动态规划先对字符串进行预处理,dp[i][j]表示s[i, j]是否是回文串
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
for(int j=0;j<s.size();j++){
for(int i=0;i<=j;i++){
if(s[i]==s[j]){
if((j-i)<=2) dp[i][j] = true;
else if(dp[i+1][j-1]) dp[i][j] = true;
}
}
}
vector<string> tmp;//用于存储中间结果
dfs(0, s, tmp, dp);
return res;
}
void dfs(int start, string s, vector<string> tmp, vector<vector<bool>> dp){
if(start==s.size()){
res.push_back(tmp);
return;
}
//选择决策
for(int i=start;i<s.size();i++){
string sub = s.substr(start, i-start+1);
if(dp[start][i]){
tmp.push_back(sub);
dfs(i+1, s, tmp, dp);
tmp.pop_back();
}
}
}
};
注意上面的循环:
for(int j=0;j<s.size();j++){
for(int i=0;i<=j;i++){
if(s[i]==s[j]){
if((j-i)<=2) dp[i][j] = true;
else if(dp[i+1][j-1]) dp[i][j] = true;
}
}
}
为什么s[i][j]依赖于s[i+1][j-1], 其实将右指针放在外循环,本身就是中心扩展的方式,且s[i+1][j-1]先被求出来。
对于动态规划部分也可以写成如下的方式:
//外层循环表示的是当前字串的长度, 且长度从小到大
for(int l=0;l<s.size();l++){
//内层循环i表示的是子字符串的起点
for(int i=0;i+l<s.size();i++){
//计算出终点j
int j = i + l;
if(s[i]==s[j]){
if((j-i)<=2) dp[i][j] = true;
else if(dp[i+1][j-1]) dp[i][j] = true;
}
}
}
题目II
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的最少分割次数。
示例:
输入: “aab”
输出: 1
解释: 进行一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串。
分析
求出最小的分割次数。定义第二个dp数组f[i],f[i]表示s[0,i]的最小分割数目。
- 如果 s[0:i] 本身就是一个回文串,那么不用分割,即 f[i] = 0
- 如果 s[0:i] 不是一个回文串则需要去遍历;接下来枚举可能分割的位置:即如果 s[0:i] 不是一个回文串,就尝试分割,枚举分割的边界 j。如果 s[j + 1, i] 不是回文串,尝试下一个分割边界。如果 s[j + 1, i] 是回文串,则 f[i] 就是在 f[j] 的基础上多一个分割。于是枚举 j 所有可能的位置,取所有 f[j] 中最小的再加 1 ,就是 f[i]。
得到状态转移方程如下:f[i] = min([f[j] + 1 for j in range(i) if s[j + 1, i] 是回文])
由上面的分析可知,上面的问题还牵扯到子问题判断s[j + 1, i]是否回文串,因此为了避免在求解f[i]的时候还要再单独去判断s[j + 1, i]是否是回文串,那么就需要把回文串的信息也存储下来。
class Solution {
public:
void partition(string s, vector<vector<bool> >& dp) {
//外层循环表示的是当前字串的长度, 且长度从小到大
for(int l=0;l<s.size();l++){
//内层循环i表示的是子字符串的起点
for(int i=0;i+l<s.size();i++){
//计算出终点j
int j = i + l;
if(s[i]==s[j]){
if((j-i)<=2) dp[i][j] = true;
else if(dp[i+1][j-1]) dp[i][j] = true;
}
}
}
}
int minCut(string s) {
vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
partition(s, dp);
vector<int> f(s.size(), INT_MAX);
//注意下面的写法是错误的,要做相应的修改, 本题作为一个动态规划问题,状态转移方程比较好写,但是对于特殊情况的处理就比较麻烦
//主要考虑以0作为开始,j作为结束的字符串,如果s[0, j]是一个回文的话,那么就将f[j]赋值为0
// for(int i=0;i<s.size();i++){
// //内层循环i表示的是子字符串的起点
// for(int j=0;j<i;j++){
// if(dp[j][i]){
// f[i] = min(f[i], f[j]+1);
// }
// }
// }
//当i等于0的时候,f[i]==0
f[0] = 0;
for(int i=1;i<s.size();i++){
if(dp[0][i]){
f[i] = 0;
continue;
}
//内层循环i表示的是子字符串的起点
for(int j=0;j<i;j++){
if(dp[j+1][i]){
f[i] = min(f[i], f[j]+1);
}
}
}
return f[s.size()-1];
}
};