DFS刷题:77. 组合

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();
        }
    }
}

参考:https://leetcode-cn.com/problems/combinations/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-ma-/

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值