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

【递归、搜索与回溯】专题(四):回溯算法综合大练兵(上)—— 子集、排列与组合的进阶

文章目录

在试错的迷宫中寻找最优解

一、 前言:从理论走向实战

💬开篇:在上一篇中,我们学习了回溯算法的基石:全排列子集。它们构成了两棵最标准的状态树。

🚀核心进阶:然而,在实际的面试和开发中,题目绝不会这么赤裸裸。它们会在标准模型上增加各种限制条件。这就是回溯算法最核心的考点——剪枝(Pruning)状态维护

💡系列规划:接下来的 15 道综合练习题是回溯算法的精华。为了保证讲解的绝对深度和细节,这 15 道题我将分为上、中、下三篇为大家彻底剖析。

👍点赞、收藏与分享:本篇为(上)篇,我们将重点攻克位运算状态、去重剪枝逻辑、独立空间组合以及规则约束。准备好发车了吗?


二、 找出所有子集的异或总和再求和(位运算+子集)

2.1 题目描述

题目链接:1863. 找出所有子集的异或总和再求和

描述
求数组nums中每个子集异或总和,计算并返回这些值相加之和。
(异或总和:数组中所有元素按位 XOR 的结果;空集为 0)。

示例
输入:nums = [1,3]
输出:6
解释:空集(0) +(1) +(3) +(1^3=2) = 6。

2.2 超详细深度剖析

1. 状态维护的奥秘

这道题的骨架就是求子集。但区别在于:普通求子集是把整个数组存下来,而这里我们只需要子集的异或和

2. ASCII 状态树图解

nums = [5, 1, 6]为例:

(根节点,空集,path=0)------------------sum+=0/|\516path=5path=1path=6sum+=5sum+=1sum+=6/\|166path=4path=3path=7sum+=4sum+=3sum+=7|6path=2sum+=2

注意:状态树上的每一个节点(包含根节点和中间节点)都是一个合法的子集。所以,我们刚进入递归函数时,就要把当前的path累加到全局的sum中。

2.3 C++ 代码实战

classSolution{private:intpath=0;// 记录当前子集的异或和intsum=0;// 记录所有子集异或和的总和public:intsubsetXORSum(vector<int>&nums){dfs(nums,0);returnsum;}// pos 表示当前从哪个下标开始挑选元素voiddfs(vector<int>&nums,intpos){// 1. 记录当前状态:因为每个节点都是合法子集,一进来就累加sum+=path;// 2. 遍历选项:为了防止选了前面的又选后面的造成重复组合,必须从 pos 开始for(inti=pos;i<nums.size();i++){// 3. 做出选择:把当前数字异或进 pathpath^=nums[i];// 4. 递归进入下一层:注意下一层的起点是 i + 1,绝不走回头路dfs(nums,i+1);// 5. 恢复现场:由于 A ^ B ^ B = A,再次异或 nums[i] 即可撤销选择path^=nums[i];}}};

三、 全排列 II(带重复元素的排列与剪枝)

3.1 题目描述

题目链接:47. 全排列 II

描述
给定一个可包含重复数字的序列nums按任意顺序返回所有不重复的全排列。

示例
输入:nums = [1,1,2]
输出:[[1,1,2], [1,2,1], [2,1,1]]

3.2 超详细深度剖析(回溯必考难点)

如果数组里有两个1(我们标记为1 A 1_A1A1 B 1_B1B),普通的排列算法会把[ 1 A , 1 B , 2 ] [1_A, 1_B, 2][1A,1B,2][ 1 B , 1 A , 2 ] [1_B, 1_A, 2][1B,1A,2]算作两种不同的排列,这就导致了重复。

1. 排序:将重复扼杀在摇篮里

要发现重复,必须先把相同的元素凑到一起。第一步绝对是排序sort(nums.begin(), nums.end())

2. 树层去重(剪枝魔法)

我们定下一个强硬的规矩:相同的元素,必须严格按照它们在原数组中的物理顺序来使用。
也就是说,只有在1 A 1_A1A被用掉之后,我才允许你用1 B 1_B1B。如果不按这个顺序,直接跳过(剪枝)。

剪枝逻辑判断:
if (i > 0 && nums[i] == nums[i - 1] && check[i - 1] == false)

3. ASCII 状态树图解(剪枝过程)
数组:[1A, 1B,2][]/|\[1A][1B](剪掉!)[2]/\^ /\[..,1B][..,2]1A没用过![2,1A][2,1B]()|||^[1A,1B,2][1A,2,1B][2,1A,1B]1A没用过!

3.3 C++ 代码实战

classSolution{private:vector<int>path;vector<vector<int>>ret;boolcheck[9]={false};// 题目规定长度 <= 8public:vector<vector<int>>permuteUnique(vector<int>&nums){// 核心第一步:必须要排序!让相同元素相邻sort(nums.begin(),nums.end());dfs(nums,0);returnret;}// pos 表示当前正在填排列的第几个空位voiddfs(vector<int>&nums,intpos){// 1. 递归出口:填满了if(pos==nums.size()){ret.push_back(path);return;}// 2. 遍历所有可选项for(inti=0;i<nums.size();i++){// 如果已经被当前路径用过了,跳过if(check[i])continue;// 【核心剪枝逻辑:树层去重】// 如果和前一个元素相同,并且前一个元素没有被使用// 说明此时如果用它,是在开拓一条和前一个元素完全平行的重复分支,必须剪掉!if(i>0&&nums[i]==nums[i-1]&&check[i-1]==false){continue;}// 3. 做出选择path.push_back(nums[i]);check[i]=true;// 4. 递归下一层dfs(nums,pos+1);// 5. 撤销选择,恢复现场path.pop_back();check[i]=false;}}};

四、 电话号码的字母组合(独立空间的组合)

4.1 题目描述

题目链接:17. 电话号码的字母组合

描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。给出数字到字母的映射如下(与电话按键相同)。

示例
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

4.2 超详细深度剖析

1. 与全排列的区别:独立的备选库

在全排列中,我们是从一个公共的nums数组里挑数字,为了防止重复挑,我们必须引入check数组。
但这道题不同!
如果digits"23"

每一个坑位去哪里挑字母,是完全独立且互不干扰的。所以,这道题不需要check数组

2. 哈希映射技巧

我们需要一个全局的string hash[10]数组,把hash[2]存为"abc",这样在递归到第pos个数字时,可以直接hash[digits[pos] - '0']拿到备选字母表。

4.3 C++ 代码实战

classSolution{private:// 映射表:下标对应数字,内容对应按键字母string hash[10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};string path;vector<string>ret;public:vector<string>letterCombinations(string digits){// 特判:空字符串直接返回空数组if(digits.size()==0)returnret;dfs(digits,0);returnret;}// pos 表示当前正在处理 digits 字符串的第 pos 个字符voiddfs(string&digits,intpos){// 1. 递归出口:所有的数字都处理完了if(pos==digits.size()){ret.push_back(path);return;}// 2. 拿到当前数字对应的“备选字母库”string letters=hash[digits[pos]-'0'];// 3. 在备选库中遍历,尝试每一种可能for(charch:letters){// 做选择path.push_back(ch);// 递归进入下一层,处理下一个数字 (pos + 1)dfs(digits,pos+1);// 撤销选择,恢复现场path.pop_back();}}};

五、 括号生成(带动态规则的排列)

5.1 题目描述

题目链接:22. 括号生成

描述
数字n代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。

示例
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

5.2 超详细深度剖析

1. 什么是有效的括号?

我们在填坑位时,每一位只有两个选择:(或者)
如果是漫无目的地填,肯定会生成))((这种废品。怎样通过剪枝把废品砍掉?
记住合法括号字符串的两个铁律(从左往右看):

  1. 左括号的数量不能超过n
  2. 右括号的数量不能超过当前已放置的左括号数量。(一旦右比左多,必定闭合失败,直接淘汰)。
2. 状态变量的设计

我们需要引入两个变量:left记录已使用的左括号数,right记录已使用的右括号数。

5.3 C++ 代码实战

classSolution{private:intn;intleft=0;// 已使用的左括号个数intright=0;// 已使用的右括号个数string path;// 当前拼接的字符串vector<string>ret;// 结果集public:vector<string>generateParenthesis(int_n){n=_n;dfs();returnret;}voiddfs(){// 1. 递归出口:当右括号也用完 n 个时,意味着左右都用完了,且合法if(right==n){ret.push_back(path);return;}// 2. 尝试添加左括号分支// 剪枝条件:左括号不能超过 nif(left<n){path.push_back('(');left++;// 维护全局状态dfs();path.pop_back();// 恢复现场left--;}// 3. 尝试添加右括号分支// 剪枝条件:右括号数量必须严格小于当前左括号的数量if(right<left){path.push_back(')');right++;// 维护全局状态dfs();path.pop_back();// 恢复现场right--;}}};

六、 组合(顺序无关的子集问题)

6.1 题目描述

题目链接:77. 组合

描述
给定两个整数nk,返回范围[1, n]中所有可能的k个数的组合。

示例
输入:n = 4, k = 2
输出:[[2,4], [3,4], [2,3], [1,2], [1,3], [1,4]]

6.2 超详细深度剖析

1. 组合与排列的核心差异
2. 极致的数学剪枝

如果n = 4k = 4
当你第一层挑了2,你还需要挑 3 个数,但是后面只剩下34两个数了。这注定是一条死路。
剪枝推导

6.3 C++ 代码实战

classSolution{private:vector<int>path;vector<vector<int>>ret;intn,k;public:vector<vector<int>>combine(int_n,int_k){n=_n;k=_k;dfs(1);// 从数字 1 开始挑选returnret;}// start 表示这一层必须从 start 及其后面的数字里挑voiddfs(intstart){// 1. 递归出口:组合的长度达到要求 kif(path.size()==k){ret.push_back(path);return;}// 2. 遍历选择// 【极致剪枝】:如果剩下的数字不够填满 path 了,直接终止循环// 例如:n=4, k=3, 目前 path 空(需3个)。如果从 3 开始挑(剩3,4两个数),3 <= 4 - 3 + 1 (即 3 <= 2) 不成立,直接不进循环for(inti=start;i<=n-(k-path.size())+1;i++){// 做出选择path.push_back(i);// 递归下一层,关键:传给下一层的起点是 i + 1,绝不回头!dfs(i+1);// 恢复现场path.pop_back();}}};

七、 总结:回溯进阶第一阶段

💬复盘总结:通过这 5 道题,我们给回溯的“裸模板”穿上了几件强大的神装。

  1. 位运算记录状态(1863题):当只需记录异或和时,变量的异或和再异或就是天然的恢复现场。
  2. 树层去重法(47题):针对含有重复元素的排列问题,“排序 +!check[i-1]是降维打击的王牌。
  3. 独立解空间(17题):如果每一层的选择库是独立的,直接映射即可,不需要全局check
  4. 动态合法性剪枝(22题):根据业务逻辑(括号匹配原理)动态决定分叉是否可行。
  5. 顺序去重法(77题):针对组合问题,引入start变量,绝不回头,配合长度剪枝,是组合问题的不二法门。

掌握了这些,你已经能在面试中见招拆招了。
在下一篇回溯大练兵(中)里,我们将挑战更复杂的组合总和系列,以及在规则怪异的数组中探索优美排列。别走开,精彩继续! 🚀

http://www.jsqmd.com/news/453978/

相关文章:

  • 跨境卖家如何应对平台对重复铺货的治理升级
  • WordPress 中的Alt文本与图像标题区别
  • 2026年度社交脱单辅助与高情商聊天工具深度测评:谁才是真正的社交解药?
  • 【C/C++】无锁SPSC环形队列
  • JVM中的垃圾回收机制(速记版)
  • VMware虚拟机的安装
  • 毕设程序javaKTV点歌系统 基于SpringBoot的在线音乐点播与管理系统 智能化歌厅曲目服务平台的设计与实现
  • Nexpose 8.38.0 for Linux Windows 发布 - 漏洞扫描
  • 电力系统优化运行与编程:电网规划、负荷预测及潮流计算的Matlab代码模型复现
  • 让预测模型自己进化:BES-SVM黑科技实战
  • AI视频三巨头:一场关于未来想象力的终极PK
  • 瑞祥卡余额怎么提现到支付宝,高效变现指南 - 淘淘收小程序
  • 【C++初阶】:(3)C++基础类和对象(中)
  • 《从零开始的java从入门到入土的学习生活——JavaWeb前端篇》Chapter16——JavaWeb前端篇学习记录——HTML、CSS、盒子模型、flex弹性布局、表单标签
  • 毕设程序javaweb的计算机课程在线学习平台 基于Java Web的计算机技术在线教学与实训平台 计算机专业网络教育及技能测评系统
  • TechWiz LCD 1D应用:高延迟膜(彩虹mura仿真)
  • 企业策略路由(PBR)实战:原理、场景与故障排查(多出口必看)
  • 跨境卖家如何建立供应商考核指标提升稳定性
  • 2026年 喷雾干燥机厂家推荐排行榜:高速离心、气流喷雾、锂电池专用等十大机型核心优势与选购指南 - 品牌企业推荐师(官方)
  • Dify 实战系列(4):实现新闻内容概要生成
  • GLM-4.5 vs GLM-4.7 vs GLM-5 全方位技术演进对比
  • 如何选择优质品牌设计公司
  • 选购费氏粒度仪的关键指标:不仅仅是看测量范围 - 品牌推荐大师1
  • 数据同步备份软件:数字化时代的“双保险”策略
  • 西门子S7-1200PLC双轴定位算法在电池焊接控制中的应用:博图程序案例与威纶触摸屏操作界面
  • 觉察 改变
  • 全栈开发核心技术解析
  • 互联网大厂Java求职面试实战:三轮技术问答与热点技术深度解析
  • 并网逆变器VSG虚拟同步控制Matlab/Simulink仿真模型及其完全正确结果
  • 2026年阿里云企业邮箱代理商哪家好?真实案例解析靠谱伙伴 - 品牌2026