解决一个回溯问题,实际上就是一个决策树的遍历过程,需要考虑下面的三个要素:
- 路径:已经做出的选择
- 选择列表:当前可以做出的选择
- 结束条件:达到决策树底层,无法做出选择的条件
代码框架如下:
var result []路径 // 保存结果的数组
func backtrack(路径,选择列表){
if 满足结束条件{
加入到result中
return
}
for _, 选择 := range 选择列表{
// 做出选择
路径 = append(路径, 选择)
backtrack(路径,选择列表)
// 撤销选择
路径 = 路径[:len(路径)-1]
}
}
其核心就是for循环里面的递归,在递归调用之前做出选择,在递归调用之后撤销选择
下面使用全排列的例子来解释
全排列问题
给出数组[1,2,3],求出其全排列所有结果,这里为了简化问题,数组中不包含重复元素
有如下的回溯树:
只要从根节点出发开始遍历这棵树,走到叶子结点就得到了一个排列,遍历整棵树之后就得到了所有的全排列,这棵树我们可以称之为决策树,因为在这棵树上的每一结点我们都在进行选择,在根节点的时候,我们需要可以选择1,也可以选择2,还可以选择3
我们定义的backtrack函数就相当于一个指针,在这棵树上进行遍历,同时也需要不断维护每个结点的属性,每当走到树的底层,其路径就是一个全排列
类比多叉树的遍历
func traverse(root *Node){
for _, child := range root.Children{
// 前序遍历
traverse(child)
// 后序遍历
}
}
前序遍历的代码在进入某个结点之前的那个时间点执行,后序遍历的代码在离开某个结点之后的那个时间点执行,backtrack函数其实就相当于树的遍历问题,我们在前序遍历的时候做出选择,到达另一个结点,在后序遍历的时候撤销这个选择,然后回到原来的结点,这样的话,可以正确维护每个结点的路径和选择关系
全排列代码如下:
var result [][]int
func permute(nums []int) {
backtrack(make([]int, 0), nums)
fmt.Println(result)
}
func backtrack(path []int, choices []int) {
// 结束条件
if len(path) == len(choices) {
result = append(result, append([]int{}, path...))
return
}
loop:
// 做出选择
for _, choice := range choices {
// 已经做出选择了的,不需要再次进行选择
// 这里使用一个循坏进行判断
// 当然也可以使用其他的方式进行判断
for _, v := range path {
if v == choice {
continue loop
}
}
// 做选择
path = append(path, choice)
backtrack(path, choices)
// 撤销选择
path = path[:len(path)-1]
}
}
回溯算法不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高
有的时候对于回溯算法,我们不需要所有合法的答案,只需要一个答案即可,那么我们在获取到一个可行解之后退出求解即可
N皇后问题
套用框架:
// 由于leetcode限制,使用全局变量会出现问题
// 本地进行调试无问题
var result [][]string
func solveNQueens(n int) {
backtrack(make([]string, 0), n)
// 输出所有的结果
for index, i := range result {
fmt.Printf("number: %d\n", index)
for _, j := range i {
fmt.Println(j)
}
fmt.Println()
}
}
func backtrack(path []string, row int) {
if len(path) == row {
result = append(result, append([]string{}, path...))
return
}
// 做出选择
for col := 0; col < row; col++ {
// 如果要放在第col列是否可行
if !isValid(col, path) {
continue
}
// 做选择
path = append(path, genDot(row, col))
backtrack(path, row)
// 撤销选择
path = path[:len(path)-1]
}
}
// 生成row个 点(.),其中第col个点为Q
func genDot(row, col int) string {
b := make([]byte, row)
for i := 0; i < row; i++ {
b[i] = '.'
}
b[col] = 'Q'
return string(b)
}
// 判断当前点是否有效
func isValid(col int, path []string) bool {
if len(path) == 0 {
return true
}
// 检查同一列有没有冲突的
for i := 0; i < len(path); i++ {
if path[i][col] == 'Q' {
return false
}
}
// 左上方是否冲突
for i := 1; len(path)-i >= 0 && col-i >= 0; i++ {
if path[len(path)-i][col-i] == 'Q' {
return false
}
}
// 右上方是否冲突
for i := 1; len(path)-i >= 0 && col+i < len(path[0]); i++ {
if path[len(path)-i][col+i] == 'Q' {
return false
}
}
return true
}
最后总结
回溯算法就是一个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置上做一些操作,算法框架如下:
var result []Type // 保存结果的数组
func backtrack(路径,选择列表){
if 满足结束条件{
加入到result中
return
}
for _, 选择 := range 选择列表{
// 做出选择
路径 = append(路径, 选择)
backtrack(路径,选择列表)
// 撤销选择
路径 = 路径[:len(路径)-1]
}
}
写回溯函数的时候,需要维护走过的路径和当前可以做的选择列表,当触发结束条件时,将路径写入结果集中即可