回溯算法思想回溯算法解题模板与回溯算法题目索引(不断更新)

Posted 阿宋同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回溯算法思想回溯算法解题模板与回溯算法题目索引(不断更新)相关的知识,希望对你有一定的参考价值。

回溯算法

  • 回溯算法是一种试探性的搜索算法,它在解决某些组合问题、优化问题、路径问题等,非常有效。回溯算法的核心思想是通过递归和深度优先搜索(DFS)来搜索问题的解空间

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

回溯算法的主要步骤如下:

  1. 定义问题的解空间:首先要确定问题的解空间,它通常是一个树形结构,其中每个节点表示一个局部解或部分解。

  2. 深度优先搜索:从根节点开始,沿着深度方向逐个搜索节点。每次递归时,都会传递当前的解或部分解。

  3. 试探与回溯:当搜索到某个节点时,首先检查该节点是否满足问题的约束条件。如果满足,就继续向下搜索;如果不满足,就回溯到上一个节点,尝试其他可能的选择。这就是所谓的“试探与回溯”。

  4. 记录解或剪枝:在搜索过程中,当找到一个可行解或者达到搜索的最大深度时,需要记录这个解。在某些情况下,还可以通过剪枝(例如,剪去不可能产生更优解的子树)来加速搜索过程。

  5. 重复以上步骤,直到搜索完整个解空间或满足停止条件。

回溯算法的优缺点:

  • 优点:
    适用范围广泛:回溯算法适用于解决许多组合问题和优化问题,如八皇后问题、旅行商问题、图着色问题等。
    容易理解和实现:回溯算法的逻辑较为简单,容易理解和实现。

  • 缺点:
    效率可能较低:回溯算法可能需要遍历整个解空间,当解空间很大时,算法的效率可能较低。
    剪枝策略依赖于问题:在某些情况下,可以通过剪枝来加速搜索过程,但剪枝策略通常依赖于问题的特点,可能需要针对不同问题进行调整。

总之,回溯算法是一种通用、易理解的搜索算法,可以有效解决许多组合问题和优化问题。但需要注意,在解空间较大时,可能需要使用剪枝等手段提高搜索效率。

回溯算法解题模版

  • 这里借用我最崇拜的Carl哥总结的回溯算法模板。

回溯算法解题三部曲:

第一步,找出回溯函数模板返回值以及参数

  • 在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。

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

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

  • 回溯函数伪代码如下:

    void backtracking(参数)
    

第二步,确定回溯函数终止条件

  • 既然是树形结构,就知道遍历树形结构一定要有终止条件,所以回溯也有要终止条件。

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

  • 所以回溯函数终止条件伪代码如下:

    if (终止条件) 
    	存放结果;
     return;
    
    

第三步,回溯搜索的遍历过程

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

  • 如图:

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

  • 回溯函数遍历过程伪代码如下:

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) 
    	处理节点;
    	backtracking(路径,选择列表); // 递归
    	回溯,撤销处理结果
    
    
  • for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。

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

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

  • 分析完过程,回溯算法模板框架如下:

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

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

这份模板很重要,后面做回溯法的题目都靠它了!

回溯算法题目索引

算法模板-回溯

简介

回溯法常用于遍历列表所有子集,是 DFS 深度优先搜索一种,一般用于全排列,穷尽所有可能,遍历的过程实际上是一个决策树的遍历过程。时间复杂度一般 O ( N ! ) O(N!) O(N!),它不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都比较高。

解题模板

回溯算法的解题一般是递归实现,可以抽象如下。最核心的思路就是从选择列表里做一个选择,然后一直递归往下搜索答案,如果遇到路径不通(搜索完成),就返回来撤销这次选择。

result = []
func backtrack(选择列表,路径):
    if 满足结束条件:
        result.add(路径)
        return
    for 选择 in 选择列表:
        做选择
        backtrack(选择列表,路径)
        撤销选择

回溯的题目我们可以理解为一棵树的搜索过程,一方面要考虑树往深度推广(下一层),另一方面也要考虑树往广度推广(这一层)。其实回溯算法关键在于:不合适就退回上一步,然后通过约束条件,,减少时间复杂度。这两个层面正是发生在深度和广度上的。

题目列表

下面是力扣主站题库里典型的回溯求解的题。

题解列表

39-组合总和

原题链接

这道题我们也可以用回溯的思路来求解所有可能的结果,先选定第i个元素,然后回溯后面位置的可能组合。

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        if not candidates or min(candidates) > target: return []
        candidates.sort()
        ans = []
        
        def backtrack(candidates, target, tmp=[]):
            if target == 0: ans.append(tmp)
            if target < 0: return
            for i in range(len(candidates)):
                if candidates[i] > target:
                    break
                backtrack(candidates[i:], target - candidates[i], tmp + [candidates[i]])
        
        backtrack(candidates, target)
        return ans

40-组合总和 II

原题链接

这道题和上一题类似,区别只是每个数字只能使用一次,因此递归的时候下标要向后一位。

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        if not candidates or min(candidates) > target: return []
        candidates.sort()
        ans = []
        
        def backtrack(i, tmp_sum, tmp=[]):
            if tmp_sum == target:
                ans.append(tmp)
                return 
            
            for j in range(i, len(candidates)):
                if tmp_sum + candidates[j] > target: break
                if j > i and candidates[j] == candidates[j-1]: continue
                backtrack(j+1, tmp_sum + candidates[j], tmp + [candidates[j]])

        
        backtrack(0, 0)
        return ans

46-全排列

原题链接

这道题要求生成全排列,组合是排列的子集,排列认为位置不同就不是同一个结果。

这道题和子集那题很像,需要注意写法的区别,子集那题我们不断后移起始位置,但是本题我们始终保证全部位置供选择。

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        res = []
        def backtrack(nums, tmp=[]):
            if not nums: 
                res.append(tmp)
                return 
            for i in range(len(nums)):
                backtrack(nums[:i] + nums[i+1:], tmp + [nums[i]])
        
        backtrack(nums)
        return res

47-全排列 II

原题链接

这道题很上一题类似,我们需要做个去重。

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        res = []
        def backtrack(nums, tmp=[]):
            if not nums and tmp not in res: 
                res.append(tmp)
                return 
            for i in range(len(nums)):
                backtrack(nums[:i] + nums[i+1:], tmp + [nums[i]])
        
        backtrack(nums)
        return res

78-子集

原题链接

这题是要求数组的所有子集,是回溯题中非常经典的题,我们可以很容易地想到,以i位置为第一个元素加上后续元素的子集。

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        
        ans = []

        def backtrack(start, tmp=[]):
            ans.append(tmp)
            for i in range(start, len(nums)):
                backtrack(i+1, tmp + [nums[i]])
            
        backtrack(0, [])

        return ans

90-子集 II

原题链接

这题排序后去重就行。

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        ans = []
        nums.sort()
        def backtrack(start, tmp=[]):
            if tmp not in ans:
                ans.append(tmp)
            for i in range(start, len(nums)):
                backtrack(i+1, tmp + [nums[i]])
            
        backtrack(0, [])

        return ans

补充说明

本文介绍了生成全排列等问题常见的回溯算法,但是解题时回溯用的可能并不多,因为本质上它就是一种暴力算法。

以上是关于回溯算法思想回溯算法解题模板与回溯算法题目索引(不断更新)的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode回溯算法#07子集问题I+II,巩固解题模板并详解回溯算法中的去重问题

算法模板-回溯

算法模板-回溯

算法模板-回溯

算法设计与分析——回溯法算法模板

回溯算法及题目