news 2026/9/9 12:16:27

用回溯法和栈解决阿里面试题排队问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用回溯法和栈解决阿里面试题排队问题

最近在网上搜索有关Catalan number的资料时发现了一道有关Catalan number的很有趣的问题。题目为有12位高矮不同的人,将他们排成两排,每排六人,要求每排六人均从矮到高排列且第二排每个人的身高均大于第一排对应的人的身高,问满足要求的排列方法有多少种。关于这个问题,不妨直接考虑一般情形,12人为2n人,网上对此的分析已经铺天盖地,答案就是Catalan number n+1/C(n,2n),具体的分析过程这里就不再重复赘述了,这里我们主要考虑用计算机解决排队问题,不仅要用高效的算法求出排法总数,还要逐一输出所有的排法。这一编程问题网上已有众多大神给出解答,个个漂亮,但我个人认为我所能搜到的解法还是稍显繁琐,循环套循环,循环接循环,一些一维二维数组,运行效率较低,而且还用到了递归,这样在n非常大,也就是递归深度较深时容易引起堆栈溢出问题。这里我自己考虑了一个算法,避免使用递归,用简单的以循环实现回溯法就可以解决,用到的体积最大的数据结构也就是一个长度为2*n的一维数组,这样就能尽可能降低空间复杂度和时间复杂度,先讲一下求解思路:这篇文章12个高矮不同的人,排成两排/Catalan数_jiyanfeng1的专栏-CSDN博客 的分析给出一种可能的求解思路。试想将2n个人按从矮到高的顺序排成一个序列,每个人若在第一排就以1表示,若在第二排就以2表示,这样在2n个人中选取n个人视为1,剩下的n个人视为2,这样就得到了一个候选解,这个候选解中第一排的n个人和第二排的n个人都是从矮到高排列。现在我们的任务是从所有候选解中选出可行解。满足第二排每个人的身高均大于第一排对应的人的身高的候选解就是可行解。那么由n个1n个2组成的候选解满足什么条件时就是可行解呢?

考察第m个2,为了满足可行解的要求,第m个1必须在第m个2的前面,这样第二排第m个人的身高才能高于第一排第m个人的身高,于是很自然的第m个2前面的1的个数必须大于等于第m个2前面且包括第m个2在内的2的个数m。这样满足以下条件的候选解就是可行解:

候选解中的每个2前面的1的个数大于等于该2前面的所有2且包括该2本身的个数

于是接下来只要编写算法利用以上条件从候选解中筛选出所有可行解就可以了

怎么筛选?

想筛选出可行解,需要按一定的先后次序将n个1和n个2依次放入保存可行解的栈,由于是从栈底起依次放入,因此这是典型的入栈操作,每当一个2被放入后,需要检查栈中包含这个2的部分序列是否是可行解的一部分,若是则继续按一定的规则执行入栈的操作,若不是需要将该2弹出栈(回溯过程),然后若新的队尾为1,则将1换为2,若为2则继续回溯,将2弹出栈,然后对栈中剩下的部分序列继续判断执行回溯或入栈操作。每次入栈时,一般以1优先入栈,如果n个1已全部入栈或2n-1个0和1已全部入栈(这意味着还有最后一个数需要入栈,这个数只能为2),则以2入栈。需要注意的是,首先入栈的必须是1(如果为2则为不合法解的一部分),此外,如果检测到栈中只剩下位于栈底的一个元素1,则所有结果均已求出,终止回溯过程。如果检测到栈满,则找到一个候选解,再检查该候选解是否为可行解,如果是输出找到的一个可行解,否则不做操作。然后再将栈底的2弹出栈,继续向后回溯。值得注意的是,以下实现上述算法的代码不仅能够保证回溯过程中的任意时刻入栈的1不超过n,也能保证入栈的2不超过n,各位看官可以想想为什么。

这就是回溯法求解排队问题的算法实现思路。代码实现过程中需要两个计数变量x1和x2记录回溯过程中某一时刻栈中序列中1和2的个数,通过对命题x1>=x2的真值的判断就可判断栈中的部分序列是否合法,还需要计数变量count记录可行解的个数。另外需要用一个重要的布尔变量TF表示新一轮回溯操作开始时上一轮回溯操作中是否已经将栈顶的2弹出使得弹出后的栈顶即为本轮回溯操作的栈顶,从而为本轮操作是继续入栈还是弹出回溯提供依据。最后,我们需要声明重要的标志变量i记录栈顶位置,在整个回溯过程中,i会不断变动,利用i可以方便地判定栈是否只剩栈底元素以及是否栈满,同时需要利用i操作栈顶,执行压入弹出操作。 以下就是具体的C语言代码实现:(代码中部分TF赋值语句可删去,但为了逻辑和结构清晰仍予以保留)

以下代码在visual studio 2022编译环境下运行通过,n=6时可行解总数为132

#include <stdio.h> #include <malloc.h> #include <string.h> int main() { int x1, x2; int i, n; //n表示排队问题中问题的规模 int count; bool TF; //为true表明上一轮弹出2否则上一轮为入栈操作 int* p; //用于指向动态创建的一维数组,也就是保存可行解的栈 printf("请输入n\n"); scanf_s("%d", &n); //输入问题的规模 p = (int*)malloc(2 * n * sizeof(int)); //创建栈 memset(p, 0, 2 * n * sizeof(int)); //初始化栈 i = 0; x1 = 1; x2 = 0; TF = false; count = 0; //初始化栈顶标志i,部分序列1,2个数x1,x2,弹出标志TF和可行解的计数变量count p[i] = 1; //首次入栈,栈空,关键变量值已在上方确定,1首先入栈,x1自增1 while (1) { if (i != 2 * n - 1) //栈未满 { //非首次入栈栈不空 if (TF == false) { if (p[i] == 1 || x2 <= x1) //部分序列合法执行入栈操作 { if (x1 != n) //1未全部入栈,栈中空位大于1 { p[++i] = 1; //1入栈 ++x1; } else //1全部入栈或栈中只有一个空位 { p[++i] = 2; //2入栈 ++x2; } continue; } else { TF = true; } } else //之前将2弹出栈 { if (p[i] == 1) //栈顶为1 { if (i == 0) //只剩下栈底元素1,回溯结束 break; else //栈中元素个数大于1 { p[i] = 2; //2压入栈取代1,x1自减1,x2自增1 ++x2; --x1; TF = false; continue; } } } } else //栈满,找到候选解 { count++; //这里必定有x2 == x1,因为最后入栈2时,n个1已经全部入栈 printf("第%d个排列:\n", count); int j; for (j = 0; j < 2 * n; j++) { if (p[j] == 2) printf("%d ", j + 1); } printf("\n"); //候选解为可行解,输出,count自增1 for (j = 0; j < 2 * n; j++) { if (p[j] == 1) printf("%d ", j + 1); } printf("\n"); TF = true; } --x2; --i; //2弹出栈回溯 } printf("总共有%d个排列\n", count); //输出可行解总数 return 0; }

运行结果:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 12:16:27

Vue 3 ref 全解析:从模板引用到响应式数据,彻底搞懂

第一次接触 Vue 的开发者&#xff0c;十有八九会被 ref 搞迷糊&#xff0c;因为它在一个框架里干了两件事。说到 vue 里的 ref&#xff0c;社区里每个月的讨论热度都不低&#xff0c;随便一搜就是“ref 是 null”“v-for 里 ref 拿到的是数组”“ref 和 reactive 到底选哪个”这…

作者头像 李华
网站建设 2026/9/9 12:15:51

LS-DYNA K文件报错不求人:K-AGENT AI助手自动定位修复与验证

写过 LS-DYNA 的工程师都有过这种经历&#xff1a;K 文件几百上千行&#xff0c;里面密密麻麻的节点、单元、材料参数、接触卡片&#xff0c;稍不注意一个空格错位、数字越界、关键字没闭合&#xff0c;求解器就把整个任务退回来。更头疼的是&#xff0c;报错信息往往很简短&am…

作者头像 李华
网站建设 2026/9/9 12:15:15

DevExpress XAF添加模块全流程:机制、依赖与坑点排查指南

最近有个刚接触DevExpress XAF的朋友问我&#xff1a;往一个已经跑起来的XAF工程里添加新模块&#xff0c;是不是右键新建一个类库&#xff0c;再引几个引用就行了&#xff1f;我跟他说&#xff0c;如果你把XAF的模块机制理解成普通的类库引用&#xff0c;后面大概率会被各种启…

作者头像 李华
网站建设 2026/9/9 12:14:41

线索二叉树原理与实现:中序线索化C/Java代码详解

做数据结构的同学一定对二叉树的遍历不陌生&#xff0c;但很多人写递归遍历时都没注意到一个问题&#xff1a;一棵 n 个节点的二叉树&#xff0c;用链式存储要开 2n 个指针域&#xff0c;真正存了孩子地址的只有 n-1 个&#xff0c;剩下 n1 个指针全是 NULL。数据量小的时候没啥…

作者头像 李华
网站建设 2026/9/9 12:13:34

2026 G2榜单:Redis、Kafka、SVN可视化工具深度解析与选型指南

做后端开发这么多年&#xff0c;我有个特别深的感受&#xff1a;一个趁手的可视化工具&#xff0c;有时候比框架选型还影响心情。尤其是排查线上问题的时候&#xff0c;别人几分钟就能定位到底是因为缓存穿透、消息积压还是版本冲突&#xff0c;你还在一堆命令行里翻帮助文档&a…

作者头像 李华
网站建设 2026/9/9 12:13:07

嵌入式软硬协同:破解‘互相等待’的四大堵点与工作流设计

1. 这不是甩锅&#xff0c;是信号链没对齐——嵌入式开发里“互相等”的本质“硬件还没调通&#xff0c;软件没法联调”“软件接口文档没给&#xff0c;我怎么画PCB&#xff1f;”“驱动写好了&#xff0c;你板子什么时候能回来&#xff1f;”这三句话&#xff0c;几乎刻在每个…

作者头像 李华