回溯算法实际上是一个类似枚举的搜索尝试过程,在搜索过程中寻找问题的解,当发现当前问题状态无解时,就“回溯”返回,尝试别的路径。
判断回溯很简单,拿到一个问题,你感觉如果不穷举一下就没法知道答案,那就可以开始回溯了。
一般回溯的问题有三种:Find a path to success 有没有解
Find all paths to success 求所有解
求所有解的个数
求所有解的具体信息
Find the best path to success 求最优解
理解回溯:给一堆选择, 必须从里面选一个. 选完之后我又有了新的一组选择.
回溯可以抽象为一棵树,我们的目标可以是找这个树有没有good leaf,也可以是问有多少个good leaf,也可以是找这些good leaf都在哪,也可以问哪个good leaf最好,分别对应上面所说回溯的问题分类。
good leaf都在leaf上。good leaf是我们的goal state,leaf node是final state,是解空间的边界。
关于回溯的三种问题,模板略有不同,
第一种,返回值是true/false。
第二种,求个数,设全局counter,返回值是void;求所有解信息,设result,返回值void。
第三种,设个全局变量best,返回值是void。
第一种:
boolean solve(Node n) {
if n is a leaf node {
if the leaf is a goal node, return true
else return false
} else {
for each child c of n {
if solve(c) succeeds, return true
}
return false
}
}
第二种:
void solve(Node n) {
if n is a leaf node {
if the leaf is a goal node, count++, return;
else return
} else {
for each child c of n {
solve(c)
}
}
}
第三种:
void solve(Node n) {
if n is a leaf node {
if the leaf is a goal node, update best result, return;
else return
} else {
for each child c of n {
solve(c)
}
}
}
八皇后 N-Queens
问题
1.给个n,问有没有解;
2.给个n,有几种解;(Leetcode N-Queens II)
3.给个n,给出所有解;(Leetcode N-Queens I)
解答
1.有没有解
怎么做:一行一行的放queen,每行尝试n个可能,有一个可达,返回true;都不可达,返回false.
边界条件leaf:放完第n行 或者 该放第n+1行(出界,返回)
目标条件goal:n行放满且isValid,即目标一定在leaf上
helper函数:
boolean solve(int i, int[][] matrix)
在进来的一瞬间,满足property:第i行还没有被放置,前i-1行放置完毕且valid
solve要在给定的matrix上试图给第i行每个位置放queen。
public static boolean solve1(int i, List<Integer> matrix, int n) {
if (i == n) {
if (isValid(matrix))
return true;
return false;
} else {
for (int j = 0; j < n; j++) {
matrix.add(j);
if (isValid(matrix)) { //剪枝
if (solve1(i + 1, matrix, n))
return true;
}
matrix.remove(matrix.size() - 1);
}
return false;
}
}
2.求解的个数
怎么做:一行一行的放queen,每行尝试n个可能。这回因为要找所有,返回值就没有了意义,用void即可。在搜索时,如果有一个可达,仍要继续尝试;每个子选项都试完了,返回.
边界条件leaf:放完第n行 或者 该放第n+1行(出界,返回)
目标条件goal:n行放满且isValid,即目标一定在leaf上
helper函数:
void solve(int i, int[][] matrix)
在进来的一瞬间,满足property:第i行还没有被放置,前i-1行放置完毕且valid
solve要在给定的matrix上试图给第i行每个位置放queen。
这里为了记录解的个数,设置一个全局变量(static)int是比较efficient的做法。
public static void solve2(int i, List<Integer> matrix, int n) {
if (i == n) {
if (isValid(matrix))
count++;
return;
} else {
for (int j = 0; j < n; j++) {
matrix.add(j);
if (isValid(matrix)) { //剪枝
solve2(i + 1, matrix, n);
}
matrix.remove(matrix.size() - 1);
}
}
}
3.求所有解的具体信息
怎么做:一行一行的放queen,每行尝试n个可能。返回值同样用void即可。在搜索时,如果有一个可达,仍要继续尝试;每个子选项都试完了,返回.
边界条件leaf:放完第n行 或者 该放第n+1行(出界,返回)
目标条件goal:n行放满且isValid,即目标一定在leaf上
helper函数:
void solve(int i, int[][] matrix)
在进来的一瞬间,满足property:第i行还没有被放置,前i-1行放置完毕且valid
solve要在给定的matrix上试图给第i行每个位置放queen。
这里为了记录解的具体情况,设置一个全局变量(static)集合是比较efficient的做法。
当然也可以把结果集合作为参数传来传去。
public static void solve3(int i, List<Integer> matrix, int n) {
if (i == n) {
if (isValid(matrix))
result.add(new ArrayList<Integer>(matrix));
return;
} else {
for (int j = 0; j < n; j++) {
matrix.add(j);
if (isValid(matrix)) { //剪枝
solve3(i + 1, matrix, n);
}
matrix.remove(matrix.size() - 1);
}
}
}
优化
上面的例子用了省空间的方法。
由于每行只能放一个,一共n行的话,用一个大小为n的数组,数组的第i个元素表示第i行放在了第几列上。
Utility(给一个list判断他的最后一行是否和前面冲突):
public static boolean isValid(List<Integer> list){
int row = list.size() - 1;
int col = list.get(row);
for (int i = 0; i <= row - 1; i++) {
int row1 = i;
int col1 = list.get(i);
if (col == col1)
return false;
if (row1 - row == col1 - col)
return false;
if (row1 - row == col - col1)
return false;
}
return true;
}
另一版本
回溯法的算法本质是:n 叉树的遍历;类似对 n 叉树进行遍历,且遍历起始编号为 0. 在遍历中加入约束与限界,以此确定一些子树不需要遍历
大致代码如下,以及一些解释:
data
需要用到的数据data->depth
需要遍历的深度depth
遍历的当前深度solution(data)
处理得到的解path[depth]
记录了根到当前节点的路径,可替换为其它需要在各个遍历节点处理的代码constraint(data, depth)
约束函数,满足约束返回 TRUE, 否则返回 FALSEbound(data, depth)
限界函数,满足减枝条件返回 TRUE, 否则返回 FALSE
void backtracking_iter(struct data *data, int depth)
{
if (data->depth == depth) {
solution(data);
return;
}
for (int i = 0; i < data->depth; i++) {
data->path[depth] = i;
if (constraint(data, depth) == TRUE && bound(data, depth) == FALSE) {
backtracking(data, depth + 1);
}
}
}
void backtracking(struct data *data)
{
backtracking_iter(data, 0);
}
利用回溯法,将行作为深度,列作为 n 叉树的分叉
假设将棋盘分成 8 行,从上到下编号为 8-1
分成 8 列,从左到右编号为 A-H
求解过程如下:
一行一行放,首先保证了皇后放在不同的行
是否放满了 8 个皇后,否,继续
在 8-A 放上一个皇后
查看是否可放,可行,继续下一步(深度 + 1)是否放满了 8 个皇后,否,继续
在 7-A 放上一个皇后
查看是否可放,不可行(与 8-A 这个皇后在同一列上),放弃这个子树,继续搜索下一个(列 + 1)是否放满了 8 个皇后,否,继续
在 7-B 放上一个皇后
查看是否可放,不可行(与 8-A 这个皇后在同一列上),放弃这个子树,继续搜索下一个(列 + 1)是否放满了 8 个皇后,否,继续
在 7-C 放上一个皇后
查看是否可放,可行,继续下一步(深度 + 1)是否放满了 8 个皇后,否,继续
在 6-A 放上一个皇后
查看是否可放,不可行(与 8-A 这个皇后在同一列上),放弃这个子树,继续搜索下一个(列 + 1)...
是否放满了 8 个皇后,否,继续
在 1-D 放上一个皇后
查看是否可放,可行继续下一步(深度 + 1)是否放满了 8 个皇后,是,输出解,回到上一步(深度 - 1)
直到搜索完所有情况
代码
#include <stdlib.h>
#include <stdio.h>
#define TRUE 1
#define FALSE 0
#define NUM_QUEENS 8
void output(int *queens)
{
for (int i = 0; i < NUM_QUEENS; i++) {
printf("%d ", queens[i]);
}
printf("\n");
}
/*
* 查看是否和 row 以上几行的皇后在同一列或同一对角线
*/
int constraint(int *queens, int row)
{
for (int i = 0; i < row; i++) {
if (queens[row] == queens[i]
|| abs(i - row) == abs(queens[row] - queens[i])) {
return FALSE;
}
}
return TRUE;
}
void eight_queens_puzzle_iter(int *queens, int row)
{
if (row == NUM_QUEENS) {
output(queens);
return;
}
for (int col = 0; col < NUM_QUEENS; col++) {
queens[row] = col; //
保持深度即行数不变,依次判断从第0列开始,直到找到符合或达到边界条件
if (constraint(queens, row) == TRUE) {
eight_queens_puzzle_iter(queens, row + 1);
}
}
}
void eight_queens_puzzle(int *queens)
{
eight_queens_puzzle_iter(queens, 0);
}
int main(int argc, char *argv[])
{
int queens[NUM_QUEENS] = {0};
eight_queens_puzzle(queens);
return 0;
}
剑指offer中的 面试题25:二叉树中和为某一路径;面试题66:矩阵中的路径;面试题67:机器人的运动范围
参考网址:https://segmentfault.com/a/1190000006121957
https://www.dreamxu.com/books/dsa/backtracking/
解空间: https://www.jianshu.com/p/f6d3732e86fb
https://github.com/xuelangZF/LeetCode/tree/master/Backtracking
例子 : https://blog.csdn.net/EbowTang/article/details/51570317
https://blog.csdn.net/versencoder/article/details/52071930