例题:
这题可以使用k层for循环去穷举,但一旦k值变大,for循环的嵌套就变得麻烦。
故 对于这种复杂的组合问题(以及子集、分割、排列)问题,光用for循环嵌套有可能无法得出解。
这时,我们使用回溯(搜索法)来做层叠嵌套(相当于k层for循环),每一次递归中(回溯是递归的副产品,有递归就会有回溯,我们说的回溯法一般也指的是递归法)嵌入一个for循环,就可以解决多层for循环的嵌套问题了(这里可以看出在本质上,递归也是一种穷举)。
回溯三部曲:
1.递归函数的参数以及返回值
递归函数的参数包含所有需要在递归过程中处理的参数,在这题中,我们要把值n和取值个数k传入回溯函数,以及一个index变量,这个参数用来记录本层的递归中,集合从哪里开始遍历(这题中集合就是[1,...,n] )。
2.递归结束的条件
每一层递归何时结束,就是递归结束条件决定的,一般是如图这种形式:
if (终止条件) {
存放结果;
return;
}
在这题中,当数组里存放的数字超过k个,这一层的递归就结束了。
3.单层循环的逻辑:
对每次循环的行为进行定义。一般来说,在每层递归中,我们需要对余下数据(从index+1的地方收集数据,构成组合)进行处理,即把它们加入数组中组成不同的组合,直到该路径的数组大小=k,每收集够足够数量的数据,我们将其保存后寻找下一个组合(即把数组的元素弹出,放入新数据),格式一般如图:
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
code:
lass Solution {
public:
vector<vector<int>> result;
vector<int> path;
vector<vector<int>> combine(int n, int k) {
backtracing(n,k,1);
return result;
}
void backtracing(int n,int k,int index)
{
if(path.size() == k)//终止条件+存放结果
{
result.push_back(path);
return ;
}
//单层搜索逻辑
for(int i=index;i <= n;i++)
{
path.push_back(i);
backtracing(n,k,i+1);
path.pop_back();
}
}
};
剪枝优化:
若k=4,且当前index到n的范围里的数字已经小于4了,这时若再进行逐一遍历已经没有意义了,平白无故还耗费了时间开销,若我们可以对剩下的数据进行判断,减少无畏的遍历过程,就是剪枝。上述情况发生在for循环体,我们可以这样解决,如图:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
这样就减少了不必要的开销,加快循环处理的过程
相似题目:
这题的不同之处在于多了一些条件,比如目标和值为n,这个时候我们在回溯过程中要记录当前的和值sum,当sum=n并且存储的数据大小为k时,符合我们的要求,才将这组数据加入result数组中 。所以在回溯循环体中,当回溯返回上一级循环时,我们需要把sum也进行减去的操作
code:
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
vector<vector<int>> combinationSum3(int k, int n) {
backtracing(n,k,1,0);
return result;
}
void backtracing(int n,int k,int index,int sum)
{
if(path.size() == k)
{
if(n == sum) result.push_back(path);
return ;
}
for(int i=index;i<=9;i++)
{
path.push_back(i);
sum += i;
backtracing(n,k,i+1,sum);
sum -= i;
path.pop_back();
}
}
};