回溯算法主要分为五个部分
组合问题
分割问题
子集问题
排列问题
棋盘问题
- 组合问题
最简单的组合问题如上,组合问题可以抽象成一颗多叉树,结点中是当前可以取的数字的集合,当一条路径中所包含的数字数量到达k,则代表找到了一个输出。
代码如下:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n , int k , int startindex ) //每层的开始index
{
// 什么时候知道到达最后一层,该取出结果了呢,就是在path的长度到达k的时候
if(path.size() == k)
{
result.push_back(path);
return;
}
// 这个循环语句可以剪枝操作
// for(int i = startindex ; i< n;i++)
for(int i = startindex;i<=n-k+path.size()+1;i++)
{
path.push_back(i);
// 相当于在树中下沉了一层
backtracking(n,k,i+1);
//递归返回后回到当前层,需要把当前层本次for循环压入的元素pop出
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
//组合问题不考虑顺序,要去重
result.clear();
path.clear();
backtracking(n,k,1);
return result;
}
};
for 循环中有剪枝操作, 因为在n个数中取k个数 , 那么应该保证 当前结点剩余的数字数量 >= 还需要的数字数量
即 k - path.size() <= n - i 。
startindex 在元素不能重复的时候需要用到。
这里的组合多出了 相加之和为n 的 k个数 ,那么找到一个组合的条件是path中有k个数的时候 && 这些数的sum是 target
递归函数的参数中 增加一个sum值,用来记录当前path中的所有数字的和,由于只需要向全局变量中的path和result增加元素,所以递归函数并不需要返回值
void backtracking (int k , int n , int startindex ,int sum)
class Solution {
public:
vector<vector<int>> result;
vector<int>path;
void backtracking(int k ,int n ,int startindex ,int sum) //相加之和为n的k个数的组合
{
//退出条件 size = k && sum = n
if(path.size() == k )
{
if(sum == n)
{
result.push_back(path);
}
return;
}
for(int i = startindex ; i<=10 -k +path.size();i++)
{
path.push_back(i);
sum += i;
backtracking(k,n,i+1,sum);
sum -= i;
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
result.clear();
path.clear();
backtracking(k,n,1,0);
return result;
}
};
电话号码的组合
这里对于不同的输入,有不同的组合,这里由于使用的不是同一个序列,所以不需要startindex来防止重复取。
首先用一个二维数组来存放各个数位之间的对应关系
const string nums[10]{
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
树的同一层都在同一个数组元素中取字符,递归往下一层的时候才会转到另一个字符串,所以这里需要一个参数来记录当前所在层次,层次可以用来取字符串。
所以递归函数定义如下:
void backtracking( string digits , int level) // level代表目前操作的是digits的第几个位置,也代表目前树递归到了第几层。
class Solution {
public:
// 数字到字母的映射
// 1 * # 等异常情况
// std::map<int, std::string> map {
// {0, ""}, {1, ""},
// {2, "abc"}, {3, "def"},
// {4, "ghi"}, {5, "jkl"},
// {6, "mno"}, {7, "pqrs"},
// {8, "tuv"}, {9, "wxyz"}
// };
const string nums[10]{
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
vector<string> result;
string path;
void backtracking(string &digits ,int level )
{
//退出条件
if(path.size()==digits.size())
{
result.push_back(path);
return;
}
int index = digits[level] - '0'; // 取出digits中的数字
string s = nums[index]; // 找到这个数字代表的字符串
// 这里不需要startindex是因为操作的是不同的字符串
for(int i = 0 ; i< s.size();i++ )
{
path.push_back(s[i]);
backtracking(digits,level+1);
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if(digits.size() == 0)
return result;
backtracking(digits,0);
return result;
}
};
这里的组合总和和上面的不同在于这里的数字可以无限重复取。
一个集合来求组合就会需要startindex
这里依旧需要一个startindex 来保证同层的元素不会取到一样的数字
( for循环代表同一层 , 在for循环中的递归是往下深入的) , 但是在当前的下一层中,依旧可以取到上层取过的数字。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates ,int target,int sum , int startindex)
{
if(sum > target) return;
if(sum == target){
result.push_back(path);
return;
}
// 不同层次间可以选一样的数字, 同层之间不能选一样的
for(int i = startindex; i < candidates.size() ;i++)
{
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i); //这里的i不用+1 表示在下一层中可以重复读当前的元素
sum -= candidates[i];
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
result.clear();
path.clear();
backtracking(candidates,target,0,0);
return result;
}
};
这题与上题的不同在于,元素之间是有重复的,对于一个组合来说,在同一树层上不能取一样的元素,因为这会导致有多个相同的子树,导致最终的结果集中会有多个相同的集合。
startindex只能保证startindex之前的数字不会再被取到,但是当存在重复的数字时,只使用startindex无法保证后面相同的数字不被取到,所以需要另外的标记来进行去重。
首先先对整个序列进行排序。
在当前树层的for循环中,需要对使用过的相同元素进行去重,考虑使用一个used数组来标识当前元素是否被选中过。
假设存在两个相邻的相同元素, 那么在同一树层中 它们表现为nums[i] = nums[i-1] ,在同一树层中,nums[i-1]会比nums[i]更先被遍历到,所以 当nums[i-1] 在本层进入递归的时候, used[i-1]为true , 而当它从递归返回到本层的时候,由于回溯, 它会被设为false ,所以,若 (nums[i-1] == nums[i] && used[i-1] == false)的时候,代表nums[i-1]在本层已经被选中过了,序列后面与它相同的元素都不应该再被选中。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates ,int target,int sum , 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 数组中,若candidates[i] = candidates[i-1],那么对cadidates[i] 来说 ,used[i-1] = true ,代表当前树枝上,前一个相同值使用过,那么树枝上相同值是可以再使用的
// used[i-1] = false,代表当前树层上,前一个相同值使用过,那么同一树层上不能再使用,防止重复的set出现
{ if(i > 0 && candidates[i] == candidates[i-1] && !used[i-1])
{
continue;
}
sum += candidates[i];
used[i] = true;
path.push_back(candidates[i]);
backtracking(candidates,target,sum,i+1 ,used);
sum -= candidates[i];
used[i] = false;
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);
result.clear();
path.clear();
sort(candidates.begin(),candidates.end());
backtracking(candidates,target,0,0,used);
return result;
}
};
那么到现在, 组合问题就已经结束了。