题目描述:
一列火车n节车厢,依次编号为1,2,3,…,n。每节车厢有两种运动方式,进栈与出栈,问n节车厢出栈的可能排列方式有多少种。
输入格式
输入一个整数n,代表火车的车厢数。
输出格式
输出一个整数s表示n节车厢出栈的可能排列方式数量。
数据范围
1≤n≤60000
输入样例:
3
输出样例:
5
分析:
与上一题不同的是,本题只需要输出栈混洗的合法方案个数。根据递推公式可以得出n个元素栈混洗的数目为Catalan(n)个,递推公式的推导以及卡特兰数公式的推导这里不多加赘述,栈混洗序列中,除了“312”禁形外还剩h(n)=C(2n,n)/(n+1)个合法序列。所以本题的考点在于用高精度实现卡特兰数的计算,即求(2n * (2n-1)*...*(n+1)) / n! / (n+1)。
方法一:(高精度)
直接进行高精度乘法和除法运算。为了缩小中间结果的位数,采取乘一个数立刻除一个数的形式,具体表现为:乘2n除1,乘2n-1除2,乘2n-2除3...。至于为何这样每次都能够整除,可归结为:三个连续的整数连乘必然能被3整除,依次类推到n个整数连乘可以被n整除。
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int n;
void mul(vector<int> &v,int a){
int size = v.size();
int t = 0;
for(int i = 0;i < size;i++){
v[i] = v[i] * a + t;
t = v[i] / 10;
v[i] %= 10;
}
while(t){
v.push_back(t % 10);
t /= 10;
}
}
void div(vector<int> &v,int a){
int size = v.size();
int t = 0;
for(int i = size - 1;i >= 0;i--){
v[i] += t * 10;
t = v[i] % a;
v[i] /= a;
}
while(v.size() > 0 && !v.back()) v.pop_back();
}
int main(){
cin>>n;
vector<int> ans;
ans.push_back(1);
for(int i = 2 * n,j = 1;j <= n;i--,j++){
mul(ans,i);
div(ans,j);
}
div(ans,n + 1);
for(int i = ans.size() - 1;i >= 0;i--) printf("%d",ans[i]);
return 0;
}
方法二:高精度+压位+质因数分解
本题的最大数据范围是六万,六万的阶乘是个相当大的数,使用高精度乘除法依旧无法在六秒的时间内结束程序,所以,必须采取更加高效的方法。本质的思想就是不进行除法,只做乘法,以此来减少运算次数,提高效率。我们将卡特兰数的公式分为两个部分,a / b。假设a = 2 * 2 * 3 * 5 * 5,b = 2 * 5,那么我们求a / b时只需计算2 * 3 * 5即可。根据算术基本定理,被除数和除数必然都能够被分解为若干个质数相乘,所以可以采用此法来避免除法运算。
第一部分:质因数分解消除除法运算
首先,进行素数筛法,筛出1-2n范围内所有的素数,所有被标记为true的数均不是素数。
for(int i = 2;i <= n;i++)
for(int j = 2 * i;j <= 2 * n;j += i)
prime[j] = true;
然后,求出n个数中包含某个因子的数目。比如1-8中包含因子2的个数为:能被2整除的数2,4,6,8中至少包含1个2,能被4整除的数4,8中至少包含2个2,能被8整除的数8中至少包含3个2,于是一共有4 + 2 + 1 = 7(包含几个2就被统计几次)个2。更一般的,n个数中包含因子m的个数为n / m + n / m^2 +... +n / m^3 +...。于是我们可以用以下代码实现该功能。
int get(int n,int m){
int cnt = 0;
while(n){
cnt += n / m;
n /= m;
}
return cnt;
}
再对2n和n分别调用该函数就能够求出卡特兰数中包含的各个质因子及其数目了(注意不要忘了减去n+1中的因子数)。
第二部分:高精度压位提高速度
除了直接使用方法一中高精度乘法进行运算外,还可以使用压位,将多个数压入一个向量来减少运算的次数。
特别需要注意的是,使用了压位技巧后,输出时一定要规范化向量各个元素的位数,比如压9位,除了向量末尾元素不需要进行位数限制外,其他元素一律在输出时使用%09lld进行格式控制,不足9位的前面补0,否则会导致输出的数结果不正确。
总的代码如下:
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
using namespace std;
typedef long long ll;
int n;
const ll maxn = 1e9;
bool prime[120010];
int pr[120010];
int get(int n,int m){//获取n个数中因子m的数目
int cnt = 0;
while(n){
cnt += n / m;
n /= m;
}
return cnt;
}
void mul(vector<ll> &v,int a){//高精度乘法,压位
int size = v.size();
ll t = 0;
for(int i = 0;i < size;i++){
v[i] = v[i] * a + t;
t = v[i] / maxn;
v[i] %= maxn;
}
while(t){
v.push_back(t % maxn);
t /= maxn;
}
}
int main(){
cin>>n;
vector<ll> ans;
ans.push_back(1);
memset(prime,0,sizeof(prime));
memset(pr,0,sizeof(pr));
for(int i = 2;i <= n;i++)//素数筛法
for(int j = 2 * i;j <= 2 * n;j += i)
prime[j] = true;
for(int i = 2;i <= 2 * n;i++){//消除除法
if(!prime[i]){
pr[i] = get(2 * n,i) - get(n,i) * 2;
}
}
int k = n + 1;
for(int i = 2;i <= k;i++){
while(k % i == 0){
k /= i;
pr[i]--;
}
}
for(int i = 2;i <= 2 * n;i++){
if(!prime[i]){
while(pr[i]--) mul(ans,i);
}
}
printf("%lld",ans.back());
for(int i = ans.size() - 2;i >= 0;i--){
printf("%09lld",ans[i]);
}
return 0;
}