DFS题主要还是递归+剪枝
77. 组合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
画递归树
根据搜索起点画出二叉树
- 如果组合里有 1 ,那么需要在 [2, 3, 4] 里再找 1 个数;
- 如果组合里有 2 ,那么需要在 [3, 4] 里再找 1数。注意:这里不能再考虑 1,因为包含 1 的组合,在第 1 种情况中已经包含。
说明:
- 叶子结点的信息体现在从根结点到叶子结点的路径上,因此需要一个表示路径的变量 path,它是一个列表,特别地,path 是一个栈;
- 每一个结点递归地在做同样的事情,区别在于搜索起点,因此需要一个变量 start ,表示在区间 [begin, n] 里选出若干个数的组合;
没有剪枝的代码如下:
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res =new ArrayList<>();
if(k<=0 || n<k){
return res;
}
//给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
//由题意可知,最终结果是从1开始的
Deque<Integer> path = new ArrayDeque<>();
dfs(n, k, 1, path, res);
return res;
}
//每一个结点递归地在做同样的事情,区别在于搜索起点,因此需要一个变量 begin ,表示在区间 [begin, n] 里选出若干个数的组合;
private void dfs(int n, int k, int begin, Deque<Integer> path, List<List<Integer>> res){
//递归终止条件:path = k
if(path.size() == k){
res.add(new ArrayList<>(path));
return;
}
//遍历可能的搜索起点
for(int i=begin;i<=n;i++){
//向path中添加一个数
path.addLast(i);
//下一轮搜索,搜索起点begin+1,因为结果不允许出现重复元素
dfs(n,k,i+1,path,res);
// 重点理解这里:深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
path.removeLast();
}
}
}
剪枝(这个剪枝条件我根本就没有想到!):
如果 n = 7, k = 4,从 5 开始搜索就已经没有意义了,这是因为:即使把 5 选上,后面的数只有 6 和 7,一共就 3 个候选数,凑不出 4 个数的组合。因此,搜索起点有上界,这个上界是多少,可以举几个例子分析。
例如:n = 6 ,k = 4。
path.size() == 1 的时候,接下来要选择 33 个数,搜索起点最大是 44,最后一个被选的组合是 [4, 5, 6];
path.size() == 2 的时候,接下来要选择 22 个数,搜索起点最大是 55,最后一个被选的组合是 [5, 6];
path.size() == 3 的时候,接下来要选择 11 个数,搜索起点最大是 66,最后一个被选的组合是 [6];
再如:n = 15 ,k = 4。
path.size() == 1 的时候,接下来要选择 33 个数,搜索起点最大是 1313,最后一个被选的是 [13, 14, 15];
path.size() == 2 的时候,接下来要选择 22 个数,搜索起点最大是 1414,最后一个被选的是 [14, 15];
path.size() == 3 的时候,接下来要选择 11 个数,搜索起点最大是 1515,最后一个被选的是 [15];
可以归纳出:
搜索起点的上界 + 接下来要选择的元素个数 - 1 = n其中,接下来要选择的元素个数
= k - path.size()
搜索起点的上界 = n - (k - path.size()) + 1
所以,我们的剪枝过程就是:把
i <= n
改成i <= n - (k - path.size()) + 1
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res =new ArrayList<>();
if(k<=0 || n<k){
return res;
}
//给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
//由题意可知,最终结果是从1开始的
Deque<Integer> path = new ArrayDeque<>();
dfs(n, k, 1, path, res);
return res;
}
//每一个结点递归地在做同样的事情,区别在于搜索起点,因此需要一个变量 begin ,表示在区间 [begin, n] 里选出若干个数的组合;
private void dfs(int n, int k, int begin, Deque<Integer> path, List<List<Integer>> res){
//递归终止条件:path = k
if(path.size() == k){
res.add(new ArrayList<>(path));
return;
}
//遍历可能的搜索起点
for(int i=begin;i<=n-(k-path.size())+1;i++){
//向path中添加一个数
path.addLast(i);
//下一轮搜索,搜索起点begin+1,因为结果不允许出现重复元素
dfs(n,k,i+1,path,res);
// 重点理解这里:深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
path.removeLast();
}
}
}