算法学习—回溯法
1、回溯法的概念
回溯法,顾名思义就是不断地重复枚举结果集,直到找到我们所要的结果。这种思想类似于暴力搜索,但是又与暴力搜索有着不同,回溯法使用了剪枝函数,除去了一些错误的解法。
回溯法在搜索的过程中,如果发现这一步的结果不符合结果的要求,则会放弃下面的查找,回溯到上一步,更换一种新的查找方式,它其实是一种选优搜索,根据选优条件逐步搜索,以达到目标。回溯法中满足回溯条件的点我们称为 “回溯点”。
在一些复杂问题,或者是比较大规模的问题中,我们都可以使用回溯法来解决,所以回溯法也有着“通用解题方法”的美称。
2、基本思想
回溯法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条新的路尝试。回溯法是一个既带有系统性又带有跳跃性的的搜索算法。它在包含问题的所有解的解空间树中,按照深度优先的策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的系统搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。而回溯法在用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种以深度优先的方式系统地搜索问题的解的算法称为回溯法,它适用于解一些组合数较大的问题。
3、解题步骤
1.定义一个解空间,它包含问题的解;
2.利用适于搜索的方法组织解空间;
3.利用深度优先法搜索解空间;
4.利用限界函数避免移动到不可能产生解的子空间。
4、实际例子
4.1、全排列问题
题目:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例1:
1 | 输入:nums = [1,2,3] |
示例2:
1 | 输入:nums = [0,1] |
提示:
1 | 1 <= nums.length <= 6 |
解题思路:
这个全排列问题是一个典型的回溯法问题。从左到右,我们不断尝试所有可能的结果,如果有出现重复的结果就除去,
我们定义递归函数 backtrack(first,output) 表示从左往右填到第 first 个位置,当前排列为 output。 那么整个递归函数分为两个情况:
- 如果 first=n,说明我们已经填完了 n 个位置(注意下标从 0 开始),找到了一个可行的解,我们将 output 放入答案数组中,递归结束。
- 如果 first<n,我们要考虑这第 first 个位置我们要填哪个数。根据题目要求我们肯定不能填已经填过的数,因此很容易想到的一个处理手段是我们定义一个标记数组 vis 来标记已经填过的数,那么在填第 first 个数的时候我们遍历题目给定的 n 个数,如果这个数没有被标记过,我们就尝试填入,并将其标记,继续尝试填下一个位置,即调用函数 backtrack(first+1,output)。回溯的时候要撤销这一个位置填的数以及标记,并继续尝试其他没被标记过的数。
示例图:
完整代码:
1 | class Solution { |
4.2、括号生成问题
题目:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例1:
1 | 输入:n = 3 |
示例2:
1 | 输入:n = 1 |
提示:
1 | 1 <= n <= 8 |
解题思路:
我们可以生成所有 2的2n次方个 ‘(’ 和 ‘)’ 字符构成的序列,然后我们检查每一个是否有效即可。
如果左括号数量不大于 n,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。
完整代码:
1 | class Solution { |
4.3、子集问题
题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例1:
1 | 输入:nums = [1,2,3] |
示例2:
1 | 输入:nums = [0] |
提示:
1 | 1 <= nums.length <= 10 |
解题思路:
类似于全排列问题。
完整代码:
1 | class Solution { |