当前位置: 首页 > article >正文

【递归与回溯深度解析:经典题解精讲(上篇)】—— LeetCode

文章目录

  • 全排列
  • 子集
  • 找出所有子集的异或总和再求和
  • 全排列||
  • 电话号码的字母组合
  • 括号生成

全排列

在这里插入图片描述
解题思路
这是一道典型的 回溯(Backtracking)问题,我们需要枚举所有可能的子集。关键点是每个数字都有两种选择:要么包含,要么不包含。

详细步骤:

  • 回溯的核心思想:
    每个元素都有两种选择:加入当前子集或者不加入。
    通过递归,处理完每个元素后生成对应的子集。
  • 终止条件:
    如果当前遍历的索引等于数组长度,说明已经生成一个完整的子集,将其加入结果。
  • 回溯的过程:
    将当前元素加入子集,继续递归。
    回溯后,将当前元素移除,继续探索不加入当前元素的可能性
class Solution 
{
    // 存储最终结果的二维数组,每个子数组是一个排列
    vector<vector<int>> ret;
    // 当前递归路径,用于构建一个排列
    vector<int> path;
    // 标记数组,用于记录 nums 中的元素是否被使用过
    bool check[7]; // 假设 nums.size() 最大为 7,实际可用动态数组优化

public:
    vector<vector<int>> permute(vector<int>& nums) 
    {
        // 开始深度优先搜索
        dfs(nums);

        return ret;
    }

    // 深度优先搜索函数
    void dfs(vector<int>& nums)
    {
        // 如果当前路径的长度等于 nums 的大小,表示形成了一个完整排列
        if(nums.size() == path.size())
        {
            // 将当前路径加入最终结果
            ret.push_back(path);
            return;
        }

        // 遍历 nums 中的每个元素
        for(int i = 0; i < nums.size(); i++)
        {
            // 如果当前元素未被使用
            if(!check[i])
            {
                // 将当前元素加入路径
                path.push_back(nums[i]);

                // 标记该元素为已使用
                check[i] = true;

                // 递归处理下一个位置
                dfs(nums);

                // 回溯:撤销选择
                path.pop_back();

                // 解除标记,表示该元素可以再次使用
                check[i] = false;
            }
        }
    }
};

子集

在这里插入图片描述
解题思路
这是一个典型的 回溯+交换(Backtracking with swapping)问题。关键点是枚举数组的所有排列组合。为了实现全排列,每次递归时需要将一个数字固定到当前位置,然后递归处理剩余数字。

详细步骤:

  • 回溯的核心思想:
    固定当前的一个数字,通过递归处理剩余数字。
    通过交换,动态调整数组,避免额外空间开销。
  • 终止条件:
    如果当前索引达到数组长度,说明已经生成一个完整排列,将其加入结果。
  • 回溯的过程:
    将当前索引位置的数字与后续数字交换,递归处理剩余数字。
    回溯时恢复数组原状(撤销交换)。
class Solution 
{
    // `ret` 用于存储所有子集的结果
    vector<vector<int>> ret; 
    // `path` 用于存储当前递归路径上的子集
    vector<int> path;
public:
    // 主函数,用于计算输入数组的所有子集
    vector<vector<int>> subsets(vector<int>& nums)
    {
        // 从第 0 个元素开始深度优先搜索
        dfs(nums, 0);    
        // 返回最终生成的所有子集
        return ret;
    }

    // 深度优先搜索函数,`nums` 为输入数组,`pos` 为当前元素的位置索引
    void dfs(vector<int>& nums, int pos)
    {
        // 递归结束条件:如果已经遍历到数组末尾,将当前路径加入结果
        if (nums.size() == pos)
        {
            ret.push_back(path); // 将当前路径(一个子集)添加到结果集中
            return; // 终止当前递归
        }

        // 选择当前元素:将当前元素加入路径
        path.push_back(nums[pos]);
        // 递归处理包含当前元素的子集
        dfs(nums, pos + 1);
        // 回溯:移除最后加入的元素,恢复路径状态
        path.pop_back();

        // 不选择当前元素:直接递归处理不包含当前元素的子集
        dfs(nums, pos + 1);
    }
};

找出所有子集的异或总和再求和

在这里插入图片描述
解题思路
这是一道典型的 回溯 问题,要求计算所有子集的异或值总和。

子集枚举:通过回溯枚举所有子集。
异或计算:在回溯的过程中,用一个变量记录当前路径的异或值。
终止条件:当遍历到数组末尾时,将当前异或值累加到结果中。
详细步骤:

  • 使用回溯生成所有子集,定义一个变量记录当前子集的异或总和。

在回溯时,每次有两种选择:

  • 选择当前元素:更新异或值并递归。
    不选择当前元素:保持当前状态递归。

遍历完后,将路径上的异或值加入结果中。

class Solution 
{
    // 用于记录当前路径中的 XOR 结果
    int path;
    // 用于记录所有子集的 XOR 结果的总和
    int sum;

public:
    int subsetXORSum(vector<int>& nums) 
    {
        // 从 0 开始的深度优先搜索 (DFS)
        dfs(nums, 0);

        // 返回累加的 XOR 和
        return sum;    
    }

    // 深度优先搜索函数,用于生成所有子集并计算 XOR
    void dfs(vector<int>& nums, int pos)
    {
        // 每次进入递归时,将当前路径的 XOR 结果加入总和
        sum += path;

        // 从当前位置开始,尝试添加下一个数字
        for(int i = pos; i < nums.size(); i++)
        {
            // 将当前数字加入路径(通过 XOR 操作)
            path ^= nums[i];

            // 递归调用,处理当前位置之后的数字
            dfs(nums, i + 1);

            // 撤销选择,恢复路径到上一个状态
            path ^= nums[i];
        }
    }
};

全排列||

在这里插入图片描述
解题思路
这是一道 回溯+剪枝 问题,核心在于生成全排列的同时避免重复。

关键点:

  • 排序去重:
    为了避免重复,先将数组排序。
    在递归时,当遇到相同的元素且上一个相同的元素还未使用完时,跳过该分支。
  • 状态数组:
    使用一个 check数组记录当前元素是否被使用,防止重复选取。
  • 回溯过程:
    在路径中加入当前数字,递归处理剩余数字。
    回溯时移除当前数字。
class Solution
{
    vector<vector<int>> ret;  // 用于存储所有不重复的排列结果
    vector<int> path;         // 当前排列的路径
    bool check[9];            // 用于标记某个元素是否已被使用(假设输入数组最多有 9 个元素)
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) 
    {
        // 对输入数组进行排序,这是为了处理重复元素,方便去重
        sort(nums.begin(),nums.end());
        dfs(nums, 0); // 从第 0 个位置开始深度优先搜索
        return ret;   // 返回所有不重复排列的结果
    }

    void dfs(vector<int>& nums, int pos)
    {
        // 如果路径的长度等于数组大小,说明找到一个完整的排列
        if (pos == nums.size())
        {
            ret.push_back(path); // 将当前排列加入结果集
            return;              // 结束当前递归
        }

        // 遍历数组中的每个元素
        for (int i = 0; i < nums.size(); i++)
        {
            // 如果当前元素未被使用
            // 并且以下任一条件成立:
            // 1. 当前是第一个元素 (i == 0)
            // 2. 当前元素与前一个元素不同 (nums[i] != nums[i - 1])
            // 3. 当前元素等于前一个元素,但前一个元素已被使用 (check[i - 1] == true)
            if (check[i] == false && (i == 0 || nums[i] != nums[i - 1] || check[i - 1]))
            {
                path.push_back(nums[i]); // 将当前元素加入路径
                check[i] = true;         // 标记当前元素已被使用
                dfs(nums, pos + 1);      // 递归处理下一个位置
                path.pop_back();         // 回溯:移除最后一个元素
                check[i] = false;        // 回溯:取消当前元素的使用标记
            }
        }
    }
};

电话号码的字母组合

在这里插入图片描述

解题思路
这是一个 回溯问题,核心在于处理多分支递归。每个数字可以映射到多个字母,相当于在路径中枚举每个数字对应的字母。

详细步骤:

  • 建立映射表:
    使用哈希表记录数字到字母的映射关系。
  • 回溯搜索:
    每次递归处理一个数字,遍历其对应的所有字母。
    将当前字母加入路径,递归处理剩余数字。
    回溯时移除当前字母。
  • 终止条件:
    如果路径长度等于输入字符串长度,生成一个完整的字母组合。
class Solution 
{
    string path;                  // 用于存储当前的组合路径
    vector<string> ret;           // 存储所有可能的字母组合结果
    string hash[10] = {"", "",    // 映射 0 和 1 对应无字母
                       "abc",     // 映射数字 2 到对应的字母
                       "def",     // 映射数字 3
                       "ghi",     // 映射数字 4
                       "jkl",     // 映射数字 5
                       "mno",     // 映射数字 6
                       "pqrs",    // 映射数字 7
                       "tuv",     // 映射数字 8
                       "wxyz"};   // 映射数字 9
public:
    vector<string> letterCombinations(string digits) 
    {
        // 如果输入的数字字符串为空,直接返回空的结果集
        if(digits.size() == 0) return ret;
        
        // 从第 0 个数字开始进行深度优先搜索 (DFS)
        dfs(digits, 0);
        
        // 返回所有可能的字母组合
        return ret;
    }

    void dfs(string& digits, int pos)
    {
        // 如果当前的路径长度等于输入的数字长度,说明已生成一个完整的字母组合
        if(pos == digits.size())
        {
            ret.push_back(path); // 将当前路径加入结果集
            return;             // 结束当前递归
        }
        
        // 遍历当前数字对应的所有字母
        for(auto ch : hash[digits[pos] - '0'])
        {
            path.push_back(ch);  // 将当前字母加入路径
            dfs(digits, pos + 1); // 递归处理下一个数字
            path.pop_back();     // 回溯:移除最后一个字母
        }
    }
};

括号生成

在这里插入图片描述
解题思路
这是一个 回溯问题,关键在于如何保证括号的合法性。

详细步骤:

  • 规则约束:
    只有在左括号数量未超过 n 时,才可以加入左括号。
    只有在右括号数量小于左括号数量时,才可以加入右括号。
  • 递归过程:
    每次递归处理一个括号,根据约束条件选择加入左括号或右括号。
  • 终止条件:
    当左右括号数量都等于 n 时,生成一个完整的括号组合。
class Solution 
{
    int left, right, n;          // `left`表示当前已经使用的左括号数量,`right`表示已使用的右括号数量,`n`表示总的括号对数
    vector<string> ret;          // 存储所有可能的合法括号组合结果
    string path;                 // 当前生成的括号路径
public:
    vector<string> generateParenthesis(int _n) 
    {
        n = _n;                  // 初始化括号对数
        dfs();                   // 开始深度优先搜索
        return ret;              // 返回所有合法的括号组合
    }

    void dfs()
    {
        // 如果右括号数量等于括号对数,说明当前路径是一个完整的合法括号组合
        if(right == n)
        {
            ret.push_back(path); // 将当前路径加入结果集
            return;              // 结束当前递归
        }

        // 如果左括号数量小于总括号对数,可以继续添加左括号
        if(left < n)
        {
            path.push_back('('); // 添加一个左括号
            left++;              // 左括号计数加一
            dfs();               // 递归处理下一个状态
            path.pop_back();     // 回溯:移除最后一个括号
            left--;              // 回溯:左括号计数减一
        }

        // 如果右括号数量小于左括号数量,可以继续添加右括号
        if(right < left)
        {
            path.push_back(')'); // 添加一个右括号
            right++;             // 右括号计数加一
            dfs();               // 递归处理下一个状态
            path.pop_back();     // 回溯:移除最后一个括号
            right--;             // 回溯:右括号计数减一
        }
    }
};

http://www.kler.cn/a/452380.html

相关文章:

  • oracle linux8.10+ oracle 23ai安装
  • JZ31 栈的压入、弹出序列
  • 实现 QTreeWidget 中子节点勾选状态的递归更新功能只影响跟节点的状态父节点状态不受影响
  • Odoo 免费开源 ERP:通过 JavaScript 创建对话框窗口的技术实践分享
  • 发际线不断后移,生发液排行榜第一名,让绒毛碎发爆出来
  • 容器技术所涉及Linux内核关键技术
  • 每天40分玩转Django:Django表单集
  • 在 Mac M2 上安装 PyTorch 并启用 MPS 加速的详细教程与性能对比
  • 使用Python探索量子机器学习
  • ByConity BSP 解锁数据仓库新未来
  • Android DRM 技术详解与应用实践
  • HarmonyOS NEXT 实战之元服务:静态案例效果--- 手机一键加速、手机垃圾清理
  • 中阳智能:量化交易助力科技与金融融合
  • 基于LSTM长短期记忆神经网络的多分类预测【MATLAB】
  • 跟我学c++中级篇——C++中的缓存利用
  • 达梦数据库-数据共享集群部署
  • vue3导入excel并解析excel数据渲染到表格中,纯前端实现。
  • CSS 居中技术完全指南:从基础到高级应用
  • SpeedTree For UE5学习笔记
  • 分布式Python计算服务MaxFrame使用心得
  • <代码随想录> 算法训练营-2024.12.25
  • Linux零基础速成篇一(理论+实操)
  • 强力巨彩租赁屏技术更新,适用多种户外应用场景
  • xtu oj 1614 数字(加强版)
  • 【Super Tilemap Editor使用详解】(十一):画笔(Brushes)
  • SSE 流式场景应用 及 方案总结