回溯
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
回溯算法将解空间看作一定的结构,通常为树形结构,一个解对应于树中的一片树叶。算法从树根(即初始状态出发),尝试所有可能到达的结点。当不能前行时就后退一步或若干步,再从另一个结点开始继续搜索,直到尝试完所有的结点。也可以用走迷宫的方式去理解回溯。
模板框架
回溯算法一般是由递归调用,每次递归前做出选择,递归的方向改变,递归完成之后,撤销之前做出的选择。优化的过程就是搜索到目标之后,或者明显不满足目标条件的时候,及时进行剪枝返回,减少多余运行时间。
相关题目
17. 电话号码的字母组合
题目:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例: 输入:“23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
- 分析:每一个2有[a,b,c]三种情况,所以首先就要建立一个数字对应的字符字典。例如:‘2’:[‘a’, ‘b’, ‘c’]。这样才能够取到每一个值。之后就是回溯算法,每次选个一个数字,用for循环去选择数字所含的字母,然后将其组合起来。
- 解题模板:
from typing import List
class Solution:
def letterCombinations(self, digits:int):
res = []
dict = {
'2':['a', 'b', 'c'],
'3':['d', 'e', 'f'],
'4':['g', 'h', 'i'],
'5':['j', 'k', 'l'],
'6':['m', 'n', 'o'],
'7':['p', 'q', 'r', 's'],
'8':['t', 'u', 'v'],
'9':['w', 'x', 'y', 'z']
}
next_digit = list(str(digits))
def rec_char(combination, next_digit):
#首先是写出每次递归的终止条件:即写一个没有数字时,则表示一次递归结束
if len(next_digit) == 0:
res.append(combination)#每次递归结束,表示树达到最深,则将输出加入列表
else:#本次没有达到树的最深处,则继续往下递归
for c in dict[next_digit[0]]:#对本次数字内的字符进行循环
rec_char(combination + c, next_digit[1:])
rec_char("", next_digit)
return res
22. 括号生成
题目:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
- 分析:生成括号分为左括号和右括号,在递归的时候首先就需要设置终止条件。其中这道题的终止条件有两个:
- 1.字符的个数达到上限。
- 2.在生成字符的过程中,根据前面的字符串看来明显不能成立,要及时止损,提前终止。中val就是字符串的值,设定左括号为+1,右括号为-1,不难发现。一旦字符串的值为负数,则该字符串一定不成。所以可以使用这个判断进行提前终止生成。还有一种方法就是在生成括号的时候,对左右括号的个数进行限制,因为设定了括号数量上限,如果要使得结果成立,那么左右括号应该各占一半的数量。这样即相当于提前终止,又对字符串的数量进行了限制,一举两得。
- 解题模板:
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ans = []
def double_tree(S = '',left = 0, right = 0):
if len(S) == 2*n:
ans.append(S)
return
if left < n:
double_tree(S + '(', left + 1, right)
if right < left:
double_tree(S + ')', left , right + 1)
double_tree()
return ans
37. 解数独
题目:编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。空白格用 ‘.’ 表示
- 分析:使用回溯的方法进行填空,每一填入一个数字然后对其合法性进行判断。所以我们还需要一个验证合法性的一个,验证合法性,需要对应行,对应列,所在九宫格三个地方不存在重复。
- 优化:
- 建立一个函数,首先对将要填入的数字进行有效性判断
- 回溯函数:首先写出弹出条件,这里是行数为9 的时候弹出,但是注意,由于是回溯,所以会有很多层,我们找到一个答案就想返回时,还会返回到上一层继续计算。这个时候需要逐层一直返回。这个使用的方法就是 return True。
- 为什么使用return True? 其实回溯的时候,回溯函数并不是直接写回溯函数,而是使用的 if 回溯函数的形式。这样就可以当得到一个正解的时候一层一层的回溯到最外层。
- 弹出条件除了回溯函数的首部,还有一处就是在for 循环内对原始的数字进行跳过的时候,如果这时候后面几个数字早已语设定好了的,那么直接可以递增到 row==9,输出正解,开始逐层回传。
- 解题模板:
class Solution(object):
def solveSudoku(self, board):
"""
:type board: List[List[str]]
:rtype: None Do not return anything, modify board in-place instead.
"""
out = []
def is_valid(row, col, num, board):#验证将填入数字的有效性
for i in range(9):
if i != row and str(num) == board[i][col]:#验证同一列
return False
if i != col and str(num) == board[row][i]:#验证同一行
return False
#根据索引将数字确定所在哪一块3*3区域
i_index = row // 3 * 3
j_index = col // 3 * 3
for i in range(3):#验证该3*3区域是否有重复
for j in range(3):
if i+i_index != row and j+j_index != col and \
str(num) == board[i+i_index][j+j_index]:
return False
return True
def backtrack(board, row, col):#定义回溯函数
if row == 9:#由于行数为[0,8],行数为9的时候代表填入完毕
out.append(board.copy())
return True
for k in range(1,10):#对1-9内的数字尝试填入
while board[row][col] != '.':#从左到右,从上到下扫描,找到待填入的位置
if col < 8:
col += 1
elif col == 8:
row += 1
col = 0
if row == 9:
out.append(board)#填入完毕
return True
if is_valid(row,col,k,board):#找到待填入的位置,验证是否可以填入
board[row][col] = str(k)#如果可以就填入该数字,进行下一轮回溯
if backtrack(board, row if (col<8) else row + 1, (col + 1)%9):
return True
board[row][col] = '.'#撤销上次的填入
return False
backtrack(board, 0 , 0)
return out[0]
39. 组合总和
题目:给定一个无重复元素的数组 candidates 和一个目