回溯实际上是一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
说到底,实际上回溯算法就是一个N叉树
的前序遍历
加上后序遍历
。
//二叉树遍历框架
func traverse(root) {
if root is nil {
return
}
//前序遍历代码
traverse(root.left)
//中序遍历代码
traverse(root.right)
//后序遍历代码
}
//N叉树遍历框架
func traverse(root) {
if root is nil {
return
}
for child := range root.children {
//前序遍历代码
traverse(child)
//后序遍历代码
}
}
回溯算法就是N叉树的遍历,这个N等于当前可做的选择(choices)的总数,同时,在前序遍历的位置作出当前选择(choose过程),然后开始递归,最后在后序遍历的位置取消当前选择(unchoose过程)。伪代码如下:
/**
* choiceList:当前可以进行的选择列表
* track : 可以理解为决策路径,就是已经作出一系列的选择
* answer : 用来存储我们符合条件的决策路径
*/
func backtrack(choiceList, track, answer) {
if track is OK {
answer.add(track)
} else {
for choice as range choiceList {
// choose: 选择一个choice加入track
backtrack(choices, track, answer)
// unchoose: 从track中撤销上面的选择
}
}
}
可以看出,回溯算法的核心其实就是一个N叉树的遍历,回溯算法相当于一个决策过程,递归的遍历一颗决策树,穷举所有的决策,同时把符合条件的决策挑出来。
如果你要吃饭,但是要去哪里吃呢,具体吃什么呢?这就是一个决策问题。
那么计算机会如何选择呢?计算机处理问题的方法就是穷举,这里计算机会穷举并比较所有的决策,然后选出某个最优的策略,比如计算机会选择:吃饭-》外卖-》肯德基-》全家桶。
如何让计算机正确的穷举并比较所有的决策,就是回溯算法的设计技巧了。回溯算法的核心就是在于如何设计choose和unchoose部分的逻辑。
全排列问题就是典型的回溯算法问题。我们的思路是,先把第一个数固定为 1,然后全排列 2,3;再把第一个数固定为 2,全排列 1,3;再把第一个数固定为 3,全排列 1,2 。这就是个决策的过程。转化成决策树表示一下:
可以发现每向下走一层就是在「选择列表」中挑一个「选择」加入「决策路径」,然后把这个选择从「选择列表」中删除(以免之后重复选择);当一个决策分支探索完成后,我们就要向上回溯,要把该分支的「选择」从「决策列表」中取出,然后把这个「选择」重新加入「选择列表」(供其他的决策分支使用)。
上面的,就是模版中choose和unchoose的过程,choose过程是向下探索,进行一个选择;unchoose过程是向上回溯,撤销刚刚的选择。
现在我们可以针对全排列问题来具体化回溯算法的模版:
/**
* choiceList:选择列表,当前可以进行的选择列表
* track : 决策路径,已经作出一系列的选择
* answer : 用来存储完成的全排列
*/
func backtrack(choiceList, track, answer) {
if track is OK {
answer.add(track)
} else {
for choice as range choiceList {
// choose过程
// 把choice加入track
// 把choice移出choiceList
backtrack(choices, track, answer)
// unchoose过程
// 把choice移出track
// 把choice重新加入choiceList
}
}
}
下面是具体代码(golang):
func permute(nums []int) [][]int {
track := []int{}
ans := [][]int{}
backTrack(nums, &track, &ans)
return ans
}
func backTrack(nums []int, track *[]int, ans *[][]int) {
if len(*track) == len(nums) {
tmp := make([]int, len(*track))
copy(tmp, *track)
*ans = append(*ans, tmp)
} else {
for _, v := range nums {
if isIn(v, *track) {
continue
}
*track = append(*track, v)
backTrack(nums, track, ans)
*track = (*track)[:len(*track)-1]
}
}
}
func isIn(i int, arr []int) bool {
for _, v := range arr {
if v == i {
return true
}
}
return false
}
func solveNQueens(n int) [][]string {
board := []string{}
ans := [][]string{}
initBoard(&board, n)
backTrack(0, n, &board, &ans)
return ans
}
//初始化棋盘
func initBoard(board *[]string, n int) {
var s string
for i := 0; i < n; i++ {
s += "."
}
for i := 0; i < n; i++ {
*board = append(*board, s)
}
}
func backTrack(row int, n int, board *[]string, ans *[][]string) {
if row == len(*board) {
tmp := make([]string, len(*board))
copy(tmp, *board)
*ans = append(*ans, tmp)
} else {
for col := 0; col < n; col++ {
//如果选择这个位置会被攻击,跳过这个位置
if !isPlace(board, row, col, n) {
continue
}
//choose过程
(*board)[row] = strRex((*board)[row], col, 'Q')
//进行下一行的选择
backTrack(row + 1, n, board, ans)
//unchoose过程
(*board)[row] = strRex((*board)[row], col, '.')
}
}
}
func strRex(s string, i int, b byte) string {
bytes := []byte(s)
bytes[i] = b
s = string(bytes)
return s
}
//判断board[row][col]是否可以放置Q
func isPlace(board *[]string, row, col, n int) bool {
//检查正上方
for i := 0; i < row; i++ {
if (*board)[i][col] == byte('Q') {
return false
}
}
//检查右斜上方
for i, j := row - 1, col + 1; i >= 0 && j < n; i, j = i-1, j+1 {
if (*board)[i][j] == byte('Q') {
return false
}
}
//检查左斜上方
for i, j := row - 1, col - 1; i >= 0 && j >= 0; i, j = i-1, j-1 {
if (*board)[i][j] == byte('Q') {
return false
}
}
//不用检查下方,因为下方还没有放置皇后
return true
}
「决策路径」:就是棋盘board,每一步决策即是board的每一行,row变量记录着当前决策路径走到了哪一步。
「选择列表」:对于给定的一行(即决策中的每一步),每一列都可能放置一个‘Q’,这就是所有的选择,代码中的for循环就是在穷举「选择列表」判断是否可以放置皇后。
重新复习一下回溯的算法框架吧
func backtrack(choiceList, track, answer) {
if track is OK {
answer.add(track)
} else {
for choice as range choiceList {
// choose: 选择一个choice加入track
backtrack(choices, track, answer)
// unchoose: 从track中撤销上面的选择
}
}
}
参考自:https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484523&idx=1&sn=8c403eeb9bcc01db1b1207fa74dadbd1&chksm=9bd7fa63aca07375b75e20404fde7f65146286ef5d5dea79f284b8514fa1adb294e389518717&scene=21#wechat_redirect
图片来源自:https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484523&idx=1&sn=8c403eeb9bcc01db1b1207fa74dadbd1&chksm=9bd7fa63aca07375b75e20404fde7f65146286ef5d5dea79f284b8514fa1adb294e389518717&scene=21#wechat_redirect