题目解析
题目描述
class Solution {
public:
vector<vector<string>> partition(string s) {
}
};
题目解析
题目意思:切割字符串s,切出的每一个子串都必须是回文串,请找出所有切分的可能。
分析
本题这涉及到两个关键问题:
- 切割问题,有不同的切割方式
- 判断回文
关键在于怎么切割。
我们用指针start试着去切割,切出一个回文串,基于新的start,继续往下切,直到start越界。
- 切出的子串满足回文,就将它加入部分解tmp数组,并继续往下切割
- 切出的子串不是回文,跳过该选择,不递归,继续下一轮迭代
为什么要回溯
因为不是找到一个合法的部分解就完事,要找齐所有合法的部分解。
下面两种情况,是结束递归的两种情况:
- 指针越界了,没有可以切分的子串了,递归到这一步,说明一直在切出回文串,现在生成了一个合法的部分解,return
- 走完了当前递归的for循环,考察了基于当前start的所有的切分可能,当前递归自然地结束
它们都代表了,当前做出的选择,所进入的递归,结束了,该分支的搜索结束了,该去搜另一分支了
所以当前递归结束后,要将当前的选择撤销,回到选择前的状态,去考察另一个选择,即进入下一轮迭代,尝试另一种切分的可能
class Solution {
bool ispalindrome(const std::string &s, int l, int r){
while (l <= r){
if(s[l] != s[r]){
return false;
}
++l;
--r;
}
return true;
}
void dfs(const std::string &s, int l, vector<string> &cur,
vector<vector<string>> &res){
if(l == s.size()){
res.push_back(cur);
return;
}
for (int r = l; r < s.size(); ++r) { // 枚举出当前的所有选项,从索引start到末尾索引
if(ispalindrome(s, l, r)){ // 当前选择i,如果 start到 i 是回文串,就切它
cur.push_back(s.substr(l, r + 1 - l)); // 切出来,加入到部分解temp
dfs(s, r + 1, cur, res);// 基于这个选择,继续往下递归,继续切
cur.pop_back(); // 上面递归结束了,撤销当前选择i,去下一轮迭代
}
}
}
}
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> res;
vector<string> cur;
dfs(s, 0, cur, res);
return res;
}
};
存在什么问题
每次递归都调用isPali判断,是否有必要?以“aab”为例,我们打印一下传入 isPali 的 start 和 i:
0 0
1 1
2 2
1 2
0 1
2 2 (重复)
0 2
我们做了重复的计算,有的子串已经判断过是否回文了,就别再判断了。
做法是用一个memo二维数组,将计算过的子问题结果存下来,下次再遇到就直接拿出来用。
加入记忆化代码
动态规划写法
- f[i][j]表示[i, j]这一段是否为回文串
- 要想f[i][j]为回文串,必须满足如下两个条:
- f[i + 1][j - 1] == true
- s[i] == s[j]
- 由于状态f[i][j]依赖于f[i + 1][j - 1],因此我们左端点i是从大到小的,右端点j是从小到大的
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> res;
vector<string> cur;
int n = s.size();
if(n == 0){
return res;
}
std::vector<std::vector<int>> f(n,std::vector<int>(n, true));
for (int i = n - 2; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
if(s[i] != s[j]){
f[i][j] = false;
}else{
f[i][j] = f[i + 1][j - 1];
}
}
}
std::function<void(int)>dfs = [&](int i){
// 递归终止条件为 i指针已到字符串最后一个字符 (终止本次递归,因为是循环 所有有多条线路递归)
if(i == n){
res.push_back(cur);
return ;
}
// 循环的目的是 可以取到字符串s的起始位置为i的所有子字符
for (int j = i; j < n; ++j) {
if(f[i][j]){ // 如果是子串会进入下一层
cur.push_back(s.substr(i, j - i + 1));
dfs(j + 1);
cur.pop_back(); // 本次递归完需要回到本次循环的上一层状态(回溯)
}
}
};
dfs(0);
return res;
}
};
总的来说,切割方案就是以首个字符为起点,枚举以其开头的所有回文串方案,加入集合,然后对生效的字符串部分继续暴力搜索
参考
类似题目
题目 | 思路 |
---|---|
leetcode:131. 切割str,使得每个子串均是回文,返回所有切割方案Palindrome Partitioning | 切割方案就是以首个字符为起点,枚举以其开头的所有回文串方案,加入集合,然后对生效的字符串部分继续暴力搜索 |
leetcode:132. 分割str,使得每个子串均是回文,返回符合要求的最少切割次数 Palindrome Partitioning II | 从左到右尝试,尝试以xxx作为切出来的第一个部分,所有的可能性,dp[i]表示:从i…开始至少分成几个部分能让每个部分都是回文串 |
leetcode:647. str中有多少个回文子串 Palindromic Substrings | 定义:dp[i][j]表示字符串s在[i, j]区间的子串是否是一个回文串;同时维护一个ans,如果发现了true,那么ans++ |
leetcode:5. str中最长回文子串 Longest Palindromic Substring | 定义:dp[i][j]表示字符串s在[i, j]区间的子串是否是一个回文串;两个变量: maxLen:统计最长的回文子串的长度(j-i), str:最长回文子串是什么 |
leetcode:516. str中最长回文子序列的长度 Longest Palindromic Subsequence | 定义:在str[L…R]区间中,返回其最长回文子序列长度。那么base case:如果L == R,那么返回1;如果L + 1 == R,如果str[L] == str[R],那么返回2,否则返回1;一般情况:最长回文子串既不以L开头,也不以R结尾;最长回文子串以L开头,不以R结尾;最长回文子串不以L开头,以R结尾;最长回文子串以L开头,以R结尾,前提是str[L] == str[R] |
leetcode:730. str中回文子序列的个数 Count Different Palindromic Subsequences | 方法一:对于每个字符都有要和不要两种选择,最终再判断是不是回文串;方法二:定义从str[i…j]的所有子序列中能搞出多少个回文来,空串不算,一共有四种可能:回文子序列一定不选择str[i],也不选择str[j];回文子序列一定不选择str[i],一定选择str[j];回文子序列一定选择str[i],一定不选择str[j];回文子序列一定选择str[i],一定选择str[j] |
leetcode:1143. str1和str2的最长公共子序列 Longest Common Subsequence | 方法一:想要求s1和s2的最长公共子序列,不妨称这个子序列为 lcs。那么对于 s1 和 s2 中的每个字符,有什么选择?很简单,两种选择,要么在 lcs 中,要么不在。所以可以这样做:用两个指针i和j从后往前遍历s1和s2,如果s1[i] == s2[j],那么这个字符一定在lcs中。否则,s1[i]和s2[j]这两个字符至少有一个不在lcs中,需要丢弃一个。对于第一种情况,找到一个 lcs 中的字符,同时将 i j 向前移动一位,并给 lcs 的长度加一;对于后者,则尝试两种情况,取更大的结果。方法二:现在我们只关心str1[0…i],str2[0…j],对于它们的最长公共子序列长度是多少。 |
1682-VIP. 最长回文子序列 II Longest Palindromic Subsequence II |