(一)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数。
(三)买票问题
分析如下:
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