【算法】回溯算法套路总结

本文总结了回溯算法的基本模板,强调了收获节点的处理,包括是否为叶子节点、是否允许重复选取数据以及剪枝和去重的方法,如used数组和set集合的使用,并探讨了它们的区别。文章通过举例说明如何在不同问题中应用这些策略,如组合、子集和全排列问题。
摘要由CSDN通过智能技术生成

目录

1、回溯算法最初的模板

2、收获节点包括剪枝、去重等操作需要在模板上加上什么东西

2.1 收获的是否为叶子节点

2.1.1 收获的是叶子节点的数据

2.1.2 收获的节点不是叶子节点的数据

2.2 能否重复选取数据

2.2.1 不能重复选取数据

2.2.2 可以重复选取数据

2.3 剪枝优化

2.4 去重

2.4.1 used 数组去重

2.4.2 set 集合去重

2.4.3 set 和 used 两者区别,该用哪个

2.5 回溯函数的返回值


        这几天刷了一下代码随想录回溯算法这一章节的题目,来总结一下一些小技巧,希望在以后能够直接看这篇文章就能想起回溯的套路

1、回溯算法最初的模板

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

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

2、收获节点包括剪枝、去重等操作需要在模板上加上什么东西

2.1 收获的是否为叶子节点

2.1.1 收获的是叶子节点的数据

        如果得到的结果收获的是叶子节点的数据,在收获结果的时候,需要加上 return; 语句。

        举个例子,什么叫收获叶子节点的数据,例如力扣第77题:组合

 

从图中我们可以看到,最终的结果都是从叶子节点中拿到的

2.1.2 收获的节点不是叶子节点的数据

        反之,当不是从叶子节点中拿到数据的时候就不需要加上 return; 谈起这个原因是因为如果我不是从叶子节点取得结果,我就需要再往下去递归我这棵树,所以不能直接返回,例如:力扣第78题:子集

2.2 能否重复选取数据

        简单来说就是,有些一个是组合问题,对于组合来说[1,2] 和 [2,1] 是一样的,是需要去重的,但是对于排列问题来说,[1,2] 和 [2,1]是不一样的。当然不仅是组合,分割,子集也都是和组合问题一样,一直往后取的,不能重复的取之前的数据,所以需要用startIndex 来标注从哪里开始下一轮递归。

2.2.1 不能重复选取数据

        首先讲一下什么叫不能重复选取数据,在上面2.1.1 和 2.1.2中,我们可以看到,

        在取了2之后,就只能再3、4中去取数据了,而不是在1、3、4中获取数据,如果可以的话,就会出现 [1,2] 和 [2,1] 这两种相同的情况了。

        所以,在进行遍历的过程中,在for循环的开始的时候,初始值就应该从 startIndex 开始,每次递归进去的时候都是让 startIndex + 1,这样就可以保证每次进去都不会选到上面那个。

2.2.2 可以重复选取数据

先看一下上面叫可以重复选取数据,例如力扣第46题:全排列

 此处我们可以看到,用一个used数组来表示哪个元素已经用过了

2.3 剪枝优化

        在有些问题中,需要的结果是固定的,例如力扣第77题:组合,题目要求结果中有 k 个数据,所以如果在剩下来需要递归的集合中,已经不满足有那么多个数据了的话,就可以直接不进行继续递归了。

        在已经选择的 path 中,已经选择的元素的个数是 path.size();

        所以剩下来所需要的元素个数是 k - path.size();

        列表中剩余的元素的个数是 n - i 个,n 代表总的个数,i 代表已经循环到了哪一个,所以,如果列表中剩下来的个数少于需要的元素的个数,那么就显然不成立,如果要成立,即

        k - path.size() <= n - i    ----------->   i <= n - k + path.size()

2.4 去重

2.4.1 used 数组去重

        首先 used 数组听名字就知道是代表,已经使用过的元素,这里还需要在介绍一个 Carl 提出的概念,就是树枝去重树层去重。以力扣第40题:组合总和II为例,

        从图中最左边的 used[1,1,0] 中我们可以看到,当一个路径中(树枝),选取了两个相同的元素,用代码表示就是

i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true

        从图中中间的 [1,0,0] 和 [0,1,0] 中我们可以看到,[1,0,0]选取的是第一个1,[0,1,0]选取的是第二个1,如果第二个还要选的话就会出现两个 [1,2],这样显然是不行的,所以我们需要将第二中,即图中画X的那条路给去掉,用代码表示就是

i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false

2.4.2 set 集合去重

        used数组去重是有一定限制的,即数组要是排序过的,因为used数组判断的是当前的值和上一个的值是否是一样的,然后才能去重,但是并不是所有的数组都可以先进行排序的,例如力扣第491题:递增子序列,由于需要求一个数组中递增的子序列,如果先进行了排序的话明显就会出问题。

unordered_set<int> uset;
!path.empty() && nums[i] < path.back() || uset.find(nums[i]) != uset.end()

2.4.3 set 和 used 两者区别,该用哪个

        使用set去重的版本相对于used数组的版本效率都要低很多

        原因在回溯算法:递增子序列 (opens new window)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。

2.5 回溯函数的返回值

        当最终的结果只需要一个的时候,回溯函数的返回值为 bool 类型,因为我只需要找到一个成功的路径就需要立刻返回,相当于找从根节点到叶子节点一条唯一路径,例如力扣第37题:解数独

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值