0介绍
回溯法是学习算法的一个坎,它将许多代码基础不够扎实的同学挡在通往更高水平的门外。究其原因,是因为回溯法本身与递归有着千丝万缕的关系,同时也要求学习者具备较强的抽象问题的能力。所以在学习回溯法之前,学习者必须熟练解决一些常见的递归问题,如二叉树的遍历、LCA问题等,这些问题leetcode均有题目,读者不妨先去练练手再回来。如果能熟练写出常见递归问题的代码,回溯法也就如庖丁解牛了。下面介绍两个回溯法练习的入门题,数组元素的全排列和全组合求解。
1 回溯法求解数组全排列
leetcode第46题考察了这个问题。问题很简单,高中生也懂,如下:
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
如果手算这个问题的答案,我们是如何计算呢?
(1)选取排列的第一个数字,可选1、2、3,按序从数组中拿出1,
(2)选取排列的第二个数字,由于1已经被选了,可选2、3,先选2,
(3)选取排列的第三个数字,由于1、2被选,只能选3,得到第一个排列[1,2,3]。
注意在第二步的时候,我们可选2、3,但是我们已经选了2;所以在第三步结束时我们退回第二步,再选3,然后进入第三步,这时可选2,这样就得到了[1、3、2]。最终退回第一步重复即可得到所有的排列。
在上述的叙述中,有一个退回的步骤,这就是我们所说的回溯。它是用递归实现的,相当于使用了一个栈保存了"案发现场",等退回上一次递归时,程序继续执行。有兴趣的读者可以画一画上面所说的过程,可以发现这个选择过程其实就是一棵树。
下面看看代码,我们用visited来标记这个数是否访问过,curNums是已经选取的数字集合,当它的size和nums的size一致时就结束递归。
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums)
{
vector<vector<int>> res;
if(nums.empty())
return res;
vector<int> curNums;
vector<int> visited(nums.size(),0);
dfs(res,nums,visited,curNums);
return res;
}
void dfs(vector<vector<int>>& res,const vector<int>&nums,vector<int>& visited,vector<int>& curNums)
{
if(curNums.size() == nums.size())
{
res.push_back(curNums);
return;
}
for(int i = 0; i < nums.size(); ++i)
{
if(!visited[i])
{
curNums.push_back(nums[i]);
visited[i] = 1;
dfs(res,nums,visited,curNums);
visited[i] = 0;//回溯,修改为未访问
curNums.pop_back();
}
}
}
};
进阶:leetcode47题。
2 回溯法求解数组全子集
这个问题题意也不难,求出一个集合的所有子集,是高中的组合问题。
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
这个问题比求解排列简单一些,如果手算的话步骤如下:
(1)从nums中得到1,[1]是子集,加入结果
(2)从nums中得到2,[1,2]是子集,加入结果
(3)从nums中得到3,[1,2,3]是子集,加入结果
从第3步退回第2步,得到[1,3]子集加入结果、退回第1步从2开始搜索即可。注意我们每次搜索均从nums当前数字的下一个数字开始搜索,这样确保结果不重复。
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums)
{
vector<vector<int>> res;
vector<int> curNums;
dfs(res,nums,curNums,0);
return res;
}
void dfs(vector<vector<int>>& res,const vector<int>& nums,vector<int>& curNums,int start)
{
res.push_back(curNums);
for(int i = start; i < nums.size(); ++i)
{
curNums.push_back(nums[i]);
dfs(res,nums,curNums,i+1);//从下一处开始搜索
curNums.pop_back();//回溯
}
}
};
进阶:leetcode90题。
3 总结
回溯法属于难者不会会者不难的方法,打好递归基础,做到心中可以模拟递归过程的话可以很快入门。