回溯算法团灭leetcode组合数
算法介绍:
首先,我们来介绍以下什么是回溯算法?
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
简单来说,回溯算法就是遍历所有的可能,直到找到满足要求的那种情况。事实上,回溯算法和深度优先搜索并没有什么太大的区别!
废话不多说,直接来算法框架:
res = [];
void backtrace(路径, 选择列表){
if (path满足要求){
res.push_back(path);
return;
}
for (auto 选择:选择列表){
做选择;
backtrace(路径, 选择列表);
撤销选择;
}
}
以上框架就是回溯算法的伪代码框架,我们的代码核心就是for循环里面的每一次选择,话不多说,我们从题目中学习。
39.组合总和
首先,我们每拿到一道回溯算法的题目,我们就需要将这个决策树给画出来,从题目的意思我们可以画出这么一棵树:
每一个箭头就是一次选择,圆圈里的数就是当前数的和,如果它满足target,那么就把这条路劲加入到res中。
我们可以先考虑没有剪枝的情况,按照上面给出的回溯算法框架,我们应该这么写:
vector<vector<int> > res;
vector<int> path;
void dfs(vector<int>& nums, int target, int sum){
if(sum > target) return;
if(sum == target){
res.push_back(path);
return ;
}
for(int i=0;i<nums.size();i++){
sum += nums[i];
path.push_back(nums[i]);
dfs(nums,target,sum);
path.pop_back();
sum -= nums[i];
}
}
按照没有剪枝的算法来写,跑出来的结果如下图所示:
我们可以看到,答案要求我们不考虑顺序,那么我们应该如何考虑把重复的枝叶减去呢?
遇到这一类相同元素不计算顺序的问题,我们在搜索的时候就需要按某种顺序搜索。具体的做法就是:每一次搜索的时候设置下一轮搜索的起点start,请看下图:
也就是说,我们在遍历第二个数的时候就不再考虑第一个数,因此我们可以考虑使用一个start变量来记录当前选择的位置,具体代码如下:
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
dfs(candidates, target, 0, 0);
return res;
}
void dfs(vector<int>& candidates, int target, int sum, int start){
if(sum > target) return;
if(sum == target){
res.push_back(path);
return ;
}
for(int i=start;i<candidates.size();i++){
sum += candidates[i];
path.push_back(candidates[i]);
dfs(candidates, target, sum, i);
path.pop_back();
sum -= candidates[i];
}
}
};
40.组合总和Ⅱ
在看到这一题,这一题和上一题的不同在于,candidates数组里的数字,只能使用一次,那我是不是可以每次选择的时候,不从start开始,而从start+1开始呢?
我们先按照这种思路跑一个结果看看:
vector<vector<int> > res;
vector<int> path;
void dfs(vector<int>& nums, int target, int sum, int start){
if(sum > target) return;
if(sum == target){
res.push_back(path);
return ;
}
for(int i=start;i<nums.size();i++){
sum += nums[i];
path.push_back(nums[i]);
dfs(nums,target,sum, i+1);
path.pop_back();
sum -= nums[i];
}
}
按照上面的代码,跑出来的结果是这样的:
我们可以看到[1,2,5]和[2,1,5]重复了,[1,7]和[7,1]重复了,那么我们应该怎么减枝呢?
这时候,我们可以考虑先对数组进行升序排序,重复的元素一定不是排好序以后相同的连续数组区域的第i个元素。也就是说,剪枝发生在:同一层数值相同的结点第2,3…个结点,因为数值相同的第1个结点已经搜索除了包含了这个数值的全部结果,同一层的其它结点,候选数的个数更少,搜索出的结果一定不会比第1个结点更多,并且是第1个结点的子集。
具体实现如下:
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
dfs(candidates, target, 0, 0);
return res;
}
void dfs(vector<int>& candidates, int target, int sum, int start){
if(sum > target) return;
if(sum == target){
res.push_back(path);
return;
}
for(int i=start;i<candidates.size(); i++){
if(i>start && candidates[i] == candidates[i-1]) continue;
sum += candidates[i];
path.push_back(candidates[i]);
dfs(candidates, target, sum, i+1);
path.pop_back();
sum -= candidates[i];
}
}
};
216.组合总和Ⅲ
对于这一道题,和之前的不一样的点就是在于,不能有重复的数字,那不就是在下一个数字的位置进行选择吗?
设置一个i=start变量记录当前选择的位置,然后依次i+1来选择,具体代码如下:
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
int hash[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
public:
vector<vector<int>> combinationSum3(int k, int n) {
dfs(k, n, 0, 0, 0);
return res;
}
void dfs(int k, int n, int depth, int sum, int start){
if(depth == k && sum == n){
res.push_back(path);
return ;
}
for(int i=start;i<9 && sum < n;i++){
sum += hash[i];
path.push_back(hash[i]);
dfs(k, n, depth+1, sum, i+1);
path.pop_back();
sum -= hash[i];
}
}
};
我们用这三道题来讲解回溯算法,希望大家能够对这个算法能够有所了解,如果能够理解这三道题,接下来两道二叉树的题,不也是一样的套路吗?
113.路径总和Ⅱ
事实上,回溯算法,或者说深度优先搜索算法,在二叉树中的使用,其实就是树的前、中、后序遍历,如果你能够真正理解树的前中后序遍历,再结合本文,一定可以解答这道题:
我们每一次的选择,不就是选择左子树或者右子树吗?
具体代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
public:
vector<vector<int>> pathSum(TreeNode* root, int sum) {
if(root == NULL) return res;
dfs(root, sum);
return res;
}
void dfs(TreeNode* root, int sum){
if(root == NULL) return;
sum -= root->val;
path.push_back(root->val);
if(!root->left && !root->right && !sum){
res.push_back(path);
}
dfs(root->left, sum);
dfs(root->right, sum);
path.pop_back();
sum+=root->val;
}
};
最后还是希望多多实践,多多思考,多多总结,才能真正理解这个算法!