Catalan数(一)

(一)Catalan数的介绍

这篇文章 还有这篇文章 讲的都不错,这里就不详谈了,只说一点,对于程序里面计算Catalan数有用的。

Catalan数的通项公式是:


事实上,在算(2n)!的时候,需要计算n!和(n+1)!,所以可以考虑在计算2n的阶乘的时候,保留必要的中间结果。

另一方面,上述公式可以化简如下:


这样,就没必要再计算(2n)的阶乘,只需要重(n+2)开始乘就可以了。下面的算法大都基于这个思想来做。

(二)出栈问题

HDJ OJ 1023 Train Problem II (问题描述见链接)。

这是个典型的出栈问题。既给定 n 个不同的数,按严格递增方式进栈,问有多少种出栈的方式。

设n个不同元素的出栈个数为:

分析:设出栈的序列编号为从1到n,则考虑第n+1个进栈的元素在出栈序列中的位置,设为i,如下图:


(CSND不能识别Visio画的图???)



该元素左边是比该元素早出栈的,右边是比它晚出栈的。

左边出栈方式的个数为:,右边出栈方式为:

则总的出栈方式数:

变换一下格式就发现者是Catalan数。


下面是我的AC的代码:

//hdj 1023 stack Catalan number
//78MS	292K	1871 B
#include <iostream>
#include <string.h>
#include <iomanip>
#define MAXN 80

using namespace std;


// 计算a*k,a是采用10000进制存储的大整数 
void multiply(int a[], int k)
{
     int *b; //b used to store a
     int m = a[0], i, j, r, carry;
     
     b = (int*)malloc(sizeof(int)*(m+1));
     for( i = 1; i<=m ; i++) b[i] = a[i];
     for( j = 1; j < k ; j++ )
     {
          for(carry = 0, i = 1; i <=m ; i++ )
          {
                    r = (i <= a[0]?a[i]+b[i]:a[i] )+carry;
                    a[i] = r%10000;
                    carry = r/10000;
          }
          if(carry)
          {
                  a[++m] = carry;         
          }   
     }
     free(b);
     a[0] = m;      
}
//计算Catalan数的分母,(n+2)*(n+3)*...*(2n)
void denominator(int a[],int n)
{
    int i = 2; 
    a[0] = a[1] = 1;   
    for( i = n+2; i <= 2*n ; i++)
    {
         multiply(a, i);     
    }  
}

void print(int a[])
{
     int index = a[0];
     while( index > 0)
     {
            if(index==a[0]) cout<<a[index];//忽略掉最高位前面的0 
            else cout<<setw(4)<<setfill('0')<<a[index];//设置位宽位4,不足的末尾补0 
            index--;
     }    
     cout<<endl;     
}


void div_frac(int a[], int n)
{
    
    int i,j, tmp = 0;
    for( j = 2 ; j <= n ; j++)
    {
         int am = a[0];
        for(i = am ; i >= 1 ; i--)
        {
              a[i] = a[i] + tmp*10000;
              tmp = a[i] % j;
              a[i] = a[i] / j ;
        }
        while( a[am] == 0 )
        {
               am--;
               a[0] = am;
        }
    }         
}

void CatalanNumber(int up[], int n)
{
        denominator(up, n);
        div_frac(up, n);    
}

int main()
{
    int n;
    int up[MAXN];   
    while(cin>>n)
    {
        memset(up, MAXN, 0);
        CatalanNumber(up, n);
        print(up);             
    }   
    return 0;  
}

(二)二叉树问题

与上题相似的是编号 1130 的问题,计算有多少棵不同的二叉搜索树。使用上述代码,可以直接AC。这里不分析了。

还有一道编号 1131 的问题,计算有多少棵树。这题首先要对 N 个不同的元素进行全排列,然后针对每一排列,其构造的树的数目恰好是Catalan数,所以总共可以构造的树的数目是n的阶乘乘以Catalan数。


(三)买票问题

题目 1133 Buy The Ticket

分析如下:

n+m个人排队买票,并且满足n \ge m,票价为50元,其中n个人各手持一张50元钞票,m个人各手持一张100元钞票,除此之外大家身上没有任何其他的钱币,并且初始时候售票窗口没有钱,问有多少种排队的情况数能够让大家都买到票。

这个题目是Catalan数的变形,不考虑人与人的差异,如果m=n的话那么就是我们初始的Catalan数问题,也就是将手持50元的人看成是+1,手持100元的人看成是-1,任前k个数值的和都非负的序列数。

这个题目区别就在于n>m的情况,此时我们仍然可以用原先的证明方法考虑,假设我们要的情况数是D_{n+m},无法让每个人都买到的情况数是U_{n + m},那么就有D_{n + m} + U_{n +m} = {n + m \choose n},此时我们求U_{n + m},我们假设最早买不到票的人编号是k,他手持的是100元并且售票处没有钱,那么将前k个人的钱从50元变成100元,从100元变成50元,这时候就有n+1个人手持50元,m-1个手持100元的,所以就得到U_{n + m} = {n + m \choose n + 1},于是我们的结果就因此得到了,表达式是D_{n + m} = {n + m \choose n} - {n + m \choose n + 1}。


参考:http://daybreakcx.is-programmer.com/posts/17315.html


事实上,这种分析方法来源于Knuth的《计算机程序设计艺术(第一卷)》的习题,2.2.1小节的习题4的答案。该书中称之为“反射原理”。(苏运霖译,第三版,508页)


为了在程序计算方便,对公式做一下变换:


AC代码:

//hdj 1133 buy the ticket
//93MS	288K	2074 B
#include <iostream>
#include <string.h>
#include <iomanip>
//注意,程序中最大的数据是200!,375位,故而需要定义为100 
#define MAXN 100

using namespace std;

// 计算a*k,a是采用10000进制存储的大整数 
void multiply(int a[], int k)
{
     int *b; //b used to store a
     int m = a[0], i, j, r, carry;
     
     b = (int*)malloc(sizeof(int)*(m+1));
     for( i = 1; i<=m ; i++) b[i] = a[i];
     for( j = 1; j < k ; j++ )
     {
          for(carry = 0, i = 1; i <=m ; i++ )
          {
                    r = (i <= a[0]?a[i]+b[i]:a[i] )+carry;
                    a[i] = r%10000;
                    carry = r/10000;
          }
          if(carry)
          {
                  a[++m] = carry;         
          }   
     }
     free(b);
     a[0] = m;      
}

void print(int a[])
{
     int index = a[0];
     while( index > 0)
     {
            if(index==a[0]) cout<<a[index];//忽略掉最高位前面的0 
            else cout<<setw(4)<<setfill('0')<<a[index];//设置位宽位4,不足的末尾补0 
            index--;
     }    
     cout<<endl;     
}
// a[]/n
void div(int a[], int n)
{
        int am = a[0];
        int tmp = 0;
        for(int i = am ; i >= 1 ; i--)
        {
              a[i] = a[i] + tmp*10000;
              tmp = a[i] % n;
              a[i] = a[i] / n ;
        }
        while( a[am] == 0 )
        {
               am--;
               a[0] = am;
        }     
     
}

//求a*n! 
void arrange(int a[], int n)
{
     if( n <= 1 ) return;
     for(int i = 2 ; i <= n ; i++)
     {
        multiply(a, i);   
     }     
     
}
int main()
{
    int m, n;
    int up[MAXN];
    int i = 1;   
    while(cin>>m>>n && (m || n))
    {
        cout<<"Test #"<<i<<":"<<endl;
        memset(up, MAXN, 0);
        if(m < n)
        {
             cout<<0<<endl;
        }
        else 
        {       
             up[0] = up[1] = 1;   
             arrange(up, m+n);
             multiply(up, m+1-n);
             div(up, m+1);
             print(up);
        }
        i++;             
    }   
    return 0;  
}


类似题目有:HDJ1267

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值