回溯算法初探

回溯算法初探

思路

  • 回溯问题,实际上就是一个决策树的遍历过程,需要思考3个问题

    • 1、路径:也就是已经做出的选择。

    • 2、选择列表:也就是你当前可以做的选择。

    • 3、结束条件:也就是到达决策树底层,无法再做选择的条件。

代码方面,回溯算法的框架

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」

46. 全排列

class Solution {
List<List<Integer>> res = new LinkedList<>();

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 记录「路径」
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}

// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 触发结束条件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (track.contains(nums[i]))
            continue;
        // 做选择
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        // 取消选择
        track.removeLast();
    }
}
}

51. N 皇后

  • 这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。
    public List<List<String>> solveNQueens(int n) {
        char[][] chess = new char[n][n];
        //初始化数组
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                chess[i][j] = '.';
        List<List<String>> res = new ArrayList<>();
        solve(res, chess, 0);
        return res;
    }

    private void solve(List<List<String>> res, char[][] chess, int row) {
        //终止条件,最后一行都走完了,说明找到了一组,把它加入到集合res中
        if (row == chess.length) {
            res.add(construct(chess));
            return;
        }
        //遍历每一行
        for (int col = 0; col < chess.length; col++) {
            //判断这个位置是否可以放皇后
            if (valid(chess, row, col)) {
                //数组复制一份
                char[][] temp = copy(chess);
                //在当前位置放个皇后
                temp[row][col] = 'Q';
                //递归到下一行继续
                solve(res, temp, row + 1);
            }
        }
    }

    //把二维数组chess中的数据测下copy一份
    private char[][] copy(char[][] chess) {
        char[][] temp = new char[chess.length][chess[0].length];
        for (int i = 0; i < chess.length; i++) {
            for (int j = 0; j < chess[0].length; j++) {
                temp[i][j] = chess[i][j];
            }
        }
        return temp;
    }

    //row表示第几行,col表示第几列
    private boolean valid(char[][] chess, int row, int col) {
        //判断当前列有没有皇后,因为他是一行一行往下走的,
        //我们只需要检查走过的行数即可,通俗一点就是判断当前
        //坐标位置的上面有没有皇后
        for (int i = 0; i < row; i++) {
            if (chess[i][col] == 'Q') {
                return false;
            }
        }
        //判断当前坐标的右上角有没有皇后
        for (int i = row - 1, j = col + 1; i >= 0 && j < chess.length; i--, j++) {
            if (chess[i][j] == 'Q') {
                return false;
            }
        }
        //判断当前坐标的左上角有没有皇后
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (chess[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }

    //把数组转为list
    private List<String> construct(char[][] chess) {
        List<String> path = new ArrayList<>();
        for (int i = 0; i < chess.length; i++) {
            path.add(new String(chess[i]));
        }
        return path;
    }
  • 因为我们不停的复制数组,代码的效率很低,可以用回溯算法求解
    public List<List<String>> solveNQueens(int n) {
        char[][] chess = new char[n][n];
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                chess[i][j] = '.';
        List<List<String>> res = new ArrayList<>();
        solve(res, chess, 0);
        return res;
    }

    private void solve(List<List<String>> res, char[][] chess, int row) {
        if (row == chess.length) {
            res.add(construct(chess));
            return;
        }
        for (int col = 0; col < chess.length; col++) {
            if (valid(chess, row, col)) {
                chess[row][col] = 'Q';
                solve(res, chess, row + 1);
                chess[row][col] = '.';
            }
        }
    }

    //row表示第几行,col表示第几列
    private boolean valid(char[][] chess, int row, int col) {
        //判断当前列有没有皇后,因为他是一行一行往下走的,
        //我们只需要检查走过的行数即可,通俗一点就是判断当前
        //坐标位置的上面有没有皇后
        for (int i = 0; i < row; i++) {
            if (chess[i][col] == 'Q') {
                return false;
            }
        }
        //判断当前坐标的右上角有没有皇后
        for (int i = row - 1, j = col + 1; i >= 0 && j < chess.length; i--, j++) {
            if (chess[i][j] == 'Q') {
                return false;
            }
        }
        //判断当前坐标的左上角有没有皇后
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (chess[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }

    //把数组转为list
    private List<String> construct(char[][] chess) {
        List<String> path = new ArrayList<>();
        for (int i = 0; i < chess.length; i++) {
            path.add(new String(chess[i]));
        }
        return path;
    }

总结

回溯算法就是个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作,算法框架如下:

def backtrack(...):
    for 选择 in 选择列表:
        做选择
        backtrack(...)
        撤销选择

backtrack 函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集

其实想想看,回溯算法和动态规划是不是有点像呢?我们在动态规划系列文章中多次强调,动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?

某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
•Alpha-Beta剪枝(Alpha-Beta pruning) 对于一般的最大最小搜索,即使每一步只有很少的下法,搜索的位置也会增长非常快;在大多数的中局棋形中,每步平均有十个位置可以下棋,于是假设搜索九步(程序术语称为搜索深度为九),就要搜索十亿个位置(十的九次方),极大地限制了电脑的棋力。于是采用了一个方法,叫“alpha-beta剪枝”,它大为减少了检测的数目,提高电脑搜索的速度。各种各样的这种算法用于所有的强力Othello程序。(同样用于其他棋类游戏,如国际象棋和跳棋)。为了搜索九步,一个好的程序只用搜索十万到一百万个位置,而不是没用前的十亿次。 •估值 这是一个程序中最重要的部分,如果这个模块太弱,则就算算法再好也没有用。我将要叙述三种不同的估值函数范例。我相信,大多数的Othello程序都可以归结于此。 棋格表:这种算法的意思是,不同的棋格有不同的值,角的值大而角旁边的格子值要小。忽视对称的话,棋盘上有10个不同的位置,每个格子根据三种可能性赋值:黑棋、白棋和空。更有经验的逼近是在游戏的不同阶段对格子赋予不同的值。例如,角在开局阶段和中局开始阶段比终局阶段更重要。采用这种算法的程序总是很弱(我这样认为),但另一方面,它很容易实现,于是许多程序开始采用这种逼近。 基于行动力的估值:这种更久远的接近有很强的全局观,而不像棋格表那样局部化。观察表明,许多人类玩者努力获得最大的行动力(可下棋的数目)和潜在行动力(临近对手棋子的空格,见技巧篇)。如果代码有效率的话,可以很快发现,它们提高棋力很多。 基于模版的估值 :正如上面提及的,许多中等力量的程序经常合并一些边角判断的知识,最大行动力和潜在行动力是全局特性,但是他们可以被切割成局部配置,再加在一起。棋子最少化也是如此。这导致了以下的概括:在估值函数中仅用局部配置(模版),这通常用单独计算每一行、一列、斜边和角落判断,再加在一起来实现。 估值合并:一般程序的估值基于许多的参数,如行动力、潜在行动力、余裕手、边角判断、稳定子。但是怎么样将他们合并起来得到一个估值呢?一般采用线性合并。设a1,a2,a3,a4为参数,则估值s:=n1*a1+n2*a2+n3*a3+n4*a4。其中n1,n2,n3,n4为常数,术语叫“权重”(weight),它决定了参数的重要性,它们取决于统计值。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值