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

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

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();
        }
    }
}
相关文章
|
2天前
|
机器学习/深度学习 算法 API
【Paddle】PCA线性代数基础 + 领域应用:人脸识别算法(1.1w字超详细:附公式、代码)
【Paddle】PCA线性代数基础 + 领域应用:人脸识别算法(1.1w字超详细:附公式、代码)
8 0
|
2天前
|
算法 关系型数据库 C语言
卡尔曼滤波简介+ 算法实现代码(转)
卡尔曼滤波简介+ 算法实现代码(转)
20 4
|
2天前
|
机器学习/深度学习
leetcode代码记录(旋转图像
leetcode代码记录(旋转图像
9 0
|
2天前
|
算法
leetcode代码记录(全排列 II
leetcode代码记录(全排列 II
13 4
|
2天前
|
算法
leetcode代码记录(全排列
leetcode代码记录(全排列
12 1
|
2天前
|
索引
leetcode代码记录(Z 字形变换
leetcode代码记录(Z 字形变换
11 1
|
2天前
leetcode代码记录(最长回文子串
leetcode代码记录(最长回文子串
8 2
|
2天前
leetcode代码记录(回文数
leetcode代码记录(回文数
11 1
|
2天前
|
算法
leetcode代码记录(寻找两个正序数组的中位数
leetcode代码记录(寻找两个正序数组的中位数
12 2
|
2天前
leetcode代码记录(两数之和
leetcode代码记录(两数之和
11 1