news 2026/4/23 14:21:20

递归回溯法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
递归回溯法

递归回溯法

一、什么是回溯

回溯本质上属于深度优先搜索(DFS,运用到的思想是暴力枚举+剪枝

从字面意思也很好理解,他的步骤就是**<枚举所有的可能,边枚举边验证,无效则回退>**显然在一些需要穷举所有可能的题就能用上。其中我们一般会定义一个char* pathint* path作为一次路径探索的容器

一次被探索(构建)完成(满足问题条件)的path代表一种完全可能(满足条件的一种可能的答案)

// 通用回溯模板(伪代码)voidbacktrack(路径,选择列表,结果集){if(触发限制条件)return;// 剪枝if(满足终止条件){// 一次完整解决了问题的情况把路径加入结果集;return;}for(每个可选的选择 in 选择列表){// 基准问题,每一步做选择(把选择加入路径);backtrack(路径,选择列表,结果集);// 递归撤销选择(把选择从路径中移除);// 回溯}}

二、回溯的基本思路

回溯的模板通常大差不差,具体区别就是每一题的具体要求使得剪枝、穷举、组合的具体规则不尽相同,因此理解每一个板块的核心思路很重要

1.确定问题被解决的情况+确定基准问题

顾名思义,这一部分的思路目标就是:

1. 确定什么情况算完成了一种可能的答案(对应模板中第二个if情况和return)

2. 把整个问题拆分细解到最小最基层的每一步(对应模板中的for循环部分)

例题:给1到n的正整数,取其中k个进行组合,能有多少种不同的组合

基准:取1到n的其中一个数,加入path中

一次被解决的情况:path内已填入元素的个数刚好等于k个

例题:每个数字可以代表多个字母(如1可以是a、b或c),给出一串数字,其代表字母的所有可能

基准:针对一个数字,取这个数字可代表的所有字母中的一个,加入path中

一次被解决的情况:每个数字都取了一个对应字母加入了path,path内字母的个数等于数字串长度

2.确定底线/剪枝条件 or 限制探索步骤

这一步就是弄明白在path探索的过程中,当探索过并已加入了path的元素触犯了什么条件就会使这一次探索不再符合题目条件,从而需要剪枝return;或者在递归调用的时候让元素从新的start开始遍历,避免重复

例题:给数字n,生成n对“合法的”括号,有多少种可能的组合

底线约束方式1:当前已生成的右括号数大于左括号数(无法向左合法匹配)

底线约束方式2:已生成的左括号数或者右括号数超过了n(多于n对)

例题:例题:给1到n的正整数,取其中k个进行组合(!!不能重复取!!),能有多少种不同的组合

底线约束方式:递归调用时,让下一个数字的遍历探索从i+1开始(函数参数多一个start,start作为for循环i的起始)

也就是分成了两种类型的剪枝:

剪枝类型作用典型场景示例条件剪枝操作
合法性剪枝终止「本身无效」的路径括号生成、组合总和括号生成:右括号数 > 左括号数;组合总和:sum > targetreturn;
重复性剪枝避免「重复生成」相同的路径组合、子集、全排列组合:for 循环从 start 开始;全排列:used 数组标记已选元素传新参start

其中传新参和return的操作都很明了易懂,下面详细讲一下用used数组标记已使用元素的做法

used数组的完整使用步骤

  1. 初始化used数组

    在主函数中分配内存并初始化为 0(未选状态),长度等于数组长度:

int*used=(int*)calloc(numsSize,sizeof(int));// calloc 自动初始化为 0,等价于 malloc + memset
  1. 遍历中判断used状态(剪枝)

    在回溯的for循环中,跳过已标记的元素:

for(inti=0;i<numsSize;i++){if(used[i]==1)continue;// 跳过已选元素(核心剪枝)// 选择元素 + 递归 + 回溯}
  1. 选择元素时标记used

    将当前元素加入路径后,标记used[i] = 1

used[i]=1;// 标记为已选path[path_idx]=nums[i];// 加入路径
  1. 递归返回后撤销标记(回溯核心)

    递归返回后,将used[i]恢复为 0,让该元素可被后续路径选择:

backtrack(...);// 递归used[i]=0;// 撤销标记(回溯)

三、解题过程中的一些坑难点

主要聚焦在由于组合问题带来的需要返回结果大小的要求(力扣)

/** * Return an array of arrays of size *returnSize. * The sizes of the arrays are returned as *returnColumnSizes array. * Note: Both returned array and *columnSizes array must be malloced, assume caller calls free(). */int**xxxxxx(...,int*returnSize,int**returnColumnSizes){}
参数名类型核心作用
int** 返回值二维 int 数组存储所有最终结果(如所有组合 / 排列 / 括号串)
int* returnSize一级指针输出参数:返回「结果集的行数」(即有多少个合法答案)
int** returnColumnSizes二级指针输出参数:返回「每行结果的列数」(即每个结果有多少个元素)

必须先为returnColumnSizes分配「和结果集行数上限匹配」的内存:

//main内intmax_size=200000;// 结果集最大行数(根据题目预估)*returnColumnSizes=(int*)malloc(max_size*sizeof(int));// 分配一维数组//自定义递归函数内的已完成分支内(*returnColumnSizes)[*answer_idx]=k;//左式相当于(*returnColumnSizes)[*answer_idx][0]虽说是一个数组,但其实只要一个数

memcpy(目标, 源, 元素个数 *sizeof(元素类型)):

memcpy(answer[*answer_idx],path,k*sizeof(int));//重点在要有sizeof类型// 字符串类(如括号生成):memcpy(ans[idx], path, len * sizeof(char));

主函数末尾必须将「实际生成的结果数」赋值给*returnSize

*returnSize=answer_idx;// answer_idx是回溯中统计的结果行数

一般常规操作:

voidbacktravel(...,int**answer,int*answer_idx,int**returnColumnSizes,intpath_idx,int*path,intstart,...){if(完整条件){//完成(填满)answer[*answer_idx]=(int*)malloc(ok_size*sizeof(int));memcpy(answer[*answer_idx],path,ok_size*sizeof(int));(*returnColumnSizes)[*answer_idx]=ok_size;//这个ok_size看题目来变动,一般是等于path_idx(*answer_idx)++;return;//不要忘了这个!!!}for(inti=start;i<=n;i++){//基准的每一步path[path_idx]=i;backtravel(...,answer,answer_idx,returnColumnSizes,path_idx+1,path,i+1,...);}}main(){int**answer=(int**)malloc(max_size*sizeof(int*));//max_size根据题意调整int*path=(int*)calloc(max_size,sizeof(int));*returnColumnSizes=(int*)malloc(max_size*sizeof(int));intanswer_idx=0;backtravel(...,&answer_idx,...);free(path);*returnSize=answer_idx;returnanswer;}

四、例题

题解:

这题只需穷举,还不存在明显的剪枝constchar*numtoword[]={//定义数字对应字母集"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz",};//确定基准:每一个数字的所有可能//for遍历解决每个数字的所有可能,递归解决这个数字紧接着的下一个数字的所有可能//path是一个字符串,代表一种完全可能(每个数字都已对应着某一种字母的排列)voidbacktrap(char*digits,char**answer,int*answer_idx,char*path,intpath_idx){if(digits[path_idx]=='\0'){//这条路已经探索完整answer[*answer_idx]=(char*)malloc((strlen(path)+1)*sizeof(char));strcpy(answer[*answer_idx],path);(*answer_idx)++;//作用于全体,因为是下一条路了return;}//对于当前数字,for遍历当前数字的所有可能intnum=digits[path_idx]-'0';intpossible_len=strlen(numtoword[num]);//这个数字对应的字母集的长度就是他所有可能的个数for(inti=0;i<possible_len;i++){//基准的最小步path[path_idx]=numtoword[num][i];backtrap(digits,answer,answer_idx,path,path_idx+1);//对于这个数字的这个字母,他的下一个数字对应的字母又有许多可能}}char**letterCombinations(char*digits,int*returnSize){intdigits_len=strlen(digits);intpossible_path_count=0;int*pb_pac=&possible_path_count;intanswer_max_size=1;//最大的 可能的“组合” 的 个数for(inti=0;i<digits_len;i++){intnum=digits[i]-'0';answer_max_size*=strlen(numtoword[num]);}char**answer=(char**)malloc(answer_max_size*sizeof(char*));char*path=(char*)malloc((digits_len+1)*sizeof(char));//每条路即每个组合的字母数显然等于数字个数,加一个给\0path[digits_len]='\0';backtrap(digits,answer,pb_pac,path,0);//从第一个数字开始*returnSize=possible_path_count;free(path);returnanswer;}

题解:

本题出现了剪枝voidbacktrap(char**answer,char*path,intpath_idx,int*answer_idx,inttotaltimes,intlessleft,intlessright){if(lessleft<0||lessright<0||lessright<lessleft)return;//剪枝if(path_idx==totaltimes&&lessleft==0&&lessright==0){//完成一条路answer[*answer_idx]=(char*)malloc((totaltimes+1)*sizeof(char));strcpy(answer[*answer_idx],path);(*answer_idx)++;return;}if(lessleft>0){path[path_idx]='(';backtrap(answer,path,path_idx+1,answer_idx,totaltimes,lessleft-1,lessright);path[path_idx]='\0';}if(lessright>0&&path_idx!=0){path[path_idx]=')';backtrap(answer,path,path_idx+1,answer_idx,totaltimes,lessleft,lessright-1);path[path_idx]='\0';}}char**generateParenthesis(intn,int*returnSize){inttotaltimes=n*2;intmax_size=2000;char**answer=(char**)malloc(max_size*sizeof(char*));char*path=(char*)calloc(totaltimes+1,sizeof(char));intpath_idx=0,answer_idx=0;int*ans_idx=&answer_idx;backtrap(answer,path,0,ans_idx,totaltimes,n,n);*returnSize=answer_idx;free(path);returnanswer;}

题解:

本题用到了used数组避免重复元素voidback(int*nums,intnumsSize,int*used,int*path,int**answer,int*answer_idx,intpath_idx,int**returnColumnSizes){if(path_idx==numsSize){answer[*answer_idx]=(int*)malloc(numsSize*sizeof(int));memcpy(answer[*answer_idx],path,numsSize*sizeof(int));(*returnColumnSizes)[*answer_idx]=numsSize;(*answer_idx)++;return;}for(inti=0;i<numsSize;i++){if(used[i]==1)continue;used[i]=1;path[path_idx]=nums[i];back(nums,numsSize,used,path,answer,answer_idx,path_idx+1,returnColumnSizes);used[i]=0;}}int**permute(int*nums,intnumsSize,int*returnSize,int**returnColumnSizes){int**answer=(int**)malloc(1000*sizeof(int*));int*path=(int*)malloc(numsSize*sizeof(int));*returnColumnSizes=(int*)malloc(1000*sizeof(int));int*used=(int*)calloc(numsSize,sizeof(int));intanswer_idx=0;back(nums,numsSize,used,path,answer,&answer_idx,0,returnColumnSizes);*returnSize=answer_idx;returnanswer;}

题解:

本题用到了传新参start避免结果的重复(如[2,2,3][2,3,2])voidbacktravel(int*candidates,intcandidatesSize,inttarget,int**returnColumnSizes,int*path,int**answer,int*answer_idx,intpath_idx,intsum,intstart){if(sum>target)return;//剪枝if(sum==target){//完成一条路answer[*answer_idx]=(int*)malloc(path_idx*sizeof(int));memcpy(answer[*answer_idx],path,path_idx*sizeof(int));(*returnColumnSizes)[*answer_idx]=path_idx;(*answer_idx)++;return;}for(inti=start;i<candidatesSize;i++){intnum=candidates[i];path[path_idx]=num;backtravel(candidates,candidatesSize,target,returnColumnSizes,path,answer,answer_idx,path_idx+1,sum+num,i);}}int**combinationSum(int*candidates,intcandidatesSize,inttarget,int*returnSize,int**returnColumnSizes){int**answer=(int**)malloc(150*sizeof(int*));int*path=(int*)calloc(100,sizeof(int));*returnColumnSizes=(int*)malloc(150*sizeof(int));intanswer_idx=0;backtravel(candidates,candidatesSize,target,returnColumnSizes,path,answer,&answer_idx,0,0,0);*returnSize=answer_idx;free(path);returnanswer;}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/19 12:27:21

Wan2.2-T2V-A14B如何生成带有地图轨迹的行进路线动画?

Wan2.2-T2V-A14B如何生成带有地图轨迹的行进路线动画&#xff1f; 你有没有遇到过这种情况&#xff1a;手头有一段复杂的运输路线&#xff0c;领导说“做个动画演示一下”&#xff0c;结果打开AE发现光建个地图场景就得半天&#xff1f;&#x1f92f; 别急——现在&#xff0c;…

作者头像 李华
网站建设 2026/4/17 16:24:36

Wan2.2-T2V-A14B在消防逃生演练视频中的紧急情境构建

Wan2.2-T2V-A14B在消防逃生演练视频中的紧急情境构建 &#x1f525; 想象一下&#xff1a;一栋写字楼突然起火&#xff0c;浓烟滚滚&#xff0c;警报拉响——但这场“灾难”其实从未真实发生。它是由一段文字描述自动生成的高清视频&#xff0c;逼真到连逃生人群的脚步节奏、弯…

作者头像 李华
网站建设 2026/4/20 3:43:54

day31(12.11)——leetcode面试经典150

71. 简化路径 71. 简化路径 我又用的笨方法哇库哇库 题目&#xff1a; 题解&#xff1a; class Solution {public String simplifyPath(String path) {//用stream进行切割筛选String[] s Arrays.stream(path.split("/")).filter(str -> !str.isEmpty()).toAr…

作者头像 李华
网站建设 2026/4/18 7:59:34

Jmeter 性能-内存溢出问题定位分析

1、堆内存溢出①稳定性压测一段时间后&#xff0c;Jmeter报错&#xff0c;日志报&#xff1a;java.lang.OutOfMemoryError.Java heap space②用jmap -histo pid命令dump堆内存使用情况&#xff0c;查看堆内存排名前20个对象。看是否有自己应用程序的方法&#xff0c;从最高的查…

作者头像 李华
网站建设 2026/4/18 5:01:45

豆包手机:当手机里住进一个会干活的室友

如果你把手机想象成一个装满小工具的盒子&#xff0c;那“豆包手机”更像是给这个盒子安了一位能干的室友&#xff1a;会主动帮你收拾、能理解你的指令、还时不时抖个机灵。它不一定是某个具体型号&#xff0c;更是一种“AI原生”的手机使用方式——把豆包这样的智能助手贯穿系…

作者头像 李华
网站建设 2026/4/22 22:11:35

10 个专科生文献综述降重工具,AI 免费网站推荐

10 个专科生文献综述降重工具&#xff0c;AI 免费网站推荐 论文路上的“隐形压力”&#xff1a;专科生如何突围&#xff1f; 对于很多专科生来说&#xff0c;撰写文献综述不仅是学术训练的一部分&#xff0c;更是毕业路上必须跨越的一道门槛。然而&#xff0c;面对繁重的写作任…

作者头像 李华