12个高矮不同的人,排成两排/Catalan数


作者:baihacker 

来源:http://hi.baidu.com/feixue http://hi.csdn.net/baihacker 

问题描述: 
12个高矮不同的人,排成两排,每排必须是从矮到高排列,而且第二排比对应的第一排的人高,问排列方式有多少种? 

还有一道和这个题相关的题:http://blog.csdn.net/jiyanfeng1/article/details/8068811 (平衡括号问题)

问题分析: 
我们先把这12个人从低到高排列,然后,选择6个人排在第一排,那么剩下的6个肯定是在第二排. 
用0表示对应的人在第一排,用1表示对应的人在第二排,那么含有6个0,6个1的序列,就对应一种方案. 
比如000000111111就对应着 
第一排:0 1 2 3 4 5 
第二排:6 7 8 9 10 11 
010101010101就对应着 
第一排:0 2 4 6 8 10 
第二排:1 3 5 7 9 11 
问题转换为,这样的满足条件的01序列有多少个. 
观察1的出现,我们考虑这一个出现能不能放在第二排,显然,在这个1之前出现的那些0,1对应的人 
要么是在这个1左边,要么是在这个1前面.而肯定要有一个0的,在这个1前面,统计在这个1之前的0和1的个数. 
也就是要求,0的个数大于1的个数. 
OK,问题已经解决. 
如果把0看成入栈操作,1看成出栈操作,就是说给定6个元素,合法的入栈出栈序列有多少个. 
这就是catalan数,这里只是用于栈,等价地描述还有,二叉树的枚举,多边形分成三角形的个数,圆括弧插入公式中的 
方法数,其通项是c(2n, n)/(n+1). 

在 < <计算机程序设计艺术>>,第三版,Donald E.Knuth著,苏运霖译,第一卷,508页,给出了证明: 
问题大意是用S表示入栈,X表示出栈,那么合法的序列有多少个(S的个数为n) 
显然有c(2n, n)个含S,X各n个的序列,剩下的是计算不允许的序列数(它包含正确个数的S和X,但是违背其它条件). 
在任何不允许的序列中,定出使得X的个数超过S的个数的第一个X的位置.然后在导致并包括这个X的部分序列中,以 
S代替所有的X并以X代表所有的S.结果是一个有(n+1)个S和(n-1)个X的序列.反过来,对一垢一种类型的每个序列,我们都能 
逆转这个过程,而且找出导致它的前一种类型的不允许序列.例如XXSXSSSXXSSS必然来自SSXSXXXXXSSS.这个对应说明,不允许 
的序列的个数是c(2n, n-1),因此an = c(2n, n) - c(2n, n-1).[Comptes Rendus Acad.Sci.105(Paris, 1887), 436~437] 


验证: 
其中F表示前排,B表示后排,在枚举出前排的人之后,对应的就是后排的人了,然后再验证是不是满足后面的比前面对应的人高的要求.
C/C++ code
#include <iostream>
using namespace std;
int bit_cnt(int n)
{
int result = 0;
for (; n; n &= n-1, ++result);
return result;
}
int main()
{
int F[6], B[6];
int ans = 0;
for (int state = 0; state < (1 << 12); ++state) if (bit_cnt(state) == 6)
{
int i = 0, j = 0;
for (int k = 0; k < 12; ++k) if (state&(1<<k)) F[i++] = k; else B[j++] = k;
int ok = 1;
for (int k = 0; k < 6; ++k) if (B[k] < F[k]) {ok = 0; break;}
ans += ok;
}
cout << ans << endl;
return 0;
}

结果:132 
而c(12, 6)/7 = 12*11*10*9*8*7/(7*6*5*4*3*2) = 132 
注意:c(2n, n)/(n+1) = c(2n, n) - c(2n, n-1) 
估计出题的人也读过 < <计算机程序艺术>>吧. 

PS: 
另一个很YD的问题: 
有编号为1到n(n可以很大,不妨在这里假定可以达到10亿)的若干个格子,从左到右排列. 
在某些格子中有一个棋子,不妨设第xi格有棋子(1 <=i <=k, 1 <=k <=n) 
每次一个人可以把一个棋子往左移若干步, 
但是不能跨越其它棋子,也要保证每个格子至多只有一个棋子. 
两个人轮流移动,移动不了的为输,问先手是不是有必胜策略.



From: http://blog.csdn.net/nicebooks/article/details/6341638

所以自己也考虑了一个算法,也在网上看到别人的不同的算法。感觉我这个算法遍历效率很高,而且也很简洁(不敢用最来形容,怕强中更有强中手,当然如果能推导出公式来求解的话肯定会比我这个算法快,这个公式是F(n) = (n! / ((n/2)! * (n/2)!))/ (n/2 +1)).

我的算法思想是这样的:

(1)把排队的问题转换成数字排列问题,类似于0- 11这12个数排成2行。

(2)可以只考虑前一排的情况,因为只要前一排是符合某些条件,剩余的数按顺序放后排自然能满足条件。

(3)第一排数要符合的条件是:

下面数值表示的是位置,

0   1   2   3   4   5

6   7   8   9 10 11

 

对于位置0,可能的数的范围是0

对于位置1,可能的数的范围是1,2

对于位置2,可能的数的范围是2,3,4

对于位置3,可能的数的范围是3,4,5,6

对于位置4,可能的数的范围是4,5,6,7,8

对于位置5,可能的数的范围是5,6,7,8,9,10

这样可以推导出:

对于位置为n的数,其数的范围是[n, 2n].

这样对于0-5的位置的数的范围就是[{0,1,2,3,4,5}, {0,2,4,6,8,10}],

我们把这个0-5的位置的数当成一个大的数来看待,对其进行++操作,只不过进位的时候是按每个位置的数的范围来进行进位。

比如:

1.目前数是{0,1,2,3,4,5}, ++操作后,数变为{0,1,2,3,4,6},因为6小于且等于位置5的最大值2n = 10,所以此组合是个符合要求的组合。

2.又如目前数是{0,1,2,3,4,10}, ++操作后,数变为{0,1,2,3,4,11},因为11大于位置5的最大值2n = 10, 因此要进位,这样位置4的数++,再检查位置4的数++后是5,小于且等于位置4的最大值2n = 8,这个位置符合条件,而位置5的数要置"0",这个0不能是再从范围的起始值开始,而是要等于位置4的数+1.因为从起始值开始的数肯定是小于位置4的值的。

3.以此类推,如果目前数是{0,1,4,6,8,10},++操作后,需要进位,一步一步往前进位,最终进位位置1的数,数就变成了{0,2,5,7,9,11},然后要把位置1之后位置的每个位置的数置"0", 等于前一个位置的数+1,这样最后数就是{0,2,3,4,5,6}.

4.检查此数组合是否符合条件是,检查最后一个数是否小于且等于其位置的最大值。

附算法如下:

#include <iostream>

#include <vector>

 

using namespace std;

 

class pai2dui

{

public:

    pai2dui(int n)

    :mMaxNum(n / 2)

    ,mCount(0)

    ,mAllCount(0)

    {

        if (n > 1)

        {

            mCount = 1;

            for(int i = 0; i < n / 2; i++)

            {

               mData.push_back(i);

            }

        }

    }

 

    void run()

    {

        if(mMaxNum <= 0)

        {

           return;

        }

        int i = mMaxNum - 1;//从最后一个数开始++操作

        while (i)

        {

            ++mData[i];    //对位置为i的数进行++操作

            mAllCount++;   // 总循环的此数计数.

            if (mData[i] <= 2 * i)

            {// 如果该位置的数不超过对应位置的最大值,那么对其后面的位置的数置”0”.

                for (int j = i + 1; j < mMaxNum; j++)

                {//其后面位置的数等前面位置的数+1,能确保这个数是进位后”最小”符合的数的组合

                    mData[j] = mData[j - 1] + 1;

                }

            }

            else

            {// 如果该位置的数超过了对应位置的数的最大值,将数的位置往前移动一位,然后使用

              // continue重新++操作。

                i--;

                continue;

            }

           

            if (mData[mMaxNum - 1] <= 2 * (mMaxNum - 1))

            {//检查最后一位的数是否小于且等于其位置的最大值,如果是的话,此数组合符合要求

             // 计数并打印

                ++mCount;

 

                for(int k = 0; k < mMaxNum; k++)

                {

                   cout<< mData[k] << " ";

                }

                cout << endl;

                // 把++操作的数的位置置回最后一个位置。

                i = mMaxNum - 1;

            }

            else

            {  // 如果大于其位置的最大值,说明已遍历完毕,超过了此数组合的最大值。

                return;

            }

        }

    }

   

    void print()

    {

        cout<<mCount<<endl;

        cout<<mAllCount<<endl;

    }

private:

    int mMaxNum;

    vector <int> mData;

    int mCount;

    int mAllCount;

};

 

int main(int argc, char* argv[])

{

   int n;

  

   while(cin>>n)

   {

      pai2dui p(n);

      p.run();

      p.print();

   }

   return 0;



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值