1. 前言
回溯算法中最经典的就是组合与排序了,下面通过这两道题来聊聊回溯算法。
2. 介绍
首先回溯算法也是一种递归算法:
回溯算法的本质是去遍历递归树,记录状态,返回上一次状态
回溯算法中最重要的三点
- 确定递归函数的参数
- 确定递归树的广度如何遍历
- 确定递归函数的返回
在回溯算法中,通常递归代码是遍历整个递归树的深度,而for循环是控制树的广度
3.题目
77. 组合(代码如下)
注意看下面代码: 请问为什么在组合问题中,永远不会选到相同的元素(这里的相同不单单指的是数字大小相同,并且它在给出数组中的下标也是相同的)
这是因为,递归后每次for循环(也就是广度)都会从之前选择数字下标往后面推,这就能保证永远不会选到相同元素
我们只需要注意递归出口处,通过此处去控制递归树的深度。
组合问题是这个思路,那么排列呢?
代码如下:
class Solution {
List<List<Integer>> res = new LinkedList();
public List<List<Integer>> combine(int n, int k) {
dfs(1,new LinkedList(),k,n);
return res;
}
void dfs(int index,List<Integer> path,int k,int n){
if(path.size() == k){
res.add(new LinkedList(path));
return;
}else{
for(int i = index;i <= n; i++){
path.add(i);
dfs(i + 1,path,k,n);
path.remove(path.size() - 1);
}
}
}
}
我觉得排列是遍历深度后遍历广度控制一下选数的原则。
排列和组合的选数范围不同,排列可以选择所有的数字,但是必须判断这一次的数字是否已经被选择(也就是参数需要一个布尔数组)。
代码如下
class Solution {
public List<List<Integer>> permute(int[] nums) {
dfs(new boolean[nums.length],nums,new LinkedList());
return res;
}
List<List<Integer>> res = new LinkedList();
void dfs(boolean[] used,int[] nums,LinkedList<Integer> path){
if(path.size() == nums.length){
res.add(new LinkedList(path));
return;
}for(int i = 0;i < nums.length;i++){
if(used[i] == true) continue;
path.add(nums[i]);
used[i] = true;
dfs(used,nums,path);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
4.总结
组合中:通过选择当前下标之后的数字避免重复
排列中:选数的策略应该是选择没有被选择过的数字
组合和排列最大的区别是,组合的递归函数需要index,去避免选择已经选过的数字,而排列则是整个范围去择数,但是需要布尔数组,去判断在递归过程中,某个数是否已经被选。