算法通关村第十八关——回溯青铜挑战笔记

该部分主要通过"N叉树遍历"这一递归中的经典问题,引出回溯并总结回溯模版,需要格外关注的是递归与回溯的关系。此外,回溯模版所能解决的问题也是十分明确的,例如:组合、分割、子集、排列、棋盘等,但是具体问题具体分析,回溯模版也会有灵活的调整。

1.N叉树的遍历

N叉树的遍历在递归模块已经深度剖析,不再过多赘述,直接上代码!

    public static void treeDFS(TreeNode node){
        //递归出口
        if(node == null) return;
        //局部枚举
        System.out.println(node.val);
        for (int i = 1; i <= node.length; i++) {
            treeDFS("第i个子节点");
        }
    }

那么,回溯的模版又是什么样子,它和N叉树的遍历有什么关系呢?相信在给出回溯模版的时候,你已经可以发现N叉树遍历模版和回溯模版的差异了!

    public static void backtracking(参数){
        //递归出口
        if(终止条件){
            存放结果;
            return;
        } 
        //局部枚举
        for (选择本层集合中元素(画成树,就是树节点孩子的大小)) {
            处理节点;
            backtracking();
            回溯,撤销处理结果;
        }
    }

观察回溯模版和递归模版,我们可以发现回溯模版总共做了三件事情:递归、局部枚举和"放下前任"。关键需要搞清楚,这三个分别指的是什么,递归:递归模版和递归出口;局部枚举:以LeetCode77为例,从集合1,2,3,4中找出所有两个数的组合,枚举就是指第二个数字可以枚举2、3、4;"放下前任":path数组维护了当前所走的路径,因此在枚举当前元素后,以递归的形式返回前,应先将当前枚举的元素移除path数组(就好比开始新化学实验之前,需要清理仪器设备,不能让前面的实验杂质干扰影响正在进行的实验)。

理解清楚回溯模版后,我们再以二叉树的路径问题来巩固回溯模版!

2.回溯热身——再论二叉树路径问题

2.1输出二叉树的所有路径

题目见LeetCode257。

题目分析:为了使用回溯模版,需要搞清楚三个问题:第一,递归出口:当root为空退出dfs;第二,局部枚举:维护一个path数组,先将当前枚举的节点加入path数组,然后再枚举左右孩子节点,若当钱枚举的节点如果是叶子结点,将path数组加入ans数组中;第三,"处理前任":枚举完当前节点和左右孩子节点后,需要将当前节点移除出path数组。

厘清思路,直接上代码!

需要注意的是,在LeetCode中不要使用定义在类属性中的静态变量,即去掉static,不然会有脏数据,其原理为:静态变量在LeetCode类加载时只初始化一次,后面再次输入实例的时候就会出现脏数据!

public class MyBinaryTreePaths {
    static List ans = new ArrayList<>();

    public static void main(String[] args) {
        BinaryTree bTree = new BinaryTree();
        bTree.root = bTree.buildBinaryTree();
        List result = binaryTreePaths(bTree.root);
        System.out.println(result);
    }
    public static List binaryTreePaths(TreeNode root) {
        dfs(root, new ArrayList<>());
        return ans;
    }

    public static void dfs(TreeNode p, ArrayList path){
        //递归出口
        if(p == null) return;
        //处理当前节点
        path.add(p.val);
        if(p.left == null && p.right == null) ans.add(getPathString(path));
        //局部枚举
        dfs(p.left, path);
        dfs(p.right, path);
        //放下前任
        path.remove(path.size() - 1);
    }

    public static String getPathString(ArrayList path){
        StringBuilder buf = new StringBuilder();
        buf.append(path.get(0));
        for (int i = 1; i < path.size(); i++) {
            buf.append("->");
            buf.append(path.get(i));
        }
        return buf.toString();
    }
}

2.2路径总和问题

题目见LeetCode113,描述为:输出树的路径和targetSum的路径。

题目分析:如果跟节点的值为val,那么只需要在左右子树中找路径和为targetSum-val的路径即可!因此,该问题就是一个递归问题,递归出口:root为空则结束递归。紧接着我们需要局部枚举:局部枚举思路与上一道题目大致相同,此外我们还需要维护一个不断更新的targetSum来记录当前需要在子树寻找路径和为多少的路径,当且仅当枚举到叶子结点且targetSum为0时,需要将path加入到ans中;"处理前任":策略与上一个题目相同,不再赘述,此时无需再次回溯targetSum,因为他是值传递且作为局部变量。

厘清思路,直接上代码!

public class HasPathSum {

    static List> ret = new LinkedList>();
    static Deque path = new LinkedList();

    public static List> pathSum(TreeNode root, int targetSum) {
        myDfs(root, targetSum);
        return ret;
    }

    public static void myDfs(TreeNode root, int targetSum) {
        //递归出口
        if(root == null) return;
        //处理当前节点
        targetSum -= root.val;
        path.offer(root.val);
        if(root.right == null && root.left == null && targetSum == 0){
            ret.add(new ArrayList(path));
        }
        //局部枚举
        myDfs(root.left, targetSum);
        myDfs(root.right, targetSum);
        //放弃前任
        path.pollLast();
    }
}

OK,《算法通关村第十八关——回溯青铜挑战笔记》结束,喜欢的朋友三联加关注!关注鱼市带给你不一样的算法小感悟!(幻听)

再次,感谢鱼骨头教官的学习路线!鱼皮的宣传!小y的陪伴!ok,拜拜,第十八关第二幕见!

你可能感兴趣的:(算法,笔记)