回溯算法是一种基于试错思想的算法,通常用于在候选解的所有可能的情况中搜索并找出满足条件的解。它通过系统地搜索问题的解空间来寻找问题的解,当发现当前的选择不能导致满足条件的解时,就回溯到上一个状态并尝试其他选择。
回溯算法的基本思路是从问题的某一种状态开始搜索,在搜索过程中不断做出选择,并验证这些选择是否满足问题的求解条件。如果当前的选择不能导致满足条件的解,就回溯到上一个状态并尝试其他选择。这个过程一直持续到找到满足条件的解或者所有可能的选择都已尝试完毕。
回溯算法通常使用递归来实现,在递归函数中,每一层表示问题的一个状态。递归过程中不断地进行选择和回溯操作。在使用回溯算法时,需要注意定义问题的状态、定义递归函数、剪枝优化等方面的问题。
回溯算法可以应用于很多问题,例如求解数独、八皇后问题、组合问题、子集问题等。由于回溯算法能够系统地搜索问题的解空间,因此在求解一些复杂问题时,它是一种非常有效的算法。
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
全排列的问题的精髓就是通过递归的深入搜索能力来尝试错误选项,并来匹配正确答案。在这道题中,我们首先需要找到全排列问题的核心问题:如何实现全排列?在现实中,我们实现全排列一般会是这样的:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
其思路来源是,先筹备一个未选择的数字集合,随后进行N次递归,每次递归i即解决第i位的归属,取出元素后,也需要将目标元素剔除,避免重复选择。
转换成代码应该是这样的:
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
import copy # 导入copy,因为列表在进行append的时候,只是浅拷贝,即append的值会随着后续的变化而变化
combination = [] # 存放组合答案的地方
def permutation(stack, num_list): # 通过递归来验证
if len(num_list) != 0: # 当未选择的数字集合不为空时
temp = copy.deepcopy(num_list) # 深拷贝,保证列表不会出错
for i in range(len(temp)): # 将未选择的数字集合中的元素逐个选择
stack.append(temp.pop(i)) # 取出元素,保证下一层递归的时候元素-1;同时栈中加入新元素,组成新的排列
permutation(stack, temp) # 进入下一层递归
temp.insert(i, stack.pop()) # 还原原来的数字集合和栈
else:
combination.append(copy.deepcopy(stack)) # 当数字集合为空时,代表所有元素被去除,此时将该栈加入到结果中。
permutation([], nums) # 初始栈为空,未选择的数字集合位全集
return combination
但这是最优解吗?我们在这个程序中通过了深拷贝,递归传入也是通过列表传入,种种因素都表示了,该程序的运行需要消耗大量的计算资源,接下来我们需要对程序做进一步优化。
优化的思路还是需要从原来的数列的排序中寻找答案,其中比较重要的一个关键就是,如何实现排序的变化?如果我们仔细观察,排序的变化似乎也是通过两个值的交换来实现的。什么意思呢?即1 2 3
通过交换最后两个元素可以变成1 3 2
。如果交换1
和2
的位置有可以变成2 1 3
。所以,我们可以并不用维系一个栈和未选择的数字集合,仅通过交换便可以实现全排列。其核心思想是,通过交换当前位置和其身后的所有元素,并将交换后的列表传入下一层递归。由于每层仅考虑当前层已经是为出现过的排列情况,故该递归实现的全排列的全部列表也都是未见过的排列组合。
代码实现如下:
def permute(self, nums: List[int]) -> List[List[int]]:
import copy
combination = []
def permutation(head, end): # head表示当前元素,end是用来标记元素底部位置,判断结束
if head != end: # 当head != end 表示列表未到底,继续递归
for i in range(head, end): # 将当前元素与尾部前的每个元素分别交换位置,递归再还原
nums[head], nums[i] = nums[i], nums[head]
permutation(head+1, end)
nums[head], nums[i] = nums[i], nums[head]
else:
combination.append(copy.deepcopy(nums)) # 递归底层添加新的组合
permutation(0, len(nums))
接下来,我们列举关于Leetcode的三道例题,并通过贪心的方式进行求解:
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
改题目首先需要做好映射表,然后通过递归的方式对每一层的元素做选择,拼接可能的字符串,最后组合返回。这道题在回溯中属于相当简单的思路,因为并没有什么值得弯弯绕的地方。
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
mapping = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
string_list = []
def permutation(string, digits):
if len(digits) == 0 and len(string) != 0:
string_list.append(string)
elif len(digits) != 0:
for s in mapping[digits[0]]:
permutation(string + s, digits[1:])
permutation("", digits)
return string_list
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
这道题作为字符串的添加,由于其有左右对称的元素组成,所以回溯的规则发生了变化。但可以肯定的是当字符串的长度为2n
时,满足输出条件;
那接下来我们需要确定什么时候可以添加括号了。对于左括号,我们输入的条件是,当左括号的数量小于n
即可。对于右括号,我们的输入条件是小于n
且小于左括号。由于小于左括号数量必然小于n
。所以条件可以合并。
总结完以后就可以写代码了。
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
answer = []
def backtrack(string, left, right):
if len(string) == 2 * n:
answer.append(''.join(string))
return
if left < n:
string.append('(')
backtrack(string, left+1, right)
string.pop()
if right < left:
string.append(')')
backtrack(string, left, right+1)
string.pop()
backtrack([], 0, 0)
return answer
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 **n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
N皇后是相当经典的一道回溯题,其思想基本上是建立在全排列的基础上。因为如果仅仅满足行列不相同的条件,我们可以通过全排列的方式作为结果。例如4皇后的话,1234
代表第一行1的位置,第二行2的位置第三行3的位置以及第四行4的位置。然后这里还有斜线的规则,这就表示我们需要在做判断。
通过全排列的方式,我们可以限定我们的搜索空间,并在基础上进行判断是否满足斜线规则。这点改如何判断呢?即通过一次循环判断当前行所选择的列的位置和之列行列的位置的行差和列差是否相等。即第i行第j列元素和第i+n行第j+n列的元素一定是一个斜边。但需要注意,斜边有两个!所以我们需要对差值做绝对值。
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
combination = []
nums = [i for i in range(n)]
def permutation(head, end):
if head != end:
for i in range(head, end):
nums[head], nums[i] = nums[i], nums[head]
flag = True
for j in range(head):
if abs(head - j) == abs(nums[head] - nums[j]):
flag = False
if flag:
permutation(head+1, end)
nums[head], nums[i] = nums[i], nums[head]
else:
combination.append(["." * i + "Q" + "." * (n-i-1) for i in nums])
permutation(0, n)
return combination
回溯相关的题目不算太多,后续更过是和其他算法的结合,如果可以掌握以上回溯的方法,并深入理解一下,对于后续其他算法的求解会有好处哦~