回溯法问题包括:
- 组合问题
- 子集问题
- 子序列问题
- 排列问题
回溯法问题解决起来大同小异
40.组合总和II
首先是代码模板和解答树(这一步最好在脑中有大概的想象)
(参考自https://programmercarl.com/)
https://programmercarl.com/%E5%9B%9E%E6%BA%AF%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html#%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80%E7%89%88%E6%9C%AC
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
第二步:实现细节(考虑树形结构中每一层和每一个树枝对元素的操作)
A. 如果题目要求同一元素不能重复遍历,那么我们在回溯函数中需要将每一次开始遍历的位置记录下来,记为start,每一次遍历从上一次遍历的地方+1开始
B. 如果题目中包含重复元素而且不是子序列问题,那么就先对数组进行排序,再通过回溯函数的循环中建立used数组用以记录该层元素的使用情况(由于used在每一层即每一次递归都是独立建立的,因此每层递归之间used没有影响),使用used数组和原数组排序后可以解决重复元素的问题
(为什么一定要先经过排序?那是因为假设数组{2,6,7,6,7}求不重复子集,如果不经过排序,考虑第一层即子集内元素为0,在遍历到第一个6时会生成以6为开头的子集例如{6,7},在遍历到第一个7时,由于元素7的used记录它还未被使用,那么会导致生成例如{7,6}的子集,这样就会产生重复子集。而如果我们先经过排序{2,6,6,7,7},那么在遇到第一个6时会生成{6,7},在遍历到7时后面的元素没有6那就一定不会产生重复)
C. 如果题目要求包含重复元素的子序列问题,那么就不能提前排序(因为排序后就会改变了原有的排序性质)。这时候解决重复元素的方法也是像上面一样,每层递归维护一个自身的used数组用以该层使用的元素情况
(为什么子序列问题不需要经过排序也能解决重复元素的问题呢?因为子序列问题是求不重复的递增或递减子序列,假设求递增子序列,{2,6,7,6,7},不会出现上面重复出现{6,7}{7,6}的原因是我们为了得到递增序列,我们会添加nums[i] > temp.back(),也就是说新加入的元素必须要大于结果数组中的最后一个元素,换言之就是这样的一个条件相当于提前数组排序的作用,因此不会出现重复的子序列)
D. 如果题目要求包含重复元素的排列问题(暂时碰到的是全排列问题),由于全排列问题需要包含原有数组中所有的元素,因此遍历到哪个元素后都需要遍历它之前和之后的元素,因此需要解决重复遍历同一元素以遍历重复元素的问题,这个时候就必须在递归过程维护一个数组used用以记录访问元素的情况,即将used作为递归函数的参数用以传递。used数组在每个树枝即从初始元素到最后元素整个过程生效,直到下一个元素才重置
组合问题
基础的组合问题,给定一个集合,要求输出符合条件的不重复组合(即结果数组中的元素排列不同依然为同一种结果)。
由于回溯法相当于一颗树的遍历,去重分为三种:
1、自身元素的去重(即同一位置的元素不能重复选取)(startindex)
2、树层上不能重复选择同一数值元素(排序和used,使用上一元素与当前元素对比去重)
3、树枝上不能重复选择同一数值元素(排序和used)
回溯法就相当于树层遍历等同于循环,树枝是递归,控制去重从这两个方面进行入手
不重复元素
对于不重复元素,由于数组中的元素不重复,因此只要不选择之前选择过的元素,组合就不会重复。
使用startindex标定每次开始遍历的元素,这样每次开始的位置均为上一次的下一元素,因此同一位置的元素就不会重复选取,也不会出现重复组合
77
class Solution {
public:
vector<vector<int> > result;
vector<int> path;
void getcombine(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);
getcombine(n,k,i + 1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
getcombine(n,k,1);
return result;
}
};
class Solution {
public:
vector<vector<int> > result;
vector<int> path;
void backtracking(int targetnum,int k,int sum,int startindex){
if(sum > targetnum){
return;
}
if(path.size() == k){
if(sum == targetnum)result.push_back(path);
return;
}
for(int i = startindex;i <= 9 - (k - path.size()) + 1;++i){
sum += i;
path.push_back(i);
backtracking(targetnum,k,sum,i + 1);
sum -= i;
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(n,k,0,1);
return result;
}
};
class Solution {
public:
vector<vector<int> > res;
vector<int> path;
void backtracking(vector<int>& candidates,int target,int startindex,int sum){
if(sum > target){
return;
}
if(sum == target){
res.push_back(path);
return;
}
for(int i = startindex;i < candidates.size();++i){
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,i,sum);
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates,target,0,0);
return res;
}
};
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;
}
};
重复元素
对于重复元素的情况,只靠startindex是不够的,因为可能会出现后面元素与前面元素相同的情况导致组合重复,例如1,2,3,2,3只记录startindex的话可能会出现两个2,3
树层需要去重,也就是同一层不能选择重复数值元素,树枝不需要去重,也就是每次递归新加入的元素数值可以重复(例如1,2,2,2,可以出现2,2,但不能出现两次2,2,此时只要控制第二次第一个选取的元素不为2即可)
两种方法去重:
1、使用startindex对同一树枝上上层元素自身的去重,使用排序和used对同一树层同一树枝的去重(后面全排列会用到):对元素进行排序,排序后相同元素会相邻,设置used数组或unordered_set记录元素使用情况,配合排序,分为:
1a、当前元素等于上一元素且used上一元素为false,说明同一树层使用过相同元素,跳过
1b、当前元素等于上一元素且used上一元素为true,说明同一树枝上前一层用过,没问题
1c、当前元素不等于上一元素,没问题
由于used在树层和树枝上都需要使用,因此used应为全局变量,在树枝返回(即递归返回时需要将对应值复原)
2、使用startindex对同一树枝上上层元素自身的去重,使用排序和startindex对同一树层同一树枝的去重:这里不需要上一步使用used的原因是startindex本身就是指向上一层使用元素的下一个元素,树枝就是从下一元素开始遍历,所以和上一层没关系;同一树层的元素,只要当前遍历的元素的位置大于这一层一开始的位置且当前元素等于上一元素,那么就重复了
40
使用startindex对同一树枝上上层元素自身的去重,使用排序和used对同一树层同一树枝的去重
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 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, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
used[i] = false;
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);
path.clear();
result.clear();
// 首先把给candidates排序,让其相同的元素都挨在一起。
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};
使用startindex对同一树枝上上层元素自身的去重,使用排序和startindex对同一树层同一树枝的去重
class Solution {
public:
vector<vector<int> > res;
vector<int> path;
void backtracking(vector<int>& candidates,int target,int sum,int startindex){
if(sum == target){
res.push_back(path);
return;
}
for(int i = startindex;i < candidates.size()&&sum + candidates[i] <= target;++i){
if(i > startindex&&candidates[i - 1] == candidates[i]){
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i + 1);
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
backtracking(candidates,target,0,0);
return res;
}
};
子集问题
子集问题其实就约等于组合问题,只是组合问题需要的是叶子节点(也就是最终结果),子集问题需要的是所有节点,所以只需要在组合问题上,把每一步的结果加入到res中即可
不重复元素
class Solution {
public:
vector<vector<int> > res;
vector<int> path;
void backtracking(vector<int> &nums,int startindex){
res.push_back(path);
for(int i = startindex;i < nums.size();++i){
path.push_back(nums[i]);
backtracking(nums,i + 1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};
重复元素
class Solution {
public:
vector<vector<int> > res;
vector<int> path;
void backtracking(vector<int>& nums,int startindex,vector<bool>& used){
res.push_back(path);
for(int i = startindex;i < nums.size();++i){
if(i > 0&&nums[i - 1] == nums[i]&&used[i - 1] == false){
continue;
}
path.push_back(nums[i]);
used[i] = true;
backtracking(nums,i + 1,used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());
backtracking(nums,0,used);
return res;
}
};
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
// 而我们要对同一树层使用过的元素进行跳过
if (i > startIndex && nums[i] == nums[i - 1] ) { // 注意这里使用i > startIndex
continue;
}
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums, 0);
return result;
}
};
递增子序列
递增子序列问题和组合问题中的重复元素类似,只是子序列问题,不能通过排序进行去重,但是本来排序的原因只是因为我们需要借助上一位置的元素和当前元素比较进行去重,现在求递增子序列自然要引入递增条件,因此起到排序的效果。
每次加入元素需要将他与path最后一个元素进行大小比较
也不能使用startindex和排序进行去重了,因为不能排序导致原数组中的同一数值元素并不相邻,必须要用used。used也不用在树枝中传递,因为树枝中不会重复选择同一数值元素,因为递增。但如果不是严格递增的话,那也没问题
class Solution {
public:
vector<vector<int> > res;
vector<int> path;
void backtracking(vector<int>& nums,int startindex){
if(path.size() > 1){
res.push_back(path);
}
int used[201] = {0};
for(int i = startindex;i < nums.size();++i){
if((!path.empty()&&nums[i] < path.back())||used[nums[i] + 100] != 0){
continue;
}
used[nums[i] + 100] = 1;
path.push_back(nums[i]);
backtracking(nums,i + 1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums,0);
return res;
}
};
注意
这里的递增子序列和前面的组合总和40,我尝试使用该方法,不排序不传递,每次递归建立一个used数组记录当前层的元素使用情况,测试用例没问题,但是提交的时候会出现超时问题,但组合总和不会,怀疑是candidate数组元素太大会造成超时情况,毕竟回溯法是穷举性质,40中用到15大小的candidate,递增子序列使用的是100。(详细原因没去分析)
全排列
全排列问题特殊点在于不需要startindex,因为当前遍历会遍历上一个元素之前的店(全排列和元素排序有关,例如2,3和3,2就是两种结果)
所以使用used数组记录树枝上使用过的元素情况
不重复元素
class Solution {
public:
vector<vector<int> > res;
vector<int> path;
void backtracking(vector<int>& nums,vector<int>& used){
if(path.size() == nums.size()){
res.push_back(path);
return;
}
for(int i = 0;i < nums.size();++i){
if(used[i] == 1){
continue;
}
used[i] = 1;
path.push_back(nums[i]);
backtracking(nums,used);
used[i] = 0;
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<int> used(nums.size(),0);
backtracking(nums,used);
return res;
}
};
重复元素
对于重复元素,此时需要同时考虑同一树层和同一树枝上的去重问题
所以还是使用排序去重的方法,比较上一元素和当前元素
class Solution {
private:
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 (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
if (used[i] == false) {
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 排序
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
};
分割线----------------------------------------------
6. 组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
class Solution {
public:
vector<vector<int> > res;
vector<int> temp;
void getcombinationSum2(vector<int>& candidates,int target,int ans,int start){
if(ans == target){
res.push_back(temp);
return;
}
if(ans > target){
return;
}
vector<int> flag(51,0);
for(int i = start;i < candidates.size();++i){
if(flag[candidates[i]] == 1){
continue;
}
temp.push_back(candidates[i]);
ans += candidates[i];
flag[candidates[i]] = 1;
getcombinationSum2(candidates,target,ans,i + 1);
temp.pop_back();
ans -= candidates[i];
}
return;
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
getcombinationSum2(candidates,target,0,0);
return res;
}
};
class Solution {
public:
vector<vector<int> > res;
vector<int> path;
void backtracking(vector<int>& candidates,int target,int sum,int startindex){
if(sum == target){
res.push_back(path);
return;
}
for(int i = startindex;i < candidates.size()&&sum + candidates[i] <= target;++i){
//注意这里判断语句中i > startindex要写在首位,放在
//后面的话会出现当i = 0时,判断语句先判断i - 1导致
//下标溢出,如果非要这样写的话就要加上i != 0的
//判断语句。如果按现在这样写的话,判断语句就不会先执行i - 1
if(i > startindex&&candidates[i - 1] == candidates[i]){
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i + 1);
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
backtracking(candidates,target,0,0);
return res;
}
};
216. 组合总和 III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
class Solution {
public:
vector<vector<int> > res;
vector<int> temp;
void getcombinationsum(int k,int n,int start,int ans){
if(ans > n){
return;
}
if(temp.size() == k){
if(ans == n){
res.push_back(temp);
}
return;
}
else{
for(int i = start;i <= 9;++i){
temp.push_back(i);
ans += i;
getcombinationsum(k,n,i + 1,ans);
ans -= i;
temp.pop_back();
}
return;
}
}
vector<vector<int>> combinationSum3(int k, int n) {
getcombinationsum(k,n,1,0);
return res;
}
};
90. 子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
class Solution {
public:
vector<vector<int> > res;
vector<int> tmp;
void getsubsetsWithDup(vector<int>& nums,int start){
if(!tmp.empty()){
res.push_back(tmp);
}
vector<int> used(21,0);
for(int i = start;i < nums.size();++i){
if(used[nums[i] + 10] == 1){
continue;
}
tmp.push_back(nums[i]);
used[nums[i] + 10] = 1;
getsubsetsWithDup(nums,i + 1);
tmp.pop_back();
}
return;
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int> emptyV = {};
res.push_back(emptyV);
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());
getsubsetsWithDup(nums,0);
return res;
}
};
491. 递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1]
输出:[[4,4]]
提示:
1 <= nums.length <= 15
-100 <= nums[i] <= 100
class Solution {
public:
vector<vector<int> > res;
vector<int> temp;
void getSubsequences(vector<int>& nums,int start){
if(temp.size() > 1){
res.push_back(temp);
}
int used[201] = {0};
for(int i = start;i < nums.size();++i){
if((!temp.empty()&&nums[i] < temp.back())||used[nums[i] + 100] != 0){
continue;
}
temp.push_back(nums[i]);
used[nums[i] + 100] = 1;
getSubsequences(nums,i + 1);
temp.pop_back();
}
return;
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
res.clear();
temp.clear();
getSubsequences(nums,0);
return res;
}
};
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
class Solution {
public:
vector<vector<int> > res;
vector<int> temp;
void getpermuteUnique(vector<int>& nums,vector<int>& used){
if(temp.size() == nums.size()){
res.push_back(temp);
return;
}
for(int i = 0;i < nums.size();++i){
if(i > 0&&nums[i] == nums[i - 1]&&used[i - 1] == 0){
continue;
}
if(used[i] == 1){
continue;
}
temp.push_back(nums[i]);
used[i] = 1;
getpermuteUnique(nums,used);
temp.pop_back();
used[i] = 0;
}
return;
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
res.clear();
temp.clear();
vector<int> used(nums.size() + 1,0);
sort(nums.begin(),nums.end());
getpermuteUnique(nums,used);
return res;
}
};
未完待续