目录
- 回溯法的概念
- 回溯法的通用框架和思路
- 用回溯法解决问题的几个例子
- 小结
本文中的算法将以Python语言给出
回溯法的概念
回溯法是算法中常用的方法,回溯顾名思义,就是向前推导。下面引用维基百科对于回溯法的定义。
回溯法(英语:backtracking)是暴力搜索法中的一种。
对于某些计算问题而言,回溯法是一种可以找出所有(或一部分)解的一般性算法,尤其适用于约束满足问题(在解决约束满足问题时,我们逐步构造更多的候选解,并且在确定某一部分候选解不可能补全成正确解之后放弃继续搜索这个部分候选解本身及其可以拓展出的子候选解,转而测试其他的部分候选解)。
回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:
- 找到一个可能存在的正确的答案
- 在尝试了所有可能的分步方法后宣告该问题没有答案
在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算。
回溯法的思路和通用框架
在我学习回溯法的初期,并没有能够根据回溯法的定义来写出相应的程序,因为当时我对回溯法中取消上一步甚至上几步的计算理解的并不好,因为在二叉树、链表等问题里,如果没有指向父节点或前面节点的指针,显然是不能够做到回溯的字面意义上的向前推导的。而很多宣称用回溯法解决问题的博客,所使用的方法在初学者看来大相径庭,并不能够抓住回溯法的主干思想,因此给我初期的学习造成了很大的困惑。在看过leetcode一道回溯法题目的官方解法后,我逐渐有了自己对于回溯法的理解,下面将我对于回溯法的思路给出。
在维基百科的定义中,回溯法是暴力搜索法的一种,也就是说,回溯法的本质还是暴力搜索。而在实际的程序中,回溯法是利用递归的方法求解所有可能的情况,在触发遍历结束的条件时,判断所生成的结果是否满足要寻求结果的要求,如果满足则输出,不满足则放弃。由上述描述我们可以分析出,回溯法的递归程序要求有两个条件作为输入:
- 当前情况下的输入
- 从初始条件到当前情况下走过的路径
由当前情况下的输入,来决定接下来应该处理的所有情况;由从初始条件到当前情况走过的路径,来决定是否触发结束结束条件与是否输出结果。根据以上分析,我们可以写出以下通用框架:
def backtrack(current_input, route):
if end:
if route is legal:
output
return
else:
'''
function_1 and function_2 process
current_input and route into new_input
and new_route
'''
new_input = function_1(current_input)
new_route = function_2(route)
backtrack(new_input, new_route)
用回溯法解决问题的几个例子
根据上面的思路和通用框架,我们试图用回溯法来解决一些简单的问题。
- 电话号码的字母组合
- 生成括号
- 全排列
- 路径总和II
- 子集
电话号码的字母组合
给定一个仅包含数字
2-9
的字符串,返回所有它能表示的字母组合。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23" 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
分析:我们首先应该建立字典,来映射数字和字母。套用上面的通用框架,用回溯法来解决这个问题,首先应确定当前情况下的输入,即为所给定的字符串;从初始条件到当前情况下走过的路径,即为已经生成的字母组合。遍历结束条件,即为当前字符串已经全部遍历完,所得到的字母组合即为需要的结果,因为对结果无限制,所以不用判断直接输出即可。在未满足结束条件之前,应遍历所有可能的情况,即为当前输入字符串的第一个数字,所对应的每一个字母,都要添加到走过的路径中,并递归的执行回溯法。综上,我们已经将代码模板与具体问题的情况一一对应,可以写出如下代码。
class Solution:
def letterCombinations(self, digits):
"""
:type digits: str
:rtype: List[str]
"""
dict_ = {"2":"abc","3":"def","4":"ghi","5":"jkl","6":"mno","7":"pqrs","8":"tuv","9":"wxyz"}
res =[]
if len(digits) == 0:
return res
if len(digits) == 1:
for i in range(len(dict_[digits])):
res.append(dict_[digits][i])
return res
def backtrack(dig=digits, strs = ""):
if len(dig) == 1:
for i in range(len(dict_[dig])):
temp = strs
temp += dict_[dig][i]
res.append(temp)
return
for j in range(len(dict_[dig[0]])):
backtrack(dig[1:], strs+dict_[dig[0]][j])
backtrack()
return res
生成括号
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如,给出 n = 3,生成结果为:
[ "((()))", "(()())", "(())()", "()(())", "()()()" ]
分析:对于该问题,我们需要递归的生成有效的括号组合。因此当前情况下的输入,即为当前生成的左括号与右括号的数量,从初始条件到当前情况下走过的路径,即为已经生成的括号组合。遍历结束条件,即为已经生成有效的n对括号,在未满足结束条件之前,应该有效的添加左括号或右括号。因此,我们可以写出以下代码。
class Solution:
def generateParenthesis(self, n):
"""
:type n: int
:rtype: List[str]
"""
res = []
def backtrack(s="",left=0,right=0):
if len(s) == 2*n:
res.append(s)
return
if left < n:
backtrack(s+"(",left+1,right)
if left > right:
backtrack(s+")",left,right+1)
backtrack()
return res
全排列
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
分析:对于该问题,我们需要递归的生成所有可能的全排列。因此当前情况下的输入,还未进行遍历的,需要返回全排列的数组,从初始条件到当前情况下走过的路径,即为已经遍历的数组。遍历结束条件,即为已经遍历的数组的长度等于输入数组的长度,在未满足结束条件之前,应该对于当前输入的数组的每一个元素,将其添加到走过的路径中,从当前输入的数组中减去该元素,并递归的执行回溯法。因此,我们可以写出以下代码。
class Solution:
def permute(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
if len(nums) == 0:
return []
if len(nums) == 1:
return [nums]
res = []
def backtrack(num=nums,array=[]):
if len(array) == len(nums):
res.append(array)
return
for i in range(len(num)):
if i == 0:
backtrack(num[1:], array+[num[0]])
elif i == len(num) - 1:
backtrack(num[:-1], array+[num[-1]])
else:
backtrack(num[:i]+num[i+1:], array+[num[i]])
backtrack()
return res
路径总和II
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和sum = 22
,5 / \ 4 8 / / \ 11 13 4 / \ / \ 7 2 5 1
返回:
[ [5,4,11,2], [5,8,4,5] ]
分析:对于该问题,我们需要递归的找到所有符合条件的路径。因此当前情况下的输入,即为还未进行遍历的二叉树节点,从初始条件到当前情况下走过的路径,即为已经遍历的路径。遍历结束条件,即为遇到叶子节点,只有在满足路径之和等于目标和时才输出,在未满足结束条件之前,应该将目前节点的左子节点和右子节点分别加入路径中,并递归的执行回溯法。因此,我们可以写出以下代码。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def pathSum(self, root, sums):
"""
:type root: TreeNode
:type sum: int
:rtype: List[List[int]]
"""
if root is None:
return []
res = []
def backtrack(root, route):
if root.left is None and root.right is None:
if sum(route) == sums:
res.append(route)
return
if root.left is not None:
backtrack(root.left, route+[root.left.val])
if root.right is not None:
backtrack(root.right, route+[root.right.val])
backtrack(root, [root.val])
return res
子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
分析:对于该问题,我们需要递归的找到所有符合条件的子集。因此当前情况下的输入,即为还未进行遍历的集合部分,从初始条件到当前情况下走过的路径,即为已经遍历的路径。遍历结束条件,即为未遍历路径为空,在未满足结束条件之前,应该将未遍历集合的元素逐个加入遍历路径,并且未遍历部分为当前加入的元素之后的所有元素集合。因此,我们可以写出以下代码。
class Solution:
def subsets(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
set_ = []
if len(nums) == 0:
return [[]]
if len(nums) == 1:
return [nums, []]
nums.sort()
def backtrack(num = nums, route=[]):
set_.append(route)
if num == []:
return
for i in range(len(num)):
if i < len(num) - 1:
backtrack(num[i+1:],route+[num[i]])
else:
backtrack([],route+[num[i]])
backtrack()
return set_
小结
以上就是我自己对于回溯法的理解与示例程序,水平有限,能力一般,如有纰漏之处,欢迎各路大神指出,感激不尽。也希望看过本文的回溯法初学者,能有所收获与启示。