这是算法进阶指南上面的题目,然后这道题需要用到卡特兰数,具体什么是卡特兰数下面是百度得来的:
出栈次序
一个栈(无穷大)的进栈序列为1,2,3,…,n,有多少个不同的出栈序列? [5-6]
常规分析
首先,我们设f(n)=序列个数为n的出栈序列种数。(我们假定,最后出栈的元素为k,显然,k取不同值时的情况是相互独立的,也就是求出每种k最后出栈的情况数后可用加法原则,由于k最后出栈,因此,在k入栈之前,比k小的值均出栈,此处情况有f(k-1)种,而之后比k大的值入栈,且都在k之前出栈,因此有f(n-k)种方式,由于比k小和比k大的值入栈出栈情况是相互独立的,此处可用乘法原则,f(n-k)*f(k-1)种,求和便是Catalan递归式。ps.author.陶百百)
首次出空之前第一个出栈的序数k将1~n的序列分成两个序列,其中一个是1~k-1,序列个数为k-1,另外一个是k+1~n,序列个数是n-k。
此时,我们若把k视为确定一个序数,那么根据乘法原理,f(n)的问题就等价于——序列个数为k-1的出栈序列种数乘以序列个数为n - k的出栈序列种数,即选择k这个序数的f(n)=f(k-1)×f(n-k)。而k可以选1到n,所以再根据加法原理,将k取不同值的序列种数相加,得到的总序列种数为:f(n)=f(0)f(n-1)+f(1)f(n-2)+……+f(n-1)f(0)。
看到此处,再看看卡特兰数的递推式,答案不言而喻,即为f(n)=h(n)= C(2n,n)/(n+1)= c(2n,n)-c(2n,n-1)(n=0,1,2,……)。
最后,令f(0)=1,f(1)=1。
非常规分析
对于每一个数来说,必须进栈一次、出栈一次。我们把进栈设为状态‘1’,出栈设为状态‘0’。n个数的所有状态对应n个1和n个0组成的2n位二进制数。由于等待入栈的操作数按照1‥n的顺序排列、入栈的操作数b大于等于出栈的操作数a(a≤b),因此输出序列的总数目=由左而右扫描由n个1和n个0组成的2n位二进制数,1的累计数不小于0的累计数的方案种数。
在2n位二进制数中填入n个1的方案数为c(2n,n),不填1的其余n位自动填0。从中减去不符合要求(由左而右扫描,0的累计数大于1的累计数)的方案数即为所求。
不符合要求的数的特征是由左而右扫描时,必然在某一奇数位2m+1位上首先出现m+1个0的累计数和m个1的累计数,此后的2(n-m)-1位上有n-m个 1和n-m-1个0。如若把后面这2(n-m)-1位上的0和1互换,使之成为n-m个0和n-m-1个1,结果得1个由n+1个0和n-1个1组成的2n位数,即一个不合要求的数对应于一个由n+1个0和n-1个1组成的排列。
反过来,任何一个由n+1个0和n-1个1组成的2n位二进制数,由于0的个数多2个,2n为偶数,故必在某一个奇数位上出现0的累计数超过1的累计数。同样在后面部分0和1互换,使之成为由n个0和n个1组成的2n位数,即n+1个0和n-1个1组成的2n位数必对应一个不符合要求的数。
因而不合要求的2n位数与n+1个0,n-1个1组成的排列一一对应。
显然,不符合要求的方案数为c(2n,n+1)。由此得出输出序列的总数目=c(2n,n)-c(2n,n-1)=c(2n,n)/(n+1)=h(n)。
然后这道题就转变成来求c(2n,n)/(n+1)的组合数列了,再看一下题意的范围达到60000所以第一直觉就是要进行大数乘法,
如果进行暴力大数乘法,时间可能会超,这时就可以来用素筛法,将质因数分解开来,分解成几个素数相乘,极大的减少了时间。
下面是我的代码,具体的解释会在代码中标注:
#include
#include
#include
using namespace std;
typedef long long LL;
const int N = 60010;
const int M = 120010;
LL res[N], tt ;
int q[M];
bool st[M];
void multi(int b)
{
LL t = 0;
for(int i = 0; i <= tt; i ++ )
{
res[i] = res[i]*b + t;
t = res[i] / 1000000000;//这个地方进行压位处理,把九个数字压在LL数组中的一位来进行
res[i] %= 1000000000;//大数乘法的基本模板
}
while (t)
{
res[++ tt] = t % 1000000000;
t /= 1000000000;
}
}
void out()
{
printf("%lld",res[tt]);//去掉前导0
for (int i = tt - 1; i >= 0; i -- )
printf("%09lld" , res[i]);
cout << endl;
}
int get(int n,int p)//来求质数因子出现的次数
{
int s = 0;
while(n)
{
s += n/p;
n /= p;
}
return s;
}
int main()
{
int n ;
cin >> n;
for(int i = 2; i <= 2*n; i ++ )
for(int j = (i << 1); j <= 2*n; j += i )
st[j] = true;
for(int i = 2; i <= 2*n; i ++ )
if(!st[i])
{
q[i] = get( n*2 , i ) - get( n * 2 - n , i ) * 2;
}
int k = n + 1;
for(int i = 2; i <= k; i ++ )
while( k % i == 0)
{
k /= i;
q[i] -- ;
}
res[0] = 1;
for(int i = 2; i <= n * 2; i ++ )
while(q[i] -- )
multi(i);
out();
return 0;
}
其中在质因子分解的地方要做一个解释:
求n的阶乘某个因子a的个数,如果n比较小,可以直接算出来,但是如果n很大,此时n!超出了数据的表示范围,这种直接求的方法肯定行不通。其实n!可以表示成统一的方式。
n!=(k^m)*(m!)*a 其中k是该因子,m=n/k,a是不含因子k的数的乘积
下面推导这个公式
n!=n*(n-1)*(n-2)*......3*2*1
=(k*2k*3k.....*mk)*a a是不含因子k的数的乘积,显然m=n/k;
=(k^m)*(1*2*3...*m)*a
=k^m*m!*a
接下来按照相同的方法可以求出m!中含有因子k的个数。
因此就可以求除n!中因子k的个数
可以将这个当作求N的阶乘的含有某个元素p的个数,其中数学推导也非常清楚。
int get(int n,int k)
{
int num=0;
while(n)
{
num+=n/p;
n/=p;
}
return num;
}
谢谢!