[学习记录]回溯算法及其应用

目录

一、简介

二、回溯算法的应用

三、回溯算法的模板

四、回溯算法解决子集问题

        (一)子集问题描述

        (二)问题解决

        (三)代码实现

        (四)剪枝操作

五、回溯算法解决分割问题


一、简介

        回溯与递归是相辅相成的,有回溯的地方必然会使用到递归。回溯算法并非是一种高效的算法,而是一种暴力法。

二、回溯算法的应用

        对于某些问题来说,即便多层for循环也无法解决,也无法写出这种程序,这时候就需要用到回溯算法。排列、组合、子集、切割、部分棋盘等问题都可以通过回溯算法解决。

        举个简单的例子来说明为什么要使用回溯法。

        假设从包含4个元素的集合中,找出所有元素个数为2的子集,这时候我们可以通过两层for循环来解决该问题,当要求子集元素个数为3时通过3层for循环解决。那么当要求子集包含的元素个数变为k时(k为较大的值),不可能通过k层for循环来写代码,这时候就需要用到回溯算法,回溯算法其实就是通过递归来自动实现多层for循环的嵌套。

三、回溯算法的模板

        

 

四、回溯算法解决子集问题

        (一)子集问题描述

        从包含n个元素的集合中,找到所有包含k个元素的子集(对应leetcode上77题)。例如从[1,2,3,4]中找到所有包含两个元素的子集,结果有[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]。

        (二)问题解决

        从[1,2,3,4]这个具体问题来理解回溯算法,这里我们树形结构来模拟回溯算法的搜索过程。

        [学习记录]回溯算法及其应用_第1张图片

        对于图中当箭头值取2时候,剩余元素仅为[3,4],而不包含1,这是因为在子集问题中,[1,2]和[2,1]表示的是同一个子集。

        使用回溯法,要清楚三个问题。

        1.函数的参数值与返回值:在子集问题中,首先函数中传入的参数有元素个数n子集元素个数k,此外还需要传入一个参数start_idx(这个参数的意义在于从哪个位置开始递归,比如当start_idx为0时候,此时取到的值为1,那么接着递归时候就要从索引为1开始(也就是值为2的地方));此外,由于每个子集会作为一个结果,因此定义一个空列表path来存放子集(上图中[1,2]的形成,是先把1存放到列表中,然后再将2存到列表中,因此需要一个空列表),最终将所有符合条件的子集存放到一个新的列表中,因此需要另一个空列表res;由于子集问题是要将所有符合条件的子集存到一个列表中,而存放结果的操作在函数内部实现,因此不需要返回值;

        2.确定终止条件:如果终止条件设置错误,则很容易变成死循环。在子集问题中,当path中元素个数等于k值时,则终止;因此终止条件为len(path)==k;

        3.确定单层搜索逻辑:也就是如何通过递归以及回溯来实现多层循环的嵌套。

        (三)代码实现

class Solution:
    def combine(self, n: int, k: int):
        path = []
        res=[]

        def backtracking(n, k, start_idx):
            # 终止条件
            if len(path) == k:
                res.append(path[:])
                return

            # 单层逻辑
            for i in range(start_idx, n):
                path.append(i + 1)
                #递归
                backtracking(n, k, i + 1)
                #回溯
                path.pop()

        backtracking(n, k, 0)
        return res

        path用来存储子集,res存储最终结果。最外层的一次for循环(即第一次for循环)表示遍历当第一个数依次取1、2、3、4时。当每次递归函数执行完成后,执行path.pop()操作(即当找到一个符合条件的子集例如为[1,2]时,终止条件成立,执行path.pop(),此时path变为[1],下一次递归path变为[1,3],当第一个数取1完成之后,path回溯两次变为空,以此类推。

        (四)剪枝操作

        对于有些情况来说,剪枝操作可以降低时间复杂度。例如对于包含4个元素的集合,找到所有包含3个元素的子集,此时当第一个元素取3时候,已经不满足条件了,而不经过剪枝的回溯算法仍然是要对该节点的分支进行搜索,这就造成很大的时间消耗。

[学习记录]回溯算法及其应用_第2张图片

        由于代码中对每个节点所在分支进行搜索是在for循环中实现,因此只需要找到满足搜索条件的最大值即可,将n替换为n-(k-len(path))+1,简单说明一下(若当前path中存放的元素个数为0,此时计算值为4-(3-0)+1=2,由于python中for循环取不到右边值,也就是符合条件的最大索引为2-1(假设索引从0开始))。

        剪枝前后时间复杂度对比如下:

        

[学习记录]回溯算法及其应用_第3张图片

五、回溯算法解决分割问题

        待更新

 

你可能感兴趣的:(数据结构与算法,算法,学习)