目录
什么是回溯法
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。其实是每一次递归后再返回上一层,由单一深度搜索拓宽为深度加广度搜索。
回溯法效率
回溯法其实是一个穷尽所有可能的算法,本质是穷举。那为什么不直接用多层for循环呢? 因为你无法确定for循环的层数,只能通过递归来层层深入。
回溯法解决的问题
- 需要穷尽所有可能,并且无法确定for循环层数。
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独等等
同时此类题必有标注,可以按任意顺序返回。
回溯抽象成树
因为即有深度又有广度,所以所有回溯问题都可以抽象转化成树。
---------------------------------------------------------------------------------------------------------------------------------
基础模板
void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } }
---------------------------------------------------------------------------------------------------------------------------------
题目一:77. 组合
解析:首先提供两个容器 ,第一个容器ans用来保存最终的答案,path用来保存每一条路径上的答案,如果符合条件再将它放入ans容器中。
vector<vector<int>> ans;
vector<int> path;
写终止条件,根据题目要求找寻k个数的组合,所以如果path中的数达到k个的时候就应该将答案放入ans中。
if(path.size()==k)
{
ans.push_back(path);
return;
}
for循环代表当 前层数的所有可能选择,比如说你在第一层,则你有四种选择,分别是1,2,3,4
然后通过递归,进入下一层。这里引用startIndex做为起始位置标志,因为当层数不断深入的时候
起始位置是i+1 ,因为第 i 项在上一层已经取过,如果下次递归的起始位置不从i+1开始的话会导致重复。
for(int i=startIndex;i<=n;i++)
{
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back();
}
当一个path结束后自动上升一层,此时就要进行回溯,实际上是弹出path中的一个元素,这时候for循环在这一层刚好后移一位,同时path缺少一个元素。再push进去一个元素,进入下一层递归,刚好满足要求,以此类推。
整体
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtracking(int n,int k,int startIndex)
{
if(path.size()==k)
{
ans.push_back(path);
return;
}
for(int i=startIndex;i<=n;i++)
{
path.push_back(i);
backtracking(n,k,i+1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(n,k,1);
return ans;
}
};
---------------------------------------------------------------------------------------------------------------------------------
题目二:216. 组合总和 III
解析:提供容器,第一个容器ans用来保存最终的答案,path用来保存每一条路径上的答案,如果符合条件再将它放入ans容器中。
vector<vector<int>> ans;
vector<int> path;
先明确终止条件是什么,目标是找到相加之和为n的k个元素。分析可知,实际上有两个终止条件,第一个是path的长度必须为k,第二是path元素相加之和为n。
这里运用逆向思维,相加为n实际上就是,总和n每次都减去元素的值。最后刚好为零的path为所求path。
if(path.size()==k)
{
if(n==0)
{
ans.push_back(path);
}
return;
}
如图所示,这幅图我没有标记的很全面,为的是让读者自己认真看图思考一下。其实你会发现,每一个匡都是一个for循环里面可选择的数据。 为什么越往下走,匡里面可循环的元素越来越少呢?是因为你在递归,每次的startIndex都会往前进一位。其实横着看是上一层所有可能选择,竖着看是单一的一条path。
for(int i=startIndex;i<=9;i++)
{
path.push_back(i);
n=n-i;
backtracking(k,n,i+1);
n=n+i;
path.pop_back();
}
每次递归都要把n减去元素i,当满足题目条件,即 n 为 0 的时候证明已经找到了k个元素相加和为n。此时要进行回溯,n 加上 i 返回上一层的for循环直到所有的遍历结束。
整体
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtracking(int k,int n,int startIndex)
{
if(path.size()==k)
{
if(n==0)
{
ans.push_back(path);
}
return;
}
for(int i=startIndex;i<=9;i++)
{
path.push_back(i);
n=n-i;
backtracking(k,n,i+1);
n=n+i;
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k,n,1);
return ans;
}
};
---------------------------------------------------------------------------------------------------------------------------------
题目三:17. 电话号码的字母组合
提供容器ans来接收所有字母组合,让后s 相当于path。
要提前处理一下按键上的字母组合,把题目上的按键转化为string 数组。同时思考一下,本体和上面题目的区别,本题样本区间取自两个字符串,但是上面题目中的取自都是一个字符串。这一点区别将会为我们如何递归埋下伏笔,也就是startIndex到底该怎么选。
例如 "abc" 和 "def" 单层先选取 "a" 下一层选取的可不是 "b" 而是 "d"。所以startIndex不能是简单的 i+1 ,需要改变样本区间。
const string letterMap[10]={
"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
};
vector<string> ans;
string s;
终止条件非常简单,当组合 s 的长度 等于 digits 的长度的时候就可以结束了。
if(index==digits.size())
{
ans.push_back(s);
return;
}
这里重点讲解index的含义是什么,相当于之前的startIndex,但是我们在此基础上动了一些手脚,index实际上是从前到后扫描 digits,让后得到的char 减去起始 ‘0’ 就可以得到到底样本区间是哪一个数字按键。
int digit=digits[index]-'0';
string letter=letterMap[digit];
for(int i=0;i<letter.size();i++)
{
s.push_back(letter[i]);
backtracking(digits,index+1);
s.pop_back();
}
到此为止,本题和上面没有什么真正的逻辑上的区别,从逻辑难度来说实际上是比较简单的,因为本体唯一的收取path的限制条件仅仅是长度罢了。其他都是照猫画虎。
看上去复杂是因为,取样本区间竟然不是在一个string当中,而是在两个string区间所取的,同时我们要预先处理一下按键字符。
class Solution {
public:
const string letterMap[10]={
"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
};
vector<string> ans;
string s;
void backtracking(const string& digits,int index)
{
if(index==digits.size())
{
ans.push_back(s);
return;
}
int digit=digits[index]-'0';
string letter=letterMap[digit];
for(int i=0;i<letter.size();i++)
{
s.push_back(letter[i]);
backtracking(digits,index+1);
s.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if(digits.size()==0) return ans;
backtracking(digits,0);
return ans;
}
};
---------------------------------------------------------------------------------------------------------------------------------
题目四:90. 子集 II
提供容器,老样子提供单条路径path,让后提供所有情况接收容器ans。
vector<vector<int>> ans;
vector<int> path;
本题没有限制,只要是他的子集就可以。 既然没有要求,那么不用写终止条件,同时并不再是一条路只收取一个path,而是一条路上各层之间的path都要收取。
同层递进减枝:这一部分是重难点,请读者认真阅读。
通过树形图展开可以发现,同层元素是不能重复的,不然会有重复的path加入到ans当中。但是上下层是可以有重复元素的比如(1,2,2)。
那么,既然有重复我们怎么去重呢? 我们可以使用条件nums[i]==num[i-1] 的时候直接跳过,有些同学会问,为什么不是nums[i]==nums[i+1]的时候跳过呢?
你仔细想一下,当我们进行对比的时候nums[i]==nums[i+1] 我们进行了元素装填么?我是先判断,才把元素放入path中,那么问题来了如果我直接判断第 i 和 i+1项的时候,实际上第 i 和 i+1 项都还没有处理呢。然后你就直接跳过第 i 项了。那么请问你的(1,2,2)三元素集合怎么来?你不是跳过了一个么??
所以我们需要预先处理再进行判断,如何预先处理? 那就是nums[i-1]==nums[i]时候跳过,你都到第 i 项了,你 i - 1 项必然已经处理了啊。发现 i 和 i - 1项一样,直接跳过第 i 项不就行了,i - 1项预先处理过在path里了。
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
你以为到这就结束了??太年轻,太年轻了。为什么我要强调同层去重和上下层去重?? 你设立的条件nums[i-1]==nums[i] 针对上下层和同层都适用啊,你上下层startIndex是i+1,那么假如你是(1,2)还余留了一个2,当你进入下一层的时候,本应该是(1,2,2)。结过你一对比,发现俩 2 一样就直接跳过最后一个2了。那么你大错特错!!!
这里引入bool 标志,上下层处理过就把used标明true,代表可以重复。递归结束后,就要进行回溯了,那么必然会回到上层的for循环中,同层不能重复,所以used标明false。
让后判断的时候有两个条件 nums[i-1]==nums[i] && used=false;
lass Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtracking(vector<int>& nums,int startIndex,vector<bool>& used)
{
ans.push_back(path);
for(int i=startIndex;i<nums.size();i++)
{
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false) continue;
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());
backtracking(nums,0,used);
return ans;
}
};
到此分析完毕,掰开揉碎,只为精品。
---------------------------------------------------------------------------------------------------------------------------------
题目五:46. 全排列
提供容器
vector<vector<int>> ans;
vector<int> path;
终止条件
if(path.size()==nums.size())
{
ans.push_back(path);
return;
}
同层起始减枝叶
for(int i=0;i<nums.size();i++)
{
if(used[i]==true) continue;
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
}
整体
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtracking(vector<int>& nums,int startIndex,vector<bool>& used)
{
if(path.size()==nums.size())
{
ans.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(used[i]==true) continue;
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(),false);
backtracking(nums,0,used);
return ans;
}
};
题目六:491. 递增子序列
提供容器
vector<vector<int>> ans;
map<vector<int>,int> mp;
vector<int> path;
终止条件
if(path.size()>=2&&path[path.size()-2]<=path[path.size()-1])
{
mp[path]++;
}else if(path.size()>=2&&path[path.size()-2]>path[path.size()-1])
{
return;
}
递归
for(int i=startIndex;i<nums.size();i++)
{
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
最后 整体 去重
class Solution {
public:
vector<vector<int>> ans;
map<vector<int>,int> mp;
vector<int> path;
void backtracking(vector<int>& nums,int startIndex)
{
if(path.size()>=2&&path[path.size()-2]<=path[path.size()-1])
{
mp[path]++;
}else if(path.size()>=2&&path[path.size()-2]>path[path.size()-1])
{
return;
}
for(int i=startIndex;i<nums.size();i++)
{
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums,0);
for(auto a:mp)
{
ans.push_back(a.first);
}
return ans;
}
};
---------------------------------------------------------------------------------------------------------------------------------
题目七:47. 全排列 II
提供容器
vector<vector<int>> ans;
map<vector<int>,int> mp;
vector<int> path;
递归
for(int i=0;i<nums.size();i++)
{
if(used[i]==true) continue;
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
}
终止条件
if(path.size()==nums.size())
{
mp[path]++;
return;
}
双重去重
class Solution {
public:
vector<vector<int>> ans;
map<vector<int>,int> mp;
vector<int> path;
void backtracking(vector<int>& nums,int startIndex,vector<bool> & used)
{
if(path.size()==nums.size())
{
mp[path]++;
return;
}
for(int i=0;i<nums.size();i++)
{
if(used[i]==true) continue;
path.push_back(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
used[i]=false;
path.pop_back();
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(),false);
backtracking(nums,0,used);
for(auto a:mp)
{
ans.push_back(a.first);
}
return ans;
}
};