从一道排座位问题谈起
问题描述
在一个班级中挑选N个学生排成一列座位(保证有足够多的男生与足够多的女生),要求座位序列中男生互不相邻,求解有多少种排列方式?(挑选男生与女生的数量与排列方式均为任意)
例如挑选三个学生,那么所有排列为: 女女女、女男女,男女女,女女男,男女男
(时间限制:1s 空间限制:65536kb)
输入
第一个数为学生总数N (0<N<30)
输出
只有一行,保证男生与男生不相邻,座位排列的所有情况数目的结果
输入样例
3
输出样例
5
问题分析
第一种思路:如果单纯从高中数学的角度看,这是一道关于排列组合的问题。不难知道,当男生人数为x 时,女生人数为N-x, 且各男生与各女生视为完全相同的个体,此题可以使用插空法解决。为了保证男生之间互不相邻,男生需要站在女生之间或两侧,共(N-x+1)个位置,从中选取x个,共有C(x, (N-x+1)))种,男生可以没有,最多可以比女生多一个。假设男生有x人,女生有y人,则有关系:
由此可得方案总数为:
于是该题的解决转移到组合数的求解问题上。但组合数的求解往往需要大量的计算,如果想要优化,不仅会使得问题变得复杂,也会增加计算量,随着N的增加,运行时间会迅速增长。因此不做进一步讨论。
第二种思路:本题是一道较为明显的动态规划问题。对于长度为N的队伍,可以以这样的方式排列:先考虑队伍最左面的同学,如果该同学为女生,则与之相邻的可以是男生或女生,因此接着排列剩余的N-1位同学,问题转移到长度为N-1的情况;如果第一位同学是男生,则第二位一定是女生,第三位又可以任意排,问题转移到长度为N-2的情况。再考虑基本情况,对于N=1,有两种情况,对于N=2,有三种情况。
假设长度为N的方案数为f(N),有
可以用递归函数实现。其代码如下:
int get(int n)
{
if(n == 1) return 2;
if(n == 2) return 3;
return get(n - 1) + get(n - 2);
}
以N=4为例,其调用情况绘制一颗树其中f(1)被计算了8次。不难发现,随着N的增加,程序运行效率将大幅降低,不能在较短时间内结束。该算法时间复杂度为O(2 ^ N),空间复杂度为O(2 ^ N)
于是考虑记忆化。我们可以开一个数组a,用a[i]表示队伍人数为i时的方案数量。按照上述关系,得到一种较短的AC代码如下:
#include<stdio.h>
int main()
{
int n;
scanf("%d",&n);
int a[35]={0,2,3};
int i;
for(i=3;i<=n;i++)
a[i]=a[i-1]+a[i-2];
printf("%d",a[n]);
}
该算法时间复杂度为O(N),空间复杂度为O(N)
至此,本题得以解决。与本题类似的还有经典的台阶问题:走完N级台阶,每次可以走一步或两步,一共有多少种走法。
同样利用f(x)=f(x-1)+f(x-2)的递推关系,只需考虑N=1和N=2的情况。
其变式也有很多,比如铺地砖问题:
对于2N的地面,如果只有21一种砖,可以在地面左上角竖着铺一块,问题变成2*(N-1)的情况,如何横着铺,则必须在左下角也横着铺一块,问题变成2*(N-2)的情况。
这里还有进阶版的铺地砖,问题变成两种地砖,这里贴上来自另一位网友的链接:
从铺砖问题到排列组合算法的实现
当然台阶问题还存在变式,比如每次可以走最多k个台阶(1<k<N),这需要算法的进一步优化,留给读者思考。
第三种思路:根据前文的分析,可以发现方案数的排列是斐波那契数列的一部分,可以利用通项公式:
当然,需注意该数列的第一项是2,需要调整指数为n,代码如下:
int get(int n)
{
double sqrt5 = sqrt(5);
double left = (1.0 + sqrt5)/2.0;
double right = (1.0- sqrt5)/2.0;
return (int)round(pow(left, n));
}
该算法时间复杂度为O(1),空间复杂度为O(1)。
但该函数对于较大的N存在精度不足的问题,有待读者进一步改进。