【LeetCode】一文吃透回溯算法(附例题)

文章介绍了回溯算法的概念,将其与深度优先搜索进行比较,并通过全排列问题举例解释了如何使用回溯法。文中还提供了C++代码示例,展示了如何解决46.全排列问题,并提到了去重策略。此外,讨论了排列型、组合型回溯问题以及相关的LeetCode题目。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

回溯 DFS 算法深入浅出,一文吃透!

原文同步在:https://github.com/EricPengShuai/Interview/blob/main/algorithm/回溯算法.md

回溯算法

主要参考的是 liweiwei 的总结

0. 概念

回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的 递归 方法来实现,在反复重复上述的步骤后可能出现两种情况:

  • 找到一个可能存在的正确的答案
  • 在尝试了所有可能的分步方法后宣告该问题没有答案

深度优先搜索 算法(Depth-First-Search,DFS)是一种用于 遍历或搜索树或图 的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。

典型的 DFS 但是没有回溯的题目:17. 电话号码的字母组合

简单比较

「回溯算法」与「深度优先遍历」都有**「不撞南墙不回头」**的意思。「回溯算法」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。

  • 回溯强调了 回退 操作对于搜索的合理性
  • DFS 强调一种 遍历 的思想,与之对应的遍历思想是 BFS

1. 例题(46.全排列

题目

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案

题解

这是一到回溯算法的入门题,很好的说明了回溯算法在选择回退时的用法,几个比较重要的点是:

  1. 递归的重点条件是排列中数字已经选满,可以使用 depth 表示递归深度 或者 index 表示数组下标
  2. 需要使用 used 来标记使用过那些变量
class Solution {
    vector<vector<int>> res;

public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<int> path;
        vector<int> used(nums.size(), 0);

        backtrace(path, used, nums, 0, nums.size());
        return res;
    }

    void backtrace(vector<int>& path, vector<int>& used, vector<int>& nums, int dep, int n) {
        if (dep == n) {
            res.push_back(path);
            return;
        }

        for (int i = 0; i < n; ++ i) {
            if (used[i]) continue;

            path.push_back(nums[i]);
            used[i] = 1;

            backtrace(path, used, nums, dep+1, n);

            used[i] = 0;    // 回溯
            path.pop_back();
        }

    }
};

另外在 C++ 里,最好还是不要写vector<bool>,因为vector<bool>返回的是一个std::vector<bool>::reference的对象,数据量大时比vector<int>要慢,参考

思考

有重复元素的全排列(LC.47)

  • 排序之后再根据 nums[i-1]==nums[i] 判重
  • 在上一个相同的元素撤销之后去重效率更高

组合问题的排列(LC.39)

  • 排列问题:讲究顺序,例如 [LC46.全排列](即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组
  • 组合问题:不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量

有一个小经验就是 used 数组begin 变量 一般不用一起使用

2. 相关问题

排列型回溯

题目说明题解
46. 全排列回溯入门问题,无重复元素的排列问题通过
47. 全排列 II去重是关键,排序比较,上一个相同的元素撤销之后再剪枝剪枝图

组合型回溯

题目说明题解
39. 组合总和组合问题需要按照某种顺序搜索:每一次搜索的时候设置 下一轮搜索的起点 begin,也可以排序之后加速剪枝通过
40. 组合总和 II和 LC.39 区别是这个有重复元素,需要去掉当前层第二个数值重复的节点 🔥剪枝
216. 组合总和 III和 LC.40 类似,这题没有重复元素,两个剪枝:小于最小的 || 大于最大的 🔥0x3F
77. 组合和 LC.39 类似,按照 begin 为起点遍历然后回溯就可以,注意不能重复通过

子集型回溯

题目说明题解
78. 子集和 LC.39 类似,按照 begin 为起点遍历回溯就可以通过
90. 子集 II在 LC.78 的基础上有重复元素的考虑,和 LC.40 类似的剪枝 🔥通过
131. 分割回文串枚举字符之间的逗号,按照 idx 顺序回溯,判断回文通过
698. 划分为k个相等的子集抽象成 k 个桶,每个桶的容量为子集和大小通过
473. 火柴拼正方形和 LC.698 一模一样,抽象成 4 个桶通过
2305. 公平分发饼干k 个桶,但是桶大小未知,从大到小DFS回溯通过
854. 相似度为 K 的字符串DFS 回溯,剪枝,有点难…通过

3. 参考

最后附一份我整理的 CPP 面试相关知识点

https://github.com/EricPengShuai/Interview

logo
如果觉得不错的话可以 ⭐️ 一下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值