0
点赞
收藏
分享

微信扫一扫

LeetCode回溯算法

Python百事通 2022-03-11 阅读 78

文章目录

回溯算法 vs 深度优先遍历

回溯算法 vs 动态规划

共同点

用于求解多阶段决策问题。多阶段决策问题即:

  • 求解一个问题分为很多步骤(阶段);
  • 每一个步骤(阶段)可以有多种选择。

不同点

  • 动态规划只需要求评估最优解是多少,最优解对应的具体解是什么并不要求。因此很适合应用于评估一个方案的效果;
  • 回溯算法可以搜索得到所有的方案(当然包括最优解),但是本质上它是一种遍历算法,时间复杂度很高。

LeetCode 22. 括号生成

题目

  • LeetCode 22. 括号生成

在这里插入图片描述

回溯

该类问题是在一棵隐式的树上求解,可以用深度优先遍历,也可以用广度优先遍历。用深度优先遍历,代码好写,使用递归的方法,直接借助系统栈完成状态的转移。广度优先遍历需自己编写节点类和借助队列。

n=2为例,画出的树形结构图如下:

在这里插入图片描述

  • 当前剩余左右括号的数量都大于0时,才可以产生分支:

    • 产生左分支时,只看当前剩余的左括号数量是否大于0
    • 产生右分支时,还受左分支的限制,剩余的右括号数量在严格大于剩余左括号数量时,才可以产生分支
  • 在剩余的左右括号数量都等于0的时候结算

public class Solution {

    // 做减法

    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        // 特判
        if (n == 0) {
            return res;
        }

        // 执行深度优先遍历,搜索可能的结果
        dfs("", n, n, res);
        return res;
    }

    /**
     * @param curStr 当前递归得到的结果
     * @param left   左括号还有几个可以使用
     * @param right  右括号还有几个可以使用
     * @param res    结果集
     */
    private void dfs(String curStr, int left, int right, List<String> res) {
        // 因为每一次尝试,都使用新的字符串变量,所以无需回溯
        // 在递归终止的时候,直接把它添加到结果集即可
        if (left == 0 && right == 0) {
            res.add(curStr);
            return;
        }

        // 剪枝(如图,左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝)
        if (left > right) {
            return;
        }

        if (left > 0) {
            dfs(curStr + "(", left - 1, right, res);
        }

        if (right > 0) {
            dfs(curStr + ")", left, right - 1, res);
        }
        
        /*
        if(left == right) {
            // 剩余左右括号数相等,下一个只能用左括号
            getParenthesis(curStr + "(", left - 1, right, res);
        } else if(left < right) {
            // 剩余左括号小于右括号,下一个可以用左括号也可以用右括号
            if(left > 0) {
                getParenthesis(curStr + "(", left - 1, right, res);
            }
            getParenthesis(curStr + ")", left, right - 1, res);
        }
        */
    }
}

严格按照「回溯法」的定义的代码如下:

public class Solution {

    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        if (n == 0) {
            return res;
        }

        StringBuilder path = new StringBuilder();
        dfs(path, n, n, res);
        return res;
    }


    /**
     * @param path  从根结点到任意结点的路径,全程只使用一份
     * @param left  左括号还有几个可以使用
     * @param right 右括号还有几个可以使用
     * @param res
     */
    private void dfs(StringBuilder path, int left, int right, List<String> res) {
        if (left == 0 && right == 0) {
            // path.toString() 生成了一个新的字符串,相当于做了一次拷贝,这里的做法等同于「力扣」第 46 题、第 39 题
            res.add(path.toString());
            return;
        }

        // 剪枝(如图,左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝,注意这个细节)
        if (left > right) {
            return;
        }

        if (left > 0) {
            path.append("(");
            dfs(path, left - 1, right, res);
            path.deleteCharAt(path.length() - 1);
        }

        if (right > 0) {
            path.append(")");
            dfs(path, left, right - 1, res);
            path.deleteCharAt(path.length() - 1);
        }
    }
}

LeetCode 39. 组合总和

题目

  • LeetCode 39. 组合总和

在这里插入图片描述

在这里插入图片描述

回溯

这一类问题都需要先画出树形图,然后编码实现。编码通过 深度优先遍历 实现,使用一个列表,在 深度优先遍历 变化的过程中,遍历所有可能的列表并判断当前列表是否符合题目的要求

以输入:candidates=[2,3,6,7],target=7为例:

在这里插入图片描述

这棵树有 4 个叶子结点的值 0,对应的路径列表是 [[2, 2, 3], [2, 3, 2], [3, 2, 2], [7]],而示例中给出的输出只有 [[7], [2, 2, 3]]。即:题目中要求每一个符合要求的解是 不计算顺序 的。

产生重复的原因是:在每一个结点,做减法,展开分支的时候,由于题目中说 每一个元素可以重复使用,我们考虑了 所有的 候选数,因此出现了重复的列表。

通常会想到借助哈希表(HashSet)天然的去重功能,但实际操作并没有那么容易实现。

可以通过设置 **下一轮搜索的起点begin**实现在搜索的过程中去重:对于这一类相同元素不计算顺序的问题,在搜索的时候就需要 按某种顺序搜索,见下图:

在这里插入图片描述

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(candidates, target, 0, new ArrayList<>());
        return res;
    }

    private void dfs(int[] nums, int target, int begin, List<Integer> path) {
        if (0 == target) {
            // 在 Java 中,参数传递是 值传递,对象类型变量在传参的过程中,复制的是变量的地址。这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,所以对 path 做一次拷贝
            res.add(new ArrayList<>(path));
            return ;
        }

        if (0 > target) {
            return ;
        }

        // 重点理解这里从 begin 开始搜索的语意
        for (int i = begin; i < nums.length; i++) {
            path.add(nums[i]);
            // 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是 i,这里非常容易弄错
            dfs(nums, target - nums[i], i, path);
            path.remove(path.size() - 1);
        }
    }
}

LeetCode 46. 全排列

题目

  • LeetCode 46. 全排列

在这里插入图片描述

在这里插入图片描述

回溯

本题的树形结构如图:

在这里插入图片描述

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        // 布尔数组 used,初始化的时候都为 false 表示这些数还没有被选择,当选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就能够以 O(1) 的时间复杂度判断这个数是否被选择过,这是一种「以空间换时间」的思想。
        boolean[] used = new boolean[nums.length];
        dfs(nums, 0, new ArrayList<>(), used);
        return res;
    }

    private void dfs(int[] nums, int n, List<Integer> path, boolean[] used) {
        if (n == nums.length) {
            res.add(new ArrayList<>(path));
            return ;
        }

        for (int i = 0; i < nums.length; i++) {
            if (!used[i]) {
                path.add(nums[i]);
                used[i] = true;
                dfs(nums, n + 1, path, used);
                used[i] = false;
                path.remove(path.size() - 1);
            }
        }
    }
}

Reference

  • 回溯算法(DFS)+BFS

  • 钰娘娘

  • 回溯算法+剪枝(回溯经典例题详解)

  • 回溯算法入门级详解

举报

相关推荐

0 条评论