回溯算法学习心得

一、回溯算法

回溯算法的原理:回溯算法是一种选优搜索法,按照选优条件向前搜索,以达到目标。但当探索到某一步的时候,发现原先选择并不优活着达不到目标的时候,就退回一步重新选择,这种走不通就退回再走的技术为回溯法

废话不多说,直接上解决回溯算法的框架。解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考如下3个问题:

  • 路径:也就是已经做出的选择
  • 选择列表:也就是你当前可以做的选择
  • 结束条件:也就是到达决策树的底层,无法再做选择的条件

回溯法的算法思想和深度优先搜索(DFS)的思想较为类似,可以结合着看

代码方面,回溯算法的框架如下所示:

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

其核心就是for循环里面的递归,在递归调用之前“做选择”,在递归调用之后“撤销选择”,就是这样。

案例一:全排列问题

问题描述如下图所示:

![image-20220326170514266](/Users/weiyifeng/Library/Application Support/typora-user-images/image-20220326170514266.png)

解决问题的代码如下所示,可结合起来进行理解:

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

    public List<List<Integer>> permute(int[] nums) {
        LinkedList<Integer> track = new LinkedList<>();
        backtarck(nums,track);
        return result;
    }

    void backtarck(int[] nums , LinkedList<Integer> track){
        //当到达叶子结点的时候,就将当前的track装入结果列表
        if(track.size() == nums.length ) {
            result.add(new LinkedList(track));
            return;
        }

        for(int i = 0 ; i < nums.length ; i++) {
            //排除当前数组中不合法的选择
            if(track.contains(nums[i])) {
                continue;
            }

            //做选择
            track.add(nums[i]);

            //进入下一层决策树
            backtarck(nums,track);

            //取消选择
            track.removeLast();
        }
    }
}

二、回溯算法在其他场景下的应用

2.1 求子集

问题描述如下所示:

![image-20220326171721726](/Users/weiyifeng/Library/Application Support/typora-user-images/image-20220326171721726.png)

拓展内容:

计算递归算法时间复杂度的方法:找到递归深度,然后乘以每次递归中迭代的次数

解法思想:

首先,空集[]肯定是一个子集。

然后,以1开头的子集有哪些呢? 有[1] [1,2] [1,2,3]

同理,以2开头的子集有[2] [2,3]

以3开头的子集有[3]

最后,把上面这些子集加起来就是【1,2,3】的所有子集

代码解法如下所示:

class Solution {
    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        LinkedList<Integer> track = new LinkedList<>();
        backTrack(nums,0,track);
        return result;
    }

    void backTrack(int[] nums , int start , LinkedList<Integer> track) {
        //前序遍历的位置
        result.add(new LinkedList(track));

        for(int i = start ; i < nums.length ; i++) {
            //做选择
            track.add(nums[i]);

            //递归回溯
            backTrack(nums , i + 1 , track);

            //撤销选择
            track.removeLast();
        }
    }
}

可以看见,由start参数实现了刚才所说的“以某个数字开头的子集”;而对res的更新处在前序遍历的位置,所以说res记录了树上的所有结点,也就是所有的子集

2.2 回溯算法的最佳实践:括号生成

有关括号问题,你只需要记住以下性质,思路就很容易想出来:

  • 一个“合法”括号组合的左括号数量一定等于有括号数量
  • 对于一个“合法”的括号字符串组合p,必然对于任何0 <= i < len(p)都有:子串p[0...i]中左括号的数量都大于或者等于右括号的数量
posted @ 2022-03-27 14:12  yfwei  阅读(502)  评论(0编辑  收藏  举报