【穷举、深搜与回溯算法】一棵决策树讲透全排列与子集

一.全排列:

1.题目描述:


2.题目链接:

46. 全排列 - 力扣(LeetCode)


3.算法原理:

第一步:画决策树

先别写代码,把"选择过程"画成树:

从图上读出三个结论:

概念 含义
层 已经选了几个数(path.size())
节点 当前的已选序列 path
叶子 深度 = n,一个完整排列,只有叶子是答案
分支数 第 1 层 3 个、第 2 层 2 个、第 3 层 1 个 → 3! = 6

第二步:定状态,写骨架

2.1 三个状态变量

变量 类型 作用
ret vector<vector<int>> 收集所有答案
path vector<int> 当前路径(已选序列)
check bool[] 标记元素是否已被使用

2.2 DFS 骨架(六步口诀)

出口 → 枚举 → 剪枝 → 选 → 递归 → 撤

cpp 复制代码
class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    bool check[7];
    vector<vector<int>> permute(vector<int>& nums) {
        dfs(nums);
        return ret;
    }

    void dfs(vector<int> nums)
    {
        if(path.size()==nums.size())
        {
            ret.push_back(path);// path满了,是一个合法排列,放进结果集
            return;
        }
        for(int i=0 ;i<nums.size();i++)
        {
            if(!check[i])//nums[i]还没有被用
            {
                //1.做选择:把nums[i]拿进来
                path.push_back(nums[i]);
                check[i] = true;

                //2.递归
                dfs(nums);

                //回溯核心和恢复现场
                //递归跑完回来,把刚才选的数吐出去,标记改成未使用
                path.pop_back();
                check[i] = false;
            }
        }
    }
};

写 DFS 时只需回答一个问题:站在某个节点上,我要做什么? ------挑一个没用过的数,递归下去,回来时把它放回去。仅此而已,不用在脑子里跑完整棵树。


第三步:三个细节

细节一:为什么要撤销?

path 和 check 是所有分支共用的一份状态,像一块黑板。走完一条路要擦干净,否则下一条分支会踩着上一条的粉笔字继续走。

cpp 复制代码
path.push_back(nums[i]); check[i] = true;   // 写
dfs(nums);
path.pop_back();   check[i] = false;  // 擦

进去动了什么,出来就还原什么。


细节二:check 防什么?

防同一个数被用两次。没它的话,每层都能从 n 个里随便挑,叶子数变成 n^n(还有 [1,1,1] 这种废品);有了它,剩下几个就只能挑几个,正好 n!。


细节三:什么时候算找到一个?

凑满 n 个数才算(path.size() == n)。排列要选满,子集"选过就算"------这就是两者的分界线。


4.代码实现:


二.子集:

1.题目描述:


2.题目链接:

78. 子集 - 力扣(LeetCode)


3.算法原理:

为了获得 nums 数组的所有⼦集,我们需要对数组中的每个元素进⾏选择或不选择的操作,即 nums数组⼀定存在 2^(数组⻓度) 个⼦集。对于查找⼦集,具体可以定义⼀个数组,来记录当前的状态,并对其进⾏递归。

对于每个元素有两种选择:

    1. 不进⾏任何操作;
    1. 将其添加⾄当前状态的集合。在递归时我们需要保证递归结束时当前的状态与进⾏递归操作前的状态不变,⽽当我们在选择进⾏步骤 2 进⾏递归时,当前状态会发⽣变化,因此我们需要在递归结束时撤回添加操作,即进⾏回溯。

递归流程如下:

  • 递归结束条件:如果当前需要处理的元素下标越界,则记录当前状态并直接返回;
  • 在递归过程中,对于每个元素,我们有两种选择:
  1. 不选择当前元素,直接递归到下⼀个元素;
  2. 选择当前元素,将其添加到数组末尾后递归到下⼀个元素,然后在递归结束时撤回添加操作;
  • 所有符合条件的状态都被记录下来,返回即可。

解法一:对每个元素做选 / 不选

树是满二叉树 ,每层只处理 nums[i] 一个元素,分两条边:

cpp 复制代码
void dfs(vector<int>& nums, int i) {
    if (i == nums.size()) {          // 所有元素都决策完了
        ret.push_back(path);         // 叶子 = 一个完整子集
        return;
    }

    // 不选 nums[i]
    dfs(nums, i + 1);

    // 选 nums[i]
    path.push_back(nums[i]);
    dfs(nums, i + 1);
    path.pop_back();                 // 回溯
}

解法二:枚举下一个选谁

cpp 复制代码
void dfs(vector<int>& nums, int pos) {
    ret.push_back(path);             // 进来就收:每个节点都是子集

    for (int i = pos; i < nums.size(); ++i) {
        path.push_back(nums[i]);
        dfs(nums, i + 1);            // 只能往后选,避免 {2,1} 重复
        path.pop_back();
    }
}

4.代码实现:

相关推荐
傅科摆 _ py3 小时前
DP,贪心,堆和栈,二分力扣复习
开发语言·python·算法
kiracrimson3 小时前
栈与队列容器:适配器、deque 与 priority_queue
开发语言·c++·适配器模式
Shaoshing4 小时前
LeetCode100 42.接雨水
java·算法·leetcode
域限奇点访客4 小时前
用AI生成问卷的网站怎么选?看题目录入结构、题型映射和生成后的校验
算法
文丑颜不良啊5 小时前
LeetCode1261~1280题目解析
java·leetcode
qq_466302455 小时前
如何通过部分文件名,读取出整个文件名
c++·qt
not coder5 小时前
一文讲通--从树到红黑树
数据结构·算法
octopus_c5 小时前
C++ 入门
开发语言·c++·算法
爱吃香菜的初学者5 小时前
八.数据结构——哈希表(1)
数据结构·散列表