目录
回溯算法理论基础
1.什么是回溯算法
回溯算法也可以叫做回溯搜索算法,简称回溯法,是一种搜索的方式。
回溯是递归的“副产品”,只要有递归的地方就会有对应回溯的过程。在后续内容中,回溯函数就是递归函数,指的都是一个函数。
2.回溯法可以解决的问题
- 组合问题:如何按照一定规则在N个数中找出k个数的集合
- 切割问题:一个字符串按照一定的规则切割,有几种切割方式
- 子集问题:一个N个数的集合中几个符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后、数独等问题。
注:有些同学可能分不清组合和排列,只需要记住组合无序,排列有序即可。
例题一
本问题涉及两个关键问题:
- 切割问题,即 s 有不同的切割方式。
- 判断回文。
其实,我们稍加分析就会发现,切割问题和组合问题其实很像。我们也同样可以将切割问题抽象为树形结构,如图所示。递归用来纵向遍历,for循环用来横向遍历,当切割线切割到字符串的的结尾位置时,说明我们找到了一种切割方案。
回溯 “三部曲” 如下:
(1)确定递归函数的参数。
全局变量数组path用于存放切割后的回文的子串,二维数组 result 用于存放结果集。本题递归函数仍需要startindex,因为切割过的地方不能重复切割。代码如下:
vector<vector<string>> result;
vector<string> path; //存放已经回文的子串
void backtracking(const string& s,int startindex)
(2)确定递归函数的终止条件。
从树形图中可以看出:切割线切割到了字符串最后面,说明找到了一种切割方案,此时本层递归终止。在这里,切割线指的就是startindex,在for循环中startindex表示的就是下一轮遍历递归的起始位置。所以终止条件的代码如下:
// 如果起始位置大于s,说明找到了一组分割方案
if(startindex>=s.size()){
result.push_back(path);
return;
}
(3)确定单层搜索的逻辑。
在for循环中定义了starindex为起始位置,[startindex,i]为要截取的字串。但是,截取之后需要判断这个字串是不是回文,如果是则加入path中,path用来记录切割过的回文字串。注意:切割过的位置不能重复切割,所以backtracking(s,i+1)传入下一层的起始位置为i+1。代码如下:
for(int i=startindex;i<s.size();i++)
{
if(check(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(); // 回溯过程,弹出本次已处理的子串
}
整体代码如下:
class Solution {
public:
vector<vector<string>> result;
vector<string> path; //存放已经回文的子串
bool check(const string& s,int i,int j){
// 利用双指针法来判断是否为回文串
for(int left=i,right=j;left<right;left++,right--)
{
if(s[left]!=s[right]){
return false;
}
}
return true;
}
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(check(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(); // 回溯过程,弹出本次已处理的子串
}
}
vector<vector<string>> partition(string s) {
backtracking(s,0);
return result;
}
};
例题二
只要意识到这是一个切割问题,就可以使用回溯搜索法,我们只需要将问题抽象为树形结构,如下图所示:
回溯 “三部曲” 如下:
(1)确定递归函数的参数。
类似于例题一,本题同样不能重复分割,所以一定需要starindex,用于记录下一层递归分割的起始位置。本题还需要添加一个int变量pointnum来记录 “.”的数量。代码如下:
vector<string> result;
void backtracking(string& s,int startindex,int pointnum)
(2)确定递归的终止条件。
本题明确要求字符串只会被分割成 4 段,所以不能用切割线切割到最后为终止条件,而是要将分割的段数作为终止条件。pointnum表示 "." 的数量,pointnum为 3 说明字符串分成了 4 层。然后验证第 4 段是否合法,如果合法就加入结果集。代码如下:
if(pointnum==3){ // "."的数量为3时,分割结束
// 这里时判断第4段是否合法,合法就加入result里
if(check(s,startindex,s.size()-1)==true){
result.push_back(s);
}
return;
}
(3)确定单层搜索的逻辑。
在for循环中,[startindex,i]这个区间就是截取的子字符串,需要判断其是否合法。如果合法,就在字符串后面加上“.”表示已经被分割。如果不合法,就结束本层循环。
注意:调用函数递归时,下一层的递归starindex要从 i+2 开始(因为需要在字符串中加入分隔符 “.”),同时记录分隔符的数量pointnum要+1。回溯的时候要删除加入的分隔符,以及pointnum要-1。代码如下:
for(int i=startindex;i<s.size();i++)
{
// 判断[starindex,i]这个区间的子字符串是否合法
if(check(s,startindex,i)==true){
s.insert(s.begin()+1+i,'.'); // 在i的后面插入一个"."
pointnum++;
// 插入"."后,下一个起始位置要i+2
backtracking(s,i+2,pointnum);
pointnum--;
s.erase(s.begin()+i+1);
}
else break; // 不合法,直接结束本层循环
}
最后就是判断子字符串是否为合法的,主要考虑如下三点:
- 子字符串以0开头的数字不合法;
- 子字符串有非整数字符不合法;
- 如果有子字符串大于255不合法;
整体代码如下:
class Solution {
public:
vector<string> result;
bool check(const string& s,int begin,int end){
if(begin>end){
return false;
}
if(s[begin]=='0'&&begin!=end){ // 这里还需要考虑单独为 "0"的情况
return false;
}
int num=0;
for(int i=begin;i<=end;i++)
{
if(s[i]<'0'||s[i]>'9'){
return false;
}
num=num*10+(s[i]-'0'); // 将字符串转化为数字的形式
if(num>255){
return false;
}
}
return true;
}
void backtracking(string& s,int startindex,int pointnum){
if(pointnum==3){ // "."的数量为3时,分割结束
if(check(s,startindex,s.size()-1)==true){
result.push_back(s);
}
return;
}
for(int i=startindex;i<s.size();i++)
{
// 判断[starindex,i]这个区间的子字符串是否合法
if(check(s,startindex,i)==true){
s.insert(s.begin()+1+i,'.'); // 在i的后面插入一个"."
pointnum++;
// 插入"."后,下一个起始位置要i+2
backtracking(s,i+2,pointnum);
pointnum--;
s.erase(s.begin()+i+1);
}
else break; // 不合法,直接结束本层循环
}
}
vector<string> restoreIpAddresses(string s) {
if(s.size()>12) return result;
backtracking(s,0,0);
return result;
}
};