-
母题清单
- 77. 组合(使用回溯算法,考虑使用剪枝操作)
- 216. 组合总和 III(子题,有两个剪枝操作)
- 17.电话号码的字母组合(把数字换成了字母,原理是一样的,但这道题无法进行剪枝操作,这道题目可以讲回溯算法理解更加透彻)
- 39. 组合总和(可使用多个重复的元素,注意如果要进行剪枝操作则需先进行先对原始数组进行排序)
- 40.组合总和II(去重是本道题的难点,需要结合题解勤加思考)
- 131. 分割回文串(分割可以看做组合的变体,注意回文串的判断方法)
- 93. 复原 IP 地址(子题,注意条件限制,题解里写的不好,采用自己的写法)
- 78. 子集(很好写,不赘述)
- 90. 子集 II(子题,比母题多了个去重操作,会去重那么这道题会很简单)
- 491.递增子序列(使用了新的去重逻辑)
- 46. 全排列(注意树层遍历范围以及树枝去重方式)
- 47. 全排列 II(子题,注意树层和树枝都需要降重)
- 77. 组合(使用回溯算法,考虑使用剪枝操作)
-
解题时可以🧠里把题目想象成一个树形结构,题目就很好做了,并且不容易出错。
0、金口玉言
- 回溯法抽象为树形结构后,其遍历过程就是:
for
循环横向遍历,递归纵向遍历,回溯不断调整结果集
1、组合问题
-
题目链接:77. 组合
-
题解
- 把组合问题抽象为如下树形结构
图中每次搜索到了叶子节点,我们就找到了一个结果。相当于只需要把达到叶子节点的结果收集起来,就可以求得n
个数中k
个数的组合集合。 - 最难理解的是单层搜索的过程
回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for
循环用来横向遍历,递归的过程是纵向遍历。
如此我们才遍历完图中的这棵树。for
循环每次从startIndex
开始遍历,然后用path
保存取到的节点i
。代码如下:
可以看出for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历 path.push_back(i); // 处理节点 backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始 path.pop_back(); // 回溯,撤销处理的节点 }
backtracking
(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。backtracking
的下面部分就是回溯的操作了,撤销本次处理的结果。
- 把组合问题抽象为如下树形结构
-
代码
class Solution { private: vector<vector<int>> result; // 存放符合条件结果的集合 vector<int> path; // 用来存放符合条件结果 void backtracking(int n, int k, int startIndex) { if (path.size() == k) { result.push_back(path); return; } for (int i = startIndex; i <= n; i++) { path.push_back(i); // 处理节点 backtracking(n, k, i + 1); // 递归 path.pop_back(); // 回溯,撤销处理的节点 } } public: vector<vector<int>> combine(int n, int k) { backtracking(n, k, 1); return result; } };
- 时间复杂度:
O(n * 2^n)
- 空间复杂度:
O(n)
- 时间复杂度:
-
给出回溯算法模板
void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } }
-
这道题目还可以在进行优化,也就是剪枝操作。举一个例子,当
n = 4
,k = 4
时,那么第一层for
循环的时候,从元素2
开始的遍历都没有意义了。 在第二层for
循环,从元素3
开始的遍历都没有意义了。
所以,可以剪枝的地方就在递归中每一层的for
循环所选择的起始位置。如果for
循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。接下来看一下优化过程如下:- 已经选择的元素个数:
path.size();
- 所需要的元素个数为:
k - path.size();
- 列表中可提供的剩余元素个数:
n-i+1;
为什么要+1
?举个例子,n=4
,i=2
,此时列表剩余的元素为[2,3,4]
,也就是4-2+1 = 3
个 - 我们需要满足 列表中可提供的剩余元素个数
>=
所需要的元素个数 ,也就是n-i+1 >= k - path.size()
,最后化简得到i <= n - (k - path.size()) + 1
最后,优化后的
for
循环是:for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
优化后整体代码如下:
class Solution { private: vector<vector<int>> result; vector<int> path; void backtracking(int n, int k, int startIndex) { if (path.size() == k) { result.push_back(path); return; } for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方 path.push_back(i); // 处理节点 backtracking(n, k, i + 1); path.pop_back(); // 回溯,撤销处理的节点 } } public: vector<vector<int>> combine(int n, int k) { backtracking(n, k, 1); return result; } };
- 已经选择的元素个数:
2、组合问题II
-
题目链接:40.组合总和II
-
题解:
- 此题需要加一个
bool
型数组used
,用来记录同一树枝上的元素是否使用过。如果candidates[i] == candidates[i - 1]
并且used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1]
,也就是说同一树层使用过candidates[i - 1]
。注意在递归之前需要先对数组进行排序操作,注意vector
的初始化方法。 - 图解
- 此题需要加一个
-
代码如下,注意如果不去重则直接出现错误结果。
class Solution { public: vector<vector<int>> result; vector<int> path; int sum = 0; void backTracking(vector<int> candidates,int target,int startIndex,vector<bool> used){ if(sum > target){ return; } if(sum == target){ result.push_back(path); return; } for(int i=startIndex;i<candidates.size();i++){ // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 // 要对同一树层使用过的元素进行跳过 if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false){ continue; } sum += candidates[i]; path.push_back(candidates[i]); used[i] = true; backTracking(candidates,target,i+1,used); used[i] = false; path.pop_back(); sum -= candidates[i]; } } vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { vector<bool> used(candidates.size(),false); // 首先把给candidates排序,让其相同的元素都挨在一起。 sort(candidates.begin(),candidates.end()); backTracking(candidates,target,0,used); return result; } };
3、分割问题
-
题目链接:131. 分割回文串
-
题解
- 分割问题其实类似于组合问题,
[startIndex,i]
其实就是这里的子串范围,注意学习string
类型的子串生成方法,以及回文串判断方法。 - 图解
- 分割问题其实类似于组合问题,
-
代码如下
class Solution { public: vector<vector<string>> result; vector<string> path; bool isPalindorme(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; } void backTracking(string s,int startIndex){ if(startIndex >= s.size()){ result.push_back(path); return; } for(int i=startIndex;i<s.size();i++){ //如果是回文串 if(isPalindorme(s,startIndex,i)){ //添加子串到结果集 path.push_back(s.substr(startIndex,i-startIndex+1)); }else{ continue; } backTracking(s,i+1); path.pop_back(); } } vector<vector<string>> partition(string s) { backTracking(s,0); //cout << isPalindorme(s,0,0) << endl; return result; } };
4、复原 IP 地址
- 题目链接:93. 复原 IP 地址
- 题解
- 这道题目有很多限制条件,但大体上和上面讲的分割问题是差不多的,注意采用自己的书写方式,这道题的题解不适合自己。下面的图解没有列举出前导0的情况,需要自行添加判断条件。
- 图解
- 代码如下
class Solution { public: vector<string> path; vector<string> result; void backTracking(string s,int startIndex){ if(path.size()==4){ //还未搜索玩整个数字字符串,直接退出 if(startIndex < s.size()){ return; } //符合规定的字符串整合成一个IP地址,加入到结果集中 string temp = path[0]+"."+path[1]+"."+path[2]+"."+path[3]; result.push_back(temp); return; } //每次最多挑选三位数字 for(int i=startIndex;i<s.size() && i-startIndex<3;i++){ //如果二位数或三位数的首位为0 或者 三位数加起来超过255 则不符合规定,直接结束递归 if(i-startIndex>=1 && s[startIndex]=='0' || (i-startIndex==2 && (s[startIndex]-'0')*100+(s[startIndex+1]-'0')*10+(s[startIndex+2]-'0')>255) ){ break; } path.push_back(s.substr(startIndex,i-startIndex+1)); backTracking(s,i+1); path.pop_back(); } } vector<string> restoreIpAddresses(string s) { //减去枝叶 if (s.size() < 4 || s.size() > 12) return result; backTracking(s,0); return result; } };
5、子集
-
题目链接:78. 子集
-
题解:
- 题目很简单,跟组合非常类似,不赘述。
- 图解:
-
代码如下:
class Solution { public: vector<vector<int>> result; vector<int> path; void backTracking(vector<int> nums,int startIndex){ // 题解中直接把收集子集的操作放这样,这样就不用在主函数开头添加空元素了 if(startIndex >= nums.size()){ return; } for(int i=startIndex;i<nums.size();i++){ path.push_back(nums[i]); result.push_back(path);// 收集子集 backTracking(nums,i+1); path.pop_back(); } } vector<vector<int>> subsets(vector<int>& nums) { result.push_back(path);// 把空元素先加入到结果集中去 backTracking(nums,0); return result; } };
6、递增子序列
- 题目链接:491.递增子序列
- 题解:
- 这道题目难在去重逻辑上,之前在做去重之前需要先对数组进行排序,所以重合的元素一定会挨着,也就是说可以用
candidates[i] == candidates[i - 1] && used[i - 1] == false
这个逻辑即可,但是现在这个题目中是不能进行排序的,所以需要使用其他的去重逻辑,也就是unordered_set<int> uset;
,下文会提及到。 - 图解:
- 这道题目难在去重逻辑上,之前在做去重之前需要先对数组进行排序,所以重合的元素一定会挨着,也就是说可以用
- 代码如下:
class Solution { private: vector<vector<int>> result; vector<int> path; void backtracking(vector<int>& nums, int startIndex) { if (path.size() > 1) { result.push_back(path); // 注意这里不要加return,要取树上的节点 } unordered_set<int> uset; // 使用set对本层元素进行去重 for (int i = startIndex; i < nums.size(); i++) { if ((!path.empty() && nums[i] < path.back()) || uset.find(nums[i]) != uset.end()) { continue; } uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了 path.push_back(nums[i]); backtracking(nums, i + 1); path.pop_back(); } } public: vector<vector<int>> findSubsequences(vector<int>& nums) { result.clear(); path.clear(); backtracking(nums, 0); return result; } };
- 其实也可以使用数组来存放重复元素,这样效率会比
unordered_set
高,因为unordered_set
底层实现是哈希表。
- 其实也可以使用数组来存放重复元素,这样效率会比
7、全排列
-
题目链接:46. 全排列
-
题解:
- 每次遍历的范围和之前不一样,之前都有
startIndex
,而现在每次都是从下标0
开始遍历,遇到重复的元素就暂时跳过。注意这里的去重逻辑是used[i]==true
,这样某个祖先节点中遍历过的话就能发现并且避免掉。 - 图解:
- 每次遍历的范围和之前不一样,之前都有
-
代码如下:
class Solution { public: vector<vector<int>> result; vector<int> path; void backtracking (vector<int>& nums, vector<bool>& used) { // 此时说明找到了一组 if (path.size() == nums.size()) { result.push_back(path); return; } for (int i = 0; i < nums.size(); i++) { if (used[i] == true) continue; // path里已经收录的元素,直接跳过 used[i] = true; path.push_back(nums[i]); backtracking(nums, used); path.pop_back(); used[i] = false; } } vector<vector<int>> permute(vector<int>& nums) { result.clear(); path.clear(); vector<bool> used(nums.size(), false); backtracking(nums, used); return result; } };
-
我个人也有不一样的代码,就是每次遍历过了直接将元素删除掉,这样下一次遍历肯定不会再有上一次已经遍历过的元素。代码如下:
class Solution { public: vector<vector<int> > result; vector<int> path; void backTracking(vector<int> nums,int size){ if(path.size() == size){ result.push_back(path); return; } for(auto it = nums.begin();it != nums.end();it++){ int num = *it; path.push_back(num); nums.erase(it); backTracking(nums,size); nums.insert(it,num); path.pop_back(); } } vector<vector<int>> permute(vector<int>& nums) { int size = nums.size(); backTracking(nums,size); return result; } };