leetcode-master/problems/0131.分割回文串.md

241 lines
8.8 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

> 切割问题其实是一种组合问题!
# 131.分割回文串
题目链接https://leetcode-cn.com/problems/palindrome-partitioning/
给定一个字符串 s将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例:
输入: "aab"
输出:
[
["aa","b"],
["a","a","b"]
]
# 思路
本题这涉及到两个关键问题:
1. 切割问题,有不同的切割方式
2. 判断回文
相信这里不同的切割方式可以搞懵很多同学了。
这种题目想用for循环暴力解法可能都不那么容易写出来所以要换一种暴力的方式就是回溯。
一些同学可能想不清楚 回溯究竟是如果切割字符串呢?
我们来分析一下切割,**其实切割问题类似组合问题**。
例如对于字符串abcdef
* 组合问题选取一个a之后在bcdef中再去选取第二个选取b之后在cdef中在选组第三个.....。
* 切割问题切割一个a之后在bcdef中再去切割第二段切割b之后在cdef中在切割第三段.....。
感受出来了不?
所以切割问题,也可以抽象为一颗树形结构,如图:
![131.分割回文串](https://img-blog.csdnimg.cn/20201123203228309.png)
递归用来纵向遍历for循环用来横向遍历切割线就是图中的红线切割到字符串的结尾位置说明找到了一个切割方法。
此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。
## 回溯三部曲
* 递归函数参数
全局变量数组path存放切割后回文的子串二维数组result存放结果集。 (这两个参数可以放到函数参数里)
本题递归函数参数还需要startIndex因为切割过的地方不能重复切割和组合问题也是保持一致的。
在[回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)中我们深入探讨了组合问题什么时候需要startIndex什么时候不需要startIndex。
代码如下:
```
vector<vector<string>> result;
vector<string> path; // 放已经回文的子串
void backtracking (const string& s, int startIndex) {
```
* 递归函数终止条件
![131.分割回文串](https://img-blog.csdnimg.cn/20201123203228309.png)
从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止终止条件。
**那么在代码里什么是切割线呢?**
在处理组合问题的时候递归参数需要传入startIndex表示下一轮递归遍历的起始位置这个startIndex就是切割线。
所以终止条件代码如下:
```
void backtracking (const string& s, int startIndex) {
// 如果起始位置已经大于s的大小说明已经找到了一组分割方案了
if (startIndex >= s.size()) {
result.push_back(path);
return;
}
}
```
* 单层搜索的逻辑
**来看看在递归循环,中如何截取子串呢?**
在`for (int i = startIndex; i < s.size(); i++)`循环中我们 定义了起始位置startIndex那么 [startIndex, i] 就是要截取的子串
首先判断这个子串是不是回文如果是回文就加入在`vector<string> path`中path用来记录切割过的回文子串。
代码如下:
```
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是回文子串
// 获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else { // 如果不是则直接跳过
continue;
}
backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经填在的子串
}
```
**注意切割过的位置不能重复切割所以backtracking(s, i + 1); 传入下一层的起始位置为i + 1**
## 判断回文子串
最后我们看一下回文子串要如何判断了,判断一个字符串是否是回文。
可以使用双指针法,一个指针从前向后,一个指针从后先前,如果前后指针所指向的元素是相等的,就是回文字符串了。
那么判断回文的C++代码如下:
```C++
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
```
如果大家对双指针法有生疏了,传送门:[双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA)
此时关键代码已经讲解完毕,整体代码如下(详细注释了)
# C++整体代码
根据Carl给出的回溯算法模板
```
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择本层集合中元素树中节点孩子的数量就是集合的大小) {
处理节点;
backtracking(路径选择列表); // 递归
回溯撤销处理结果
}
}
```
不难写出如下代码:
```C++
class Solution {
private:
vector<vector<string>> result;
vector<string> path; // 放已经回文的子串
void backtracking (const string& s, int startIndex) {
// 如果起始位置已经大于s的大小说明已经找到了一组分割方案了
if (startIndex >= s.size()) {
result.push_back(path);
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是回文子串
// 获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else { // 不是回文,跳过
continue;
}
backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经填在的子串
}
}
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
public:
vector<vector<string>> partition(string s) {
result.clear();
path.clear();
backtracking(s, 0);
return result;
}
};
```
# 总结
这道题目在leetcode上是中等但可以说是hard的题目了但是代码其实就是按照模板的样子来的
那么难究竟难在什么地方呢
**我列出如下几个难点:**
* 切割问题可以抽象为组合问题
* 如何模拟那些切割线
* 切割问题中递归如何终止
* 在递归循环中如何截取子串
* 如何判断回文
**我们平时在做难题的时候,总结出来难究竟难在哪里也是一种需要锻炼的能力**
一些同学可能遇到题目比较难但是不知道题目难在哪里反正就是很难其实这样还是思维不够清晰这种总结的能力需要多接触多锻炼
**本题我相信很多同学主要卡在了第一个难点上:就是不知道如何切割,甚至知道要用回溯法,也不知道如何用。也就是没有体会到按照求组合问题的套路就可以解决切割**
如果意识到这一点算是重大突破了接下来就可以对着模板照葫芦画瓢
**但接下来如何模拟切割线,如何终止,如何截取子串,其实都不好想,最后判断回文算是最简单的了**
除了这些难点**本题还有细节例如切割过的地方不能重复切割所以递归函数需要传入i + 1**。
所以本题应该是一个道hard题目了
**可能刷过这道题目的录友都没感受到自己原来克服了这么多难点就把这道题目AC了**这应该叫做无招胜有招人码合一哈哈哈
当然本题131.分割回文串还可以用暴力搜索一波132.分割回文串II和1278.分割回文串III 爆搜就会超时需要使用动态规划了我们会在动态规划系列中详细讲解
**就酱如果感觉「代码随想录」不错就把Carl宣传一波吧**
> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!**
**如果感觉对你有帮助,不要吝啬给一个👍吧!**