DFS中的回溯法(纯暴力穷举)

  • Post author:
  • Post category:其他


首先回溯法是深度搜索(DFS)的一种,即把所有可能穷举,根据条件筛选出符合条件的路径。

回溯法模板格式

dfs(...){
//根据递归终止条件进行筛选
if(符合需要的条件){
存储合理路径
return ...;
}
//对可能路径进行遍历
for(int i=起始条件;i<极限边界;i++){
向路径便令中添加元素
//进行下一轮搜索
def(...);
//深度优化的回头
...removeLast();
}
return ...;
}

下面举一个leetCode上的题目:

给出正整数n,k,求出1,2,3,4…n;求出所有含k个数字且无重复的全部结果。(题目大致是这样的)

用回溯法的解决如下:

public class Backtrack {
    private static Backtrack test;

    public static void main(String[] args) {
        int k = 3;
        int n = 5;
        shen = new Backtrack2();
        test.cobine(n, k);
    }
    public List<List<Integer>> cobine(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        if (k <= 0 || n < k) {
            return res;
        }
//        创建一个list记录搜索路径
        List<Integer> path = new ArrayList<>();
        dfs(n, k, 1, path, res);
        return res;
    }
    public List<List<Integer>> dfs(int n, int k, int start, List<Integer> path, List<List<Integer>> res) {
//        递归终止条件是:path的长度为k
//        System.out.println(path);思路不清晰的话可以打印出每条路径path看下过程对理解很有帮助
        if (path.size() == k) {
            res.add(new ArrayList<Integer>(path));
            return res;
        }
//        遍历可能的搜索起点
        for (int i = start; i <= n; i++) {
//            向路径变量中添加一个数字
            path.add(i);
//            下一轮搜索,设置的搜索起点要加1,因为组合数里不允许出现重复的元素
            dfs(n, k, i + 1, path, res);
//            重点理解这里,深度优化遍历有回头的过程,因此递归之前做了什么,递归之后就要做相同的逆操作
            path.remove(path.size() - 1);
        }
        return res;
    }
}

上面的题目是要求不能有重复元素的,下面给出一道可以有重复题目:

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取

说明:

所有数字(包括 target)都是正整数。

解集不能包含重复的组合。

一个简单的java解题代码如下:

这里假设target=7,candidate={2,3,6,7}

public class CombinationSum {
    public static void main(String[] args) {
        int target = 7;
        int[] candidates = {2, 3, 6, 7};
        CombinationSum shen = new CombinationSum();
        System.out.println("结果是:" + shen.combinationSum(candidates, target).toString());
    }
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
//        判断cambinationSum是否为空,若是空值接返回不需要往下算了
        List<List<Integer>> res = new ArrayList<>();
        if (candidates == null || candidates.length == 0) {
            return null;
        }
//        创建路径记录变量
        List<Integer> path = new ArrayList<>();
        dfs(0,path,candidates,target,res);
        return res;
    }
    public void dfs(int start, List<Integer> path,  int[] candidates, int target,List<List<Integer>> res) {
//        根据终止条件这里先求出每个路径的和再来判断,容易理解,但是代码会比较臃肿
        int sum = 0;
        for (int i = 0; i < path.size(); i++) {
            sum += path.get(i);
        }
        System.out.println(path);//思路不清晰的话可以打印出每条路径path看下过程对理解很有帮助
//        下面的这两个if的判断条件非常重要,DFS里面需要思考的就是这个判断条件部分,其他部分的格式几乎是一样的
        if (sum > target) {
            return ;
        }
        if (sum == target) {
            res.add(new ArrayList<Integer>(path));
            return ;
        }
//       遍历可能的搜索起点
        for (int i = start; i < candidates.length; i++) {
//            想路径变量中添加元素
            path.add(candidates[i]);
//            这里需要注意一下第一个值:i, 元素可以重复的话搜索起点为:i;元素不可重复的话搜索起点为:i+1
            dfs( i, path, candidates, target, res);
//            深度优化搜索的回头过程,这里使用的是List作为搜索路径path,也可以使用其他容器
            path.remove(path.size()-1 );
        }
    }
}

上面两种做法只是完成了最基本的功能并没有进行优化,比较适合像我一样刚接触回溯和dfsd的小白,在这基础上可以进行剪枝,减去不合理路径,使得运算效率更快,下面是对借鉴的另一种解法:

public class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        int len = candidates.length;
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }
        Deque<Integer> path = new ArrayDeque<>();
        dfs(candidates, 0, len, target, path, res);
        return res;
    }
    /**
     * @param candidates 候选数组
     * @param begin      搜索起点
     * @param len        冗余变量,是 candidates 里的属性,可以不传
     * @param target     每减去一个元素,目标值变小
     * @param path       从根结点到叶子结点的路径,是一个栈
     * @param res        结果集列表
     */
    private void dfs(int[] candidates, int begin, int len, int target, Deque<Integer> path, List<List<Integer>> res) {
        // target 为负数和 0 的时候不再产生新的孩子结点
        if (target < 0) {
            return;
        }
        if (target == 0) {
            res.add(new ArrayList<>(path));
            return;
        }
        // 重点理解这里从 begin 开始搜索的语意
        for (int i = begin; i < len; i++) {
            path.addLast(candidates[i]);

            // 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是 i,这里非常容易弄错
            dfs(candidates, i, len, target - candidates[i], path, res);

            // 状态重置
            path.removeLast();
        }
    }
}

这是leetCode比较经典的解法,使用了剪枝,也用了Deque容器,进入链接看具体的讲解:

链接:https://leetcode-cn.com/problems/combination-sum/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/



版权声明:本文为weixin_49823868原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。