zoukankan      html  css  js  c++  java
  • 回溯算法

    回溯算法:

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

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

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

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

    回溯算法的框架:

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

    result用于接收答案,当路径满足了结束条件时就把路径收入其中。路径就是之前做选择的结果,选择列表就是当前可选的“剩余的”选择,结束条件是到达决策树的底层,或者说满足了题目条件。
    其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。

    实例1:全排列(leetcode46)
    怎么穷举全排列的呢?比方说给三个数[1,2,3],你肯定不会无规律地乱穷举,一般是这样:先固定第一位为 1,然后第二位可以是 2,那么第三位只能是 3;然后可以把第二位变成 3,第三位就只能是 2 了;然后就只能变化第一位,变成 2,然后再穷举后两位……

    或者可以用图来说明:

    只要从根遍历这棵树,记录路径上的数字,其实就是所有的全排列。我们不妨把这棵树称为回溯算法的「决策树」

    为啥说这是决策树呢,因为你在每个节点上其实都在做决策。比如说你站在下图的红色节点上:

    你现在就在做决策,可以选择 1 那条树枝,也可以选择 3 那条树枝。为啥只能在 1 和 3 之中选择呢?因为 2 这个树枝在你身后,这个选择你之前做过了,而全排列是不允许重复使用数字的。

    现在可以解答开头的几个名词:[2]就是「路径」,记录你已经做过的选择;[1,3]就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层,在这里就是选择列表为空的时候

    如果明白了这几个名词,可以把「路径」和「选择列表」作为决策树上每个节点的属性,比如下图列出了几个节点的属性:

    我们定义的backtrack函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其「路径」就是一个全排列。所以呢,我们要做的就是从根节点往下游走,不停的更新选择列表和路径,当方向是往下的时候,选择列表变短,路径变长,往上的时候,要把一些选择重新加入到选择列表之中。
    再进一步,如何遍历一棵树?这个应该不难吧。各种搜索问题其实都是树的遍历问题,而多叉树的遍历框架就是这样:

    void traverse(TreeNode root) {
        for (TreeNode child : root.childern)
            // 前序遍历需要的操作
            traverse(child);
            // 后序遍历需要的操作
    }
    

    前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行。!!!!!!!!!!!!!!
    回想我们刚才说的,「路径」和「选择」是每个节点的属性,函数在树上游走要正确维护节点的属性,那么就要在这两个特殊时间点搞点动作:

    for 选择 in 选择列表:
        # 做选择
        将该选择从选择列表移除
        路径.add(选择)
        backtrack(路径, 选择列表)
        # 撤销选择
        路径.remove(选择)
        将该选择再加入选择列表
    

    代码全部:

    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)); //这里是把track的值赋给一个新的linkedlist并返回这个新的
            //每次这样返回完,track本身会逐次removelast
            return;
        }
    
        for (int i = 0; i < nums.length; i++) {
            // 排除不合法的选择
            if (track.contains(nums[i]))
                continue;
            // 做选择
            track.add(nums[i]);
            // 进入下一层决策树
            backtrack(nums, track);
            // 取消选择
            track.removeLast();
        }
    }
    }
    

    但是必须说明的是,不管怎么优化,都符合回溯框架,而且时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高

    n皇后问题:
    给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
    PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。

    这是 N = 8 的一种放置方法:

    这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。

    函数backtrack依然像个在决策树上游走的指针,每个节点就表示在棋盘上放置皇后,而isvalid可以将不符合条件的情况剪枝。

    class Solution {
        List<List<String>> res = new ArrayList<List<String>>();//用于最终接收答案
    
        public List<List<String>> solveNQueens(int n) {
            char[][] arr = new char[n][n];
            //填充棋盘,每个格子默认是“.”表示没有放置皇后
            for(int i=0;i<n;++i) {
                Arrays.fill(arr[i],'.');
            }
    
            backtrack(arr,0,n);
            return res;
        }
    
        public void backtrack(char[][] arr,int row,int n){
            //这里是出错点,本来写的是row==n-1,但事实上当递归到最后一行的时候,仍然会先递归再碰到这个
            //判断,所以那个时候的row==n
            if(row==n){
                res.add(char2list(arr,n));
                return;
            }
            //还是回溯法的框架
            for(int col=0;col<n;col++){
                if(!isvalid(arr,row,col,n)){
                    continue;
                }
                arr[row][col]='Q';
    
                backtrack(arr,row+1,n);
    
                arr[row][col]='.';
            }
        }
    	//用于判断是否能在那一列加上皇后,要注意的是,我们的选择是一行一行进行的(递归之前做选择)
        //而那个时候下面是没有值的,所以只需要判断左上,右上,和上方
        public boolean isvalid(char[][] arr,int row,int col,int n){
            //上方
            for(int i=0;i<n;i++){
                if(arr[i][col]=='Q'){
                    return false;
                }
            }
    		//右上
            for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
                if(arr[i][j]=='Q'){
                    return false;
                }
            }
    		//左上
            for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){
                if(arr[i][j]=='Q'){
                    return false;
                }
            }
    
            return true;
        }
    
        public List<String> char2list(char[][] arr,int n){
            List<String> ans = new ArrayList<String>();
            for(int i=0;i<n;i++){
                StringBuilder tmp = new StringBuilder();
                for(int j=0;j<n;j++){
                    if(arr[i][j]=='Q'){
                        tmp.append('Q');
                    }else{
                        tmp.append('.');
                    }
                }
                ans.add(tmp.toString());
            }
    
            return ans;
        }
    
    }
    

    在自己写回溯法的时候,可以在脑海中想象出一颗树,搞清楚每一个层级和不同树列在干什么,比如这里的全排列问题,每个层级其实代表着:排列中的第一位数字,第二位数字,第三位数字.......而不同列则表示对该位数字的不同选择;而对于n皇后问题来说,每一个层级表示不同的行row,而树列则表示在不同的行上,放置皇后的选择(也就是放在哪一个列上col)。而我们可以看到,对于树列上的遍历由for完成,每次要保证排除不合法选择;对于行的遍历,由递归完成。(草稿上不妨写出每行的代表意义以帮助思考)。

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

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

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

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

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

  • 相关阅读:
    Vue+element tree使用,当弹窗关闭时,this.$refs.tree.setCheckedKeys([])不能清空选中
    npm ERR! A complete log of this run can be found in
    Vue面试题之vue实现MVVM数据绑定
    渐变色的ie兼容
    安徽地图
    数组去重的四种方法
    EF Code First创建数据库
    Code First Migrations更新数据库结构(数据迁移)
    Bootstrap 样式大全
    C#程序中:如何向记事本中写入内容和导出内容
  • 原文地址:https://www.cnblogs.com/shiji-note/p/14402535.html
Copyright © 2011-2022 走看看