回溯法

回溯算法实际上是一个类似枚举的搜索尝试过程,在搜索过程中寻找问题的解,当发现当前问题状态无解时,就“回溯”返回,尝试别的路径。

判断回溯很简单,拿到一个问题,你感觉如果不穷举一下就没法知道答案,那就可以开始回溯了。

一般回溯的问题有三种:
  1. Find a path to success 有没有解

  2. Find all paths to success 求所有解

    • 求所有解的个数

    • 求所有解的具体信息

  3. 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, 否则返回 FALSE
  • bound(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

求解过程如下:
一行一行放,首先保证了皇后放在不同的行

  1. 是否放满了 8 个皇后,否,继续
    在 8-A 放上一个皇后
    查看是否可放,可行,继续下一步(深度 + 1)

  2. 是否放满了 8 个皇后,否,继续
    在 7-A 放上一个皇后
    查看是否可放,不可行(与 8-A 这个皇后在同一列上),放弃这个子树,继续搜索下一个(列 + 1)

  3. 是否放满了 8 个皇后,否,继续
    在 7-B 放上一个皇后
    查看是否可放,不可行(与 8-A 这个皇后在同一列上),放弃这个子树,继续搜索下一个(列 + 1)

  4. 是否放满了 8 个皇后,否,继续
    在 7-C 放上一个皇后
    查看是否可放,可行,继续下一步(深度 + 1)

  5. 是否放满了 8 个皇后,否,继续
    在 6-A 放上一个皇后
    查看是否可放,不可行(与 8-A 这个皇后在同一列上),放弃这个子树,继续搜索下一个(列 + 1)

  6. ...

  7. 是否放满了 8 个皇后,否,继续
    在 1-D 放上一个皇后
    查看是否可放,可行继续下一步(深度 + 1)

  8. 是否放满了 8 个皇后,是,输出解,回到上一步(深度 - 1)

  9. 直到搜索完所有情况

代码

#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



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值