最近在网上搜索有关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;
}
运行结果: