代码随想录算法训练营第24天 | 回溯法基础 组合

代码随想录系列文章目录

回溯篇 + 贪心篇


文章目录

  • 代码随想录系列文章目录
  • 前言
  • 回溯法基础
    • 什么是回溯法
    • 使用原因以及解决的问题
    • 如何理解回溯法
    • 回溯模板,递归三部曲
    • **回溯算法的模板框架**
  • 77.组合


前言

因为前两个月刷过一遍回溯篇的题目了,但是贪心篇章可以说是底子比较薄弱,所以我准备一起进行这两个篇章的学习。多思考多总结。但是,每天的博客总结里,我就先只总结回溯篇章的题了。

回溯法基础

什么是回溯法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。回溯函数也就是递归函数,指的都是一个函数。

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

使用原因以及解决的问题

那么既然回溯法并不高效为什么还要用它呢?

因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。

比如给定一个集合nums, 想要搜索得出所有k 个元素的子集,比如k = 2, 我们还可以用两个循环去做,但是k不定,我们无法用k个循环去找吧。

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

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

记住组合无序,排列有序,就可以了。

如何理解回溯法

回溯法解决的问题都可以抽象为树形结构, 所以说,之前叫我要画递归树,我想在面试的时候,就画画递归树吧。

回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

回溯模板,递归三部曲

递归三部曲:
1.递归函数返回值以及参数

回溯算法中函数返回值一般为void。

参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。

2.出口,也就是递归终止条件

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

3.单层的递归逻辑

回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。

代码随想录算法训练营第24天 | 回溯法基础 组合_第1张图片

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

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

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

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

回溯算法的模板框架

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

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

77.组合

题目链接
这个题是回溯的开门鼻祖了感觉,希望这道题能牢牢记住吧,但是两个月前做的,两个月后就忘了

如果说,把参数path 写进参数里,并且每一层的遍历在参数里实现操作,就这么写就行

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        def rec(start, path, res):
            if len(path) == k:
                res.append(path)
                return
            for i in range(start, n+1):
                rec(i+1, path+[i], res)
            return res
        return rec(1, [], [])

但是如果想手动实现一下回溯这个操作的话,res.append()进的应该是path[:], 这是一种深拷贝python中深拷贝的处理方式

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        path, res = [], []
        def rec(start, n, k):
            if len(path) == k:
                res.append(path[:])
                return
            for i in range(start, n+1):
                path.append(i)
                rec(i+1, n, k)
                path.pop()
        rec(1, n, k)
        return res

剪枝,每一层的遍历到了一定程度,后面再怎么塞就凑不够k个了,所以在每层遍历时,可以提前结束

for i in range(start, n-(k-len(path))+2):

举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。就是2后面的都不用去遍历了,但是2要取

你可能感兴趣的:(代码随想录算法训练营打卡,算法,贪心算法)