第一章--核心套路篇 之 回溯算法解题套路框架

解决一个回溯问题,实际上就是一个决策树的遍历过程,需要考虑下面的三个要素:

  1. 路径:已经做出的选择
  2. 选择列表:当前可以做出的选择
  3. 结束条件:达到决策树底层,无法做出选择的条件

代码框架如下:

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 51题

套用框架:

// 由于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]
  }
}

写回溯函数的时候,需要维护走过的路径和当前可以做的选择列表,当触发结束条件时,将路径写入结果集中即可

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值