数据结构与算法系列
数据结构与算法之哈希表
数据结构与算法之跳跃表
数据结构与算法之字典树
数据结构与算法之2-3树
数据结构与算法之平衡二叉树
数据结构与算法之十大经典排序
数据结构与算法之二分查找三模板
数据结构与算法之动态规划
数据结构与算法之回溯算法
目录
数据结构与算法之回溯算法
前言
好久不见,十分想念,因为一些事情,九月份耽搁了博客的更新,后续小泉的算法之路和安卓之路即将继续启程。今天小泉想跟大家介绍的是回溯算法。
定义
以下是回溯法在维基百科上的定义。
回溯法(英语:backtracking)是暴力搜索法中的一种。
对于某些计算问题而言,回溯法是一种可以找出所有(或一部分)解的一般性算法,尤其适用于约束补偿问题(在解决约束满足问题时,我们逐步构造更多的候选解,并且在确定某一部分候选解不可能补全成正确解之后放弃继续搜索这个部分候选解本身及其可以拓展出的子候选解,转而测试其他的部分候选解)。
从上面的定义可以知晓,回溯法其实遍历了所有解决问题的可能性。
并且根据回溯法的定义,回溯算法,其解决问题是按步进行“试错”,朝着一个方向层层深入,如若某一步发现出错(即已经无法解决问题或者不满足解决问题的条件)之后,就会返回到上一步,重新决定下一步该如何走,反之则继续向下一步进行。最终会得到两种结果:
- 搜索到所有满足问题条件的解决方案
- 不存在满足条件的问题解决方案
那么根据回溯算法的定义以及以前学习DFS的经验,我们可以提出回溯算法的三大要素:
- 路径(Paths)
- 选择(Choose)
- 条件(Condition)
1. 路径(Paths)
在解决问题之前,需要知晓在当前阶段“下一步”的可选路径有哪些?列出可走路径。
2. 选择(Choose)
在知道可选路径之后,就需要进行路径选择。再回退到当前步之前,仅只能选择一条路径前行。
3. 条件(Condition)
在选择“下一步”时,需要观察走到现在这一步,目前所在路径是否还满足解决问题的条件,或者是否到达了最后一步,没有“下一步”可走。
4. 算法模板
backtracking(step, nowPath, paths) {//step:当前步 nowPath:当前路径,path:当前步的后续可走的步
nowPath.add(step);
if (满足解决条件) {
result.add(nowPath);//加入到解决办法列表里
} else {
for nextStep in path {//“选择下一步”
nextPath = nextStep.path;
result = backtracking(nextStep, nextPath);//进入到下一步
}
}
nowPath.remove(step);//到此说明没有下一步可走,要返回到上一步的状态,那么此步不再保留
return result;
}
根据上面的算法模板,以及处理问题的流程可以看出,回溯算法的实质还是深度优先搜索算法(DFS),其核心思想是递归以及止归条件和止归时路径的回退操作。
下面就举两个例子来进行示范。

回溯算法例题
二叉树中和为某一值的路径
二叉树中和为某一值的路径是剑指offer中选出的比较有代表性的回溯算法的题目。
问题描述
剑指 Offer 34. 二叉树中和为某一值的路径
输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
问题分析
看到输出路径,搜索类的问题,第一想法就是回溯算法。
那么回溯算法确定三要素:
- 路径
由于本题为二叉树搜索问题,因此路径则是二叉树的左右节点。 - 选择
先选择左节点,然后再选择右节点。 - 条件
条件:没有左右节点即路径到达叶子节点
解决问题:路径之和为sum值。 - 过程
每次先将当前所在节点的值保存下来,然后先选择左节点进行递归,同时每次sum值要减去当前所在节点的值作为子节点往后的剩余sum值,当到达叶子节点,判断剩余sum值是否为0)
代码(Java+Go)
流程注释于java版本。
Java版本
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
List<List<Integer>> target = new LinkedList();//问题解决方法的集合
List<Integer> sumPath = new LinkedList();//记录路径
public List<List<Integer>> pathSum(TreeNode root, int sum) {
if (root == null) {//根节点为空,直接返回空
return target;
}
sumPath.add(root.val);//将当前步记录到路径中
if (root.left != null) {
target = pathSum(root.left, sum - root.val);//先左节点递归
}
if (root.right != null) {
target = pathSum(root.right, sum - root.val);//再右节点递归
}
if (root.left == null && root.right == null && sum - root.val == 0) {//到达叶子节点,是否路径总和为sum
List<Integer> tmp = new LinkedList(sumPath);
target.add(tmp);
}
sumPath.remove(sumPath.size() - 1);//返回到上一步,当前路径去掉该节点
return target;
}
}
Go版本
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func pathSum(root *TreeNode, sum int) [][]int {
var target [][] int
var sumPath []int
if root == nil {
return target
}
target = backtracking(root, sum, sumPath, target)
return target
}
func backtracking(root *TreeNode, sum int, sumPath []int, target [][]int) [][]int {
if root.Left != nil {
sumPath = append(sumPath,root.Val)
length := len(sumPath)
target = backtracking(root.Left, sum - root.Val, sumPath, target)
sumPath = sumPath[0:length-1]
}
if root.Right != nil {
sumPath = append(sumPath,root.Val)
length := len(sumPath)
target = backtracking(root.Right, sum - root.Val, sumPath, target)
sumPath = sumPath[0:length-1]
}
if root.Left == nil && root.Right == nil && sum - root.Val == 0 {
sumPath = append(sumPath,root.Val)
tmp := make([]int , len(sumPath))
copy(tmp, sumPath)
target = append(target,tmp)
}
return target
}
N皇后
N皇后问题是最经典的一道回溯算法题目了。
问题描述
力扣
设计一种算法,打印 N 皇后在 N × N 棋盘上的各种摆法,其中每个皇后都不同行、不同列,也不在对角线上。这里的“对角线”指的是所有的对角线,不只是平分整个棋盘的那两条对角线。
问题分析
N皇后是八皇后的基础上升级版。
抽象出来之后,N皇后其实就是一个搜索问题。
先确定一个标准,按照行去放置皇后,即考虑每一行皇后的位置。
那么回溯算法确定三要素:
- 路径
行的每一列都是路径 - 选择
选取该行的某一列作为当前步 - 条件
条件:皇后所在行、列完全不同,且任意一个皇后与其他皇后均不在对角线上
解决问题:当前路径下,第n行皇后也安排好了位置。 - 过程
本文只介绍简单的一种回溯方法,基于集合的回溯算法。
对于条件的限定,行列不同十分容易解决:按照行进行放置已经避免了行不同,列不同通过记录已放置列即可规避。
对于对角线,相对简单的思路就是,利用一小点数学的思路,对于行列可以当做是x,y
斜向上对角线,其斜率为1 : y = x + const ------> y - x = const对角线上元素行列差值相同
斜向下对角线,其斜率为-1 : y = - x + const ------> y + x = const对角线上元素行列求和相同
根据以上条件来选择“路径”,最后选择完最后一行的皇后的位置之后形成“路径”。
代码(Java+Go)
代码注解在java版本中。
Java版本
class Solution {
public List<List<String>> solveNQueens(int n)
{
List<List<String>> solutions = new LinkedList();//解决办法集合
int[] queens = new int[n];//皇后位置
Arrays.fill(queens, -1);
Set<Integer> columns = new HashSet();//记录放置皇后时的每一列 ---->下一步的路径
Set<Integer> diagonalsUp = new HashSet();//斜向上对角线:y - x = const
Set<Integer> diagonalsDown = new HashSet();//斜向下对角线 x + y = const
backtracking(solutions, queens, n, 0, columns, diagonalsUp, diagonalsDown);
return solutions;
}
public void backtracking(List solutions, int[] queens, int n, int row , Set columns, Set diagonalsUp, Set dialogDown)
{
if (row == n)
{
solutions.add(getSolution(queens, n));
} else
{
for (int i =0; i < n; i++) //放置第row行的queen位置(列) 选择
{
if (columns.contains(i))
{//不同列
continue;
}
int tmpDiagonalsUp = i - row;
if(diagonalsUp.contains(tmpDiagonalsUp))
{//不在斜向上对角线
continue;
}
int tmpDiagonalsDown = i + row;
if(diagonalsDown.contains(tmpDiagonalsDown))
{//不在斜向下对角线
continue;
}
queens[row] = i;
columns.add(i);
diagonalsDown.add(tmpDiagonalsDown);
diagonalsUp.add(tmpdiagonalsUp);
backtracking(solutions, queens, n, row + 1, columns, diagonalsUp, diagonalsDown);
columns.remove(i);
diagonalsDown.remove(tmpDiagonalsDown);
diagonalsUp.remove(tmpdiagonalsUp);
}
}
}
public List<String> getSolution(int[] queens, int n)
{
List<String> solution = new LinkedList();
for (int i = 0; i < n; i++)
{
int index = queens[i];
String strs = "";
for(int j =0; j < n; j++)
{
if (j == index)
{
strs += "Q";
} else
{
strs += ".";
}
}
solution.add(strs);
}
return solution;
}
}
Go版本
func solveNQueens(n int) [][]string {
solutions := [][]string{}
queens := make([]int, n)
for i := 0; i < n; i++ {
queens[i] = -1
}
columns, diagonalsUp, diagonalsDown := map[int] bool{}, map[int] bool{}, map[int] bool{}
solutions = backtracking(solutions, queens, n, 0, columns, diagonalsDown, diagonalsUp)
return solutions
}
func backtracking(solutions [][] string, queens []int, n int, row int, columns map[int]bool, diagonalsDown map[int]bool, diagonalsUp map[int]bool)[][]string {
if row == n {
solution := getSolution(queens, n)
solutions = append(solutions, solution)
} else {
for i := 0; i < n; i++ {
if columns[i] {
continue
}
tmpDiagnalsUp := i + row;
if diagonalsUp[tmpDiagnalsUp] {
continue
}
tmpDiagnalsDown := i - row;
if diagonalsDown[tmpDiagnalsDown] {
continue
}
queens[row] = i
columns[i] = true
diagonalsUp[tmpDiagnalsUp] = true
diagonalsDown[tmpDiagnalsDown] = true
solutions = backtracking(solutions, queens, n, row + 1, columns, diagonalsDown, diagonalsUp)
queens[row] = -1
delete(columns, i)
delete(diagonalsUp, tmpDiagnalsUp)
delete(diagonalsDown, tmpDiagnalsDown)
}
}
return solutions
}
func getSolution(queens []int, n int) []string {
solution :=[]string{}
for i := 0; i < n; i++ {
strs := make([]byte, n)
for j := 0; j < n; j++ {
strs[j] = '.'
}
strs[queens[i]]= 'Q'
solution = append(solution, string(strs))
}
return solution
}
总结
回溯算法初步讲解就到这里结束了,搞清楚回溯算法的内核以及三个要素,然后想方设法去满足回溯算法进行的条件即可。
回溯算法的内核就是DFS,当遇到一些可递归、搜索、路径、记录过程等等的问题时,可以考虑是否是回溯算法。在运用过程中可以询问自己如下问题:
- 是否使用了递归?
- 递归是否需要满足一定条件?
- 递归过程是否需要记录一开始到当前状态的路径?
- 是否是搜索性质的问题?
- 如何能够满足问题条件?(关键)
回溯算法很大程度是使用了dfs的思想,并且与动态规划的备忘录优化颇有些相似之处,在学习过程可以将DFS、回溯算法以及动态规划放在一起进行学习与理解。
如有兴趣,可以关注我的公众号,每周和你一起修炼数据结构与算法。



429

被折叠的 条评论
为什么被折叠?



