代码随想录算法训练营第二十四天 | LeetCode 77.组合

代码随想录算法训练营第二十四天 | LeetCode 77.组合

文章链接:回溯算法理论基础        组合及其剪枝优化

视频链接:回溯算法理论基础        组合        组合剪枝优化

目录

代码随想录算法训练营第二十四天 | LeetCode 77.组合

1. 回溯算法理论基础 

1.1 回溯法

1.2 回溯法的效率

1.3 回溯法解决的问题

1.4 如何理解回溯法

1.5 回溯法的模板

1.5.1 回溯函数模板返回值以及参数

1.5.2 回溯函数终止条件

1.5.3 回溯函数遍历过程

1.6 回溯算法模板框架

2. LeetCode 77.组合

2.1 思路

2.2 代码

2.3 剪枝优化思路

2.4 代码


1. 回溯算法理论基础 

1.1 回溯法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。

回溯是递归的副产品,只要有递归就会有回溯。

因此,回溯函数也就是递归函数,指的都是一个函数。

1.2 回溯法的效率

回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。

但由于有些题目用一般的暴力求解也无法求解,能用回溯法暴力求解也已经很可以了。

1.3 回溯法解决的问题

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

1.4 如何理解回溯法

回溯法解决的问题都可以抽象为树形结构。

因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。(这句话很关键)

递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

1.5 回溯法的模板

回溯三部曲:

1.5.1 回溯函数模板返回值以及参数

函数起名字为backtracking,这是业内常用起名

返回值一般是void

参数一般是先写逻辑,然后需要什么参数,就填什么参数

void backtracking(参数)
1.5.2 回溯函数终止条件

什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。

if (终止条件) {
    存放结果;
    return;
}
1.5.3 回溯函数遍历过程

在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度

注意图中,我特意举例集合大小和孩子的数量是相等的!

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    处理节点;
    backtracking(路径,选择列表); // 递归
    回溯,撤销处理结果
}

for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

backtracking这里自己调用自己,实现递归。

大家可以从图中看出for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。

1.6 回溯算法模板框架

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

2. LeetCode 77.组合

2.1 思路

  1. 这题为什么用回溯算法?因为这题想用一般的暴力求解时,那层循环是由k(数位)决定,因此嵌套循环层数无法确定。那么回溯算法如何实现嵌套循环的效果的呢?其实回溯算法就是通过递归来控制有多少层for循环,递归的每一层就是一层for循环,往后一层就是下一层for循环。
  2. 由于回溯算法能解决的问题都可以抽象成一个树形结构。
  3. 这个树形结构中叶子节点就是我们要求的所有的组合。

    层数=组合大小=回溯深度。

    可能会差异为什么第二层中取2以后子集合只剩下“34”没有“1”了呢?如果还有1,那下面叶子节点就多一个“21”,就与“12”重复了,这就是“组合”与“排列”的区别。

    以下是回溯三部曲

  4. 回溯(递归)函数的参数和返回值:返回值一般都是void,函数名backtracking,参数呢?题目的答案中一个组合就是一个数组,把这个一维数组称为path,这题取数的过程就是求路径的过程,二维数组为result,这两个数组都定义为全局变量,参数需要n和k、startIndex,n表示n个数,k表示组合大小,startIndex,这个就是用来表示子集合为什么第一个是从2开始,第二个是从3开始,就是表示要搜索的起始位置
  5. 终止条件:到叶子节点就是结束了。怎么看出是到叶子节点呢?就是结果的大小长度就是组合的大小,就是如果path.length==k就可以result.add(path)然后return了
  6. 单层搜索(递归)的逻辑:每一个节点就是一个for循环,起始位置就是从startIndex开始的,然后遍历剩余的元素,for(int i=startIndex;i<=n;i++)注意这里是小于等于n,因为最开始startIndex是从1开始的。接着就是path把i加入到数组中。然后就是进入递归函数的过程,不同的是传入的startIndex要+1,也就是i+1。然后就是回溯的过程了,path要把元素弹出去,为什么呢?比如我们已经取到了“12”了,那么我们要取“13”前是不是要把“2”先弹出才能取到我们的“12”对吧。

2.2 代码

//
class Solution {
    List<List<Integer>> result= new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        backtracking(n,k,1);
        return result;
    }

    public void backtracking(int n,int k,int startIndex){
        if (path.size() == k){
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i =startIndex;i<=n;i++){
            path.add(i);
            backtracking(n,k,i+1);
            path.removeLast();
        }
    }
}

2.3 剪枝优化思路

  1. 以这个为例子,第二层取2以后包括2也才剩下3个元素,怎么样也取不到4个数为组合了,后面也是一样,这些分支就可以做剪枝了,这个剪的枝如果不剪,那下面可能还有扩展,那么这个剪枝的效果就很明显了
  2. 回溯函数的参数和返回值:同上述普通思路
  3. 终止条件:同上述普通思路
  4. 单层搜索的逻辑:回溯算法通常都是递归函数里嵌套着for循环,而这题里每个节点都是for循环的过程。上面说到for(int i=startIndex;i<=n;i++),接着就是path把i加入到数组中。然后就是进入递归函数的过程。然后就是回溯的过程了,path要把元素弹出去。
  5. 而我们现在优化的就是要剪去节点的子孩子,有些没必要去搜索的,优化的地方就是在for循环里。
  6. 从上面的例子可以看出i=1可以继续往下搜索,i=2后面的都没必要搜索了,那我们知道path.size()是我们的当前已经选取结果的长度,我们还剩k-path.size()的元素要取,因为一共要取k个。那我们还要选取的元素至多可以让你从n-(k-path.size())+1开始搜索,+1是因为包括startIndex的位置。举个例子,4个元素选3个,最多让你从第2个元素开始选,所以就是4-3+1=2。k-path.size 表示path数组还能放几个元素, 再用n - 他们的和就是计算我从哪个下标开始能够满足path放k个元素
  7. 大多数回溯算法要做剪枝都是从这个i的范围做文章的,就是你思考是否可以做剪枝,就思考一下这个范围是不是有点大了,是否可以适当缩小一下

2.4 代码

//
class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        combineHelper(n, k, 1);
        return result;
    }

    /**
     * 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
     * @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
     */
    private void combineHelper(int n, int k, int startIndex){
        //终止条件
        if (path.size() == k){
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
            path.add(i);
            combineHelper(n, k, i + 1);
            path.removeLast();
        }
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值