LeetCode刷题:DFS与回溯1

引导

回溯算法写法(※难点)
①画出递归树,找到状态变量(回溯函数的参数)※
②确立结束条件
③找准选择列表(与函数参数相关),做出不同的决策,与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择

递归写法:

  1. 异常/失败退出条件

  2. 成功退出条件

  3. 记忆化搜索条件,判定有值直接返回。状态空间一定是memo[m][n]
    在这里插入图片描述

  4. 所有不同的决策。每个决策决定了操作后本节点的状态转移 + 当前规模问题的解,全部列出进行递归:这一步向上向下向左向右(要构造一棵树出来,重点是方向/策略的判断),走出去了结果就完全不同,因此是不同决策;要全部列出来当前能够进行的决策。

  5. 处理当前达到的地方并记录记忆化

  6. 清除当前决策到达的标记
    如果有结果集合,入参中需要传入结果集合对某种可能解进行收录

两种类型:
自顶向下:不断记录当前的历程并且向下深入即可,最后一步存储了所有的历程以及结果,并且是最终问题的解。过程中无需回溯,往往没有对本步结果的处理,在本步骤对dfs的返回值做任何处理,只需要在后续清除影响即可
dfs往往也是void

自底向上(回溯):返回结果,这一步的结果是一个小问题小范围的解,然后回溯回去变为一个大范围解的一部分。需要记录状态/当前规模问题的解
dfs需要返回值并且进行据此修改本次问题的解

DFS 和回溯算法区别
DFS 是一个劲的往某一个方向搜索,而回溯算法建立在 DFS 基础之上的,不同的是在搜索过程中,达到结束条件后,恢复状态,回溯上一层,再次搜索。因此回溯算法与 DFS 的区别就是有无状态重置
https://leetcode-cn.com/problems/subsets/solution/c-zong-jie-liao-hui-su-wen-ti-lei-xing-dai-ni-gao-/

自顶向下类型

113. 路径总和 II

https://leetcode-cn.com/problems/path-sum-ii/在这里插入图片描述对于方案记录,某一个临时方案的任何处理必须在进行本次递归动作后立即回退,任何添加都要无污染,否则会造成某些方案是混合了之前解的。

自顶向下类型:不断记录当前的历程并且向下深入即可,在最后的时候做结果判断与收录

// 正确解法1:
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
    
    
        List<List<Integer>> ans = new ArrayList<>(); // 存储最终所有的结果
        List<Integer> tmp = new ArrayList<>(); // 存储当前结果
        if(root == null) {
    
    
            return ans;
        }
        tmp.add(root.val);
        dfs(root, ans, tmp, root.val, targetSum);
        return ans;
    }
    private void dfs(TreeNode treeNode, List<List<Integer>> ans, List<Integer> tmp, int tmpSum, int target) {
    
    
        // success,到达叶子节点且过程中的总和为target
        if(tmpSum == target && (treeNode.left == null && treeNode.right == null)) {
    
    
            ans.add(new ArrayList<>(tmp));
            return;
        }
        // left
        if(treeNode.left != null) {
    
    
            tmp.add(treeNode.left.val);
            // dfs参数变化
            dfs(treeNode.left, ans, tmp, tmpSum + treeNode.left.val, target);
            tmp.remove(tmp.size() - 1); // 清除影响
        }
        //right
        if(treeNode.right != null) {
    
    
            tmp.add(treeNode.right.val);
            dfs(treeNode.right, ans, tmp, tmpSum + treeNode.right.val, target);
            tmp.remove(tmp.size() - 1);
        }
    }
 
 // 不良解法1:没有清除当前步骤导致对后续造成影响
    private void dfs(TreeNode root, int target, List<Integer> path) {
    
    
        // fail
        if(root == null) {
    
    
            return;
        }
        // success
        if(root.val == target && root.right == null && root.left == null) {
    
    
		// 造成下方new的问题出现在这里:每一步都会改变了path,导致最后不得不去new一个新数组
		// 对临时变量的处理必须在进行本次递归动作后回退
            path.add(root.val);
            result.add(new ArrayList<>(path));
            return;
        }
        
        path.add(root.val);
        dfs(root.left, target - root.val, new ArrayList<>(path));
        // 这一步如果不用new,path就会受到上一步的影响
        dfs(root.right, target - root.val, new ArrayList<>(path));     
    }

// 对上述的优化,减法版本
class Solution {
    
    
    private void dfs(TreeNode root, int target, List<Integer> path) {
    
    
        // fail
        if(root == null) {
    
    
            return;
        }
        // success
        if(target == 0 && root.right == null && root.left == null) {
    
    
            result.add(new ArrayList<>(path));
            return;
        }
        if(root.left != null) {
    
    
            path.add(root.left.val);
            dfs(root.left, target - root.left.val, path);
            path.remove(path.size() - 1);
        }
 
        if(root.right != null) {
    
    
		// 对临时变量的处理全部在进行递归动作前后发生
		// 保证了临时变量的纯净,因为知道下一步dfs不会对临时变量造成任何污染
		// 唯一的污染就在当前的add操作,下一步remove掉就好
            path.add(root.right.val);
            dfs(root.right, target - root.right.val, path);
            path.remove(path.size() - 1);
        }
    }
}
 
// 另一种不良写法:回退影响的位置很怪,不易理解
    private void dfs(TreeNode root, int target, List<Integer> path) {
    
    
        // fail
        if(root == null) {
    
    
            return;
        }
        // success
        if(root.val == target && root.right == null && root.left == null) {
    
    
            path.add(root.val);
            result.add(new ArrayList<>(path));
  		    // 回退对临时变量的污染
            path.remove(path.size() - 1);
            return;
        }
        
      // 这里意味着选取了当前的节点
        path.add(root.val);
     // 当前节点加入path,去深入孩子节点是没有问题的
        dfs(root.left, target - root.val, path);
        dfs(root.right, target - root.val, path);
     // 回退对上一步大规模解的影响,其实是上一步选取了左节点对右节点的影响,如果不回退,每次选取右节点前都会受到左节点这一步的影响
        path.remove(path.size() - 1);
             
    }
 

39. 组合总和

在这里插入图片描述

// 自顶向下
// 减法版本
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    
    
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> tmp = new ArrayList<>();
        dfs(candidates, ans, tmp, target, 0);
        return ans;
    }
// 减法版本的dfs
    private void dfs(int[] candidates, List<List<Integer>>ans, List<Integer> tmp, int target, int index) {
    
    
        // 成功
        if(target == 0) {
    
    
            ans.add(new ArrayList<>(tmp)); // 防止堆污染
            return;
        }
        // 失败,往往是剪枝提前退出
        if(target < 0) {
    
    
            return;
        }
        if(index == candidates.length) {
    
    
            return;
        }
        // 不选自己,直接向下一步走去,自顶向下不做任何处理
        dfs(candidates, ans, tmp, target, index + 1);
        // 选择自己,下一次走到这里还可以选择自己或者不选择自己,在下一步的选择自己就是此步,不选自己就是上一步
       //  tmp add自己即可
        tmp.add(candidates[index]);
        dfs(candidates, ans, tmp, target - candidates[index], index);
        tmp.remove(tmp.size() - 1); // 清除影响,每一步add都会有一个remove,那么就可以确认dfs操作不会对临时方案结果产生影响
    }

// 加法版本 + for循环优化
    private void dfs(int[] candidates, List<List<Integer>> ans, List<Integer> tmp, int target, int cur, int index) {
    
    
        // fail
        if(cur > target) {
    
    
            return;
        }
        // success
        if(cur == target) {
    
    
            ans.add(new ArrayList<>(tmp));
            return;
        }
        for(int i = index; i < candidates.length; i++) {
    
    
        // 小优化1,将递归转化为for循环,将几种情况的递归用for循环优化,避免开栈
        // 从当前index开始,可以选择自己
            tmp.add(candidates[i]);
            dfs(candidates, ans, tmp, target,cur + candidates[i], i);
            tmp.remove(tmp.size() - 1);
        }
    }

// 加法版本的dfs
private void dfs1(int[] candidates, List<List<Integer>> ans, List<Integer> tmp, int target, int cur, int index) {
    
    
        // fail
        if(cur > target || index >= candidates.length) {
    
    
            return;
        }
        // success
        if(cur == target) {
    
    
            ans.add(new ArrayList<>(tmp));
            return;
        }
        // skip
        dfs(candidates, ans, tmp, target,cur, index + 1);
        // choose,区别就在于cur是否加上了这个结果
       //  结合skip的步骤,可以知道index只能在两个范围内跳动,当前的与下一个的。一直skip的实际上就是for循环中的步骤,但是for循环的效率更高的原因在于避免了递归开栈,直接for循环中搞定
        tmp.add(candidates[index]);
        dfs(candidates, ans, tmp, target,cur + candidates[index], index);
        tmp.remove(tmp.size() - 1);
    }

78. 子集 :数组抽取树结构

https://leetcode-cn.com/problems/subsets/
在这里插入图片描述在这里插入图片描述
自顶向下解法:最后生成结果

class Solution {
    
    
    public List<List<Integer>> subsets(int[] nums) {
    
    
        int len = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        Deque<Integer> path = new ArrayDeque<>();
 
        dfs(nums,len,0,res,path);
 
        return res;
    }
 
    private void dfs(int[] nums,int len,int index,List<List<Integer>> res,Deque<Integer> path){
    
    
        // 到最后一位就是整个的结束
        if (index==len){
    
    
            res.add(new ArrayList<>(path));
            return;
        }
		//一直不选则当前数;
        dfs(nums,len,index+1,res,path);
        //选当前数:区别就在于path中添加
        path.add(nums[level]);
        dfs(nums,len,index+1,res,path);
        path.removeLast();
    }
}

46. 全排列:两个状态量回退

在这里插入图片描述
在这里插入图片描述

自顶向下,需要清除2个影响,一个是list的影响,一个是bool矩阵的影响

public class Solution {
    
    
 
    public List<List<Integer>> permute(int[] nums) {
    
    
        int len = nums.length;
        // 使用一个动态数组保存所有可能的全排列
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
    
    
            return res;
        }
 
        boolean[] used = new boolean[len];
        List<Integer> path = new ArrayList<>();
 
        dfs(nums, len, 0, path, used, res);
        return res;
    }
 
    private void dfs(int[] nums, int len, int depth,
                     List<Integer> path, boolean[] used,
                     List<List<Integer>> res) {
    
    
        if (depth == len) {
    
    
            res.add(new ArrayList(path));
            return;
        }
 
         // 区别在于dfs选择操作的结果,这里要求的是全字母的排列,每一个都不能漏,所以每一次都要从0开始撸(for),但是要判断前面处理过没有,如是引入了used
        // 在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。
        for (int i = 0; i < len; i++) {
    
    
            if (!used[i]) {
    
    
                path.add(nums[i]);
                used[i] = true; // 引入了判断是否选取
 
                dfs(nums, len, depth + 1, path, used, res);
                // 注意:下面这两行代码发生 「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
                // 这里将当前的选择重新调整为了不选
                used[i] = false;
                path.remove(path.size() - 1);
            }
        }
    }
 
    public static void main(String[] args) {
    
    
        int[] nums = {
    
    1, 2, 3};
        Solution solution = new Solution();
        List<List<Integer>> lists = solution.permute(nums);
        System.out.println(lists);
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_38370441/article/details/115244115
今日推荐