卡特兰数--zhengjun
卡特兰数简介
卡特兰数其实不是一个数,而是一个数列。
一、了解卡特兰数
卡特兰数又称卡塔兰数,它是组合数学中一个常出现在各种计数问题中出现的数列,其前几项为 :
1
,
1
,
2
,
5
,
14
,
42
,
132
,
429
,
1430
,
4862
,
1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862,
1,1,2,5,14,42,132,429,1430,4862,
16796
,
58786
,
208012
,
.
.
.
16796, 58786, 208012,...
16796,58786,208012,...
关于卡特兰数公式有好多,先罗列几个,暂时不推导。
f ( n ) = 1 n + 1 C 2 n n f(n)=\dfrac{1}{n+1}C_{2n}^n f(n)=n+11?C2nn?
f ( n ) = ∑ i = 0 n ? 1 ( C a t a l a n i × C a t a l a n n ? i ) f(n)=\sum\limits_{i=0}^{n-1}(Catalan_i\times Catalan_n-i) f(n)=i=0∑n?1?(Catalani?×Catalann??i)
f ( n ) = C 2 n n ? C 2 n n ? 1 f(n)=C_{2n}^n-C_{2n}^{n-1} f(n)=C2nn??C2nn?1?
卡特兰数模型应用:
(1)二叉树的计数:
已知二叉树有 n 个结点,求能构成多少种不同的二叉树
(2)括号化问题:
一个合法的表达式由()包围,()可以嵌套和连接,如:(())()也是合法表达式,现给出n 对括号,求可以组成的合法表达式的个数
(3)划分问题:
将一个凸 n+2 多边形区域分成三角形区域的方法数
(4)出栈问题:
一个栈的进栈序列为 1,2,3,…n,求不同的出栈序列有多少种
(5)路径问题:
在 n*n 的方格地图中,从一个角到另外一个角,求不跨越对角线的路径数有多少种
(6)握手问题:
2n 个人均匀坐在一个圆桌边上,某个时刻所有人同时与另一个人握手,要求手之间不能交叉,求共有多少种握手方法
接下来,我们来讲讲有关卡特兰数,也就是本题的具体处理。首先,我想到的是直接用计算机模拟栈的操作。
一、直接模拟
时限般 1 秒,这题如果你直接用栈来模拟太费时!(我算到 n=50 时就 2 秒了,结果很大:
1978261657756160653623774456)
而这里 n=50,离我们预期的目标 n=60000 还有很长的路,因此,我们后面肯定还需用高精度来做数据。
#include
#include
#include
#include
using namespace std;
stack<int> sta;
vector<int> ans;
int n,num=0;
void dfs(int x) {
if(x>=n&&sta.empty()) {
num++;
for(int i=0; i<ans.size(); ++i) printf("%d",ans[i]);
printf("\n");
return;
}
if(!sta.empty()) {
int t=sta.top();
ans.push_back(t);
dfs(x);
sta.pop();
ans.pop_back();
}
if(x<=n) {
sta.push(x);
dfs(x+1);
sta.pop();
}
}
int main() {
scanf("%d",&n);
dfs(1);
printf("%d\n",num);
return 0;
}
分析 :
我们先将问题图形化,向右水平方向表示入栈,向上垂直方向表示出栈,根据题意,无论何时,入栈的次数均不少于出栈的次数,因此从 A 点到达 B 点,在绿色线内的所有蓝点都是合法方案数,而我们要找到问题解就在绿色直线红点上,这就是卡特兰数问题模型,标记的数值描述了几何图形含义。
类似问题 1:
有 2n 个人排成一行进入剧场。入场费 5 元。其中只有 n 个人有一张 5 元钞票,另外 n 人只有 10 元钞票, 剧院无其它钞票,问有多少中方法使得只要有 10 元的人买票,售票处就有 5 元的钞票找零?(将持 5 元者到达视作将 5 元入栈,持 10 元者到达视作使栈中某 5 元出栈)
类似问题 2:
一位大城市的律师在他住所以北 n 个街区和以东 n 个街区处工作,每天她走 2n 个街区去上班。如果他从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路?
回到前面问题,对于每一个数来说,必须进栈一次、出栈一次。我们把进栈设为状态‘1’,出栈设为状态‘0’。 n个数的所有状态对应 n 个 1 和 n 个 0 组成的 2n 位二进制数。由于等待入栈的操作数按照 1‥n 的顺序排列、入栈的操作数 b 大于等于出栈的操作数 a(a≤b),因此输出序列的总数目=由左而右扫描由 n 个 1 和 n 个 0 组成的2n 位二进制数,1 的累计数不小于 0 的累计数的方案种数。
下面这个图形漂亮又直观(但不是我做的,百度来的)。
二、动态规划初步
编程最容易想到的就是动态规划,当我往格子里填数的时候,还没填完动态规划思想就产生了。
动态规划在处理问题过程中速度快的很大原因是,程序在运算过程中将我们处理的状态记录下来,并在此基 础上,直接一步到位推导出下一状态。因此,动态规划解题一般都具有较佳的时间复杂度。
但对于本题而言,一开数组就发懵了(n=60000),如果将程序运行中各种状态都记录下来,数组得开
(
60000
×
60000
=
3.6
×
1
0
9
60000\times60000=3.6\times10^9
60000×60000=3.6×109),你想这么做,但计算机内存空间不允许!
下面代码只通过了 n<20 小数据,原因是卡特兰数增长速度快,炸了!因此,我们还得高精度处理!
#include
#include
#define N 610
using namespace std;
long long a[N][N];
int n=19;
int main() {
for(int i=0; i<=n; ++i) {
a[i][0]=1;
a[0][i]=0;
}
for(int j=1; j<=n; ++j) {
for(int i=j; i<=n; ++i) {
a[i][j]=a[i][j-1]+a[i-1][j];
}
}
printf("%d\n",a[n][n]);
return 0;
}
三、滚动数组优化动态规划
下面再提供的改良方案,只记录与当前状态需要的前一种状态,这里我采用滚动数组来节省空间。
#include
#include
#define N 600010
using namespace std;
long long a[2][N]= {0};
int n,t=0;
int main() {
for(int i=0; i<=n; ++i) a[t][i]=1;
for(int j=1; j<=n; ++j) {
t=1-t; //用变量 t 来实现两个数组滚动
a[t][j-1]=0; //重新前面这个位置值 0,因为这个位置曾用过有数值在
for(int i=j; i<=n; ++i) {
a[t][i]=a[1-t][i]+a[t][i-1];
printf("%lld ",a[t][i]);
}
printf("\n");
}
return 0;
}
卡特兰数的几点补充:
1.给顶节点组成二叉树的问题
给定 N 个节点,能构成多少种形状不同的二叉树?
(一定是二叉树!先取一个点作为顶点,然后左边依次可以取 0 至 N-1 个相对应的,右边是 N-1 到 0 个,两两配对相乘,就是 h(0)*h(n-1) + h(2)*h(n-2) + + h(n-1)h(0)=h(n)) (能构成 h(N)个)
在 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)=1/(n+1)*c(2n,n)。
(这个公式的下标是从 h(0)=1 开始的)
2.图形化卡特兰数证明 2(折线法)
和我前面给的图像证明如出一辙,下面这个处理同样非常有意思。
事实上,可以认为问题是,任意两种操作,要求每种操作的总次数一样,且进行第 k 次操作 2 前必须先进行至少 k 次操作 1。我们假设一个人在原点,操作 1 是此人沿右上角 45°走一个单位(一个单位设为根号 2,这样他第一次进行操作 1 就刚好走到(1,1)点),操作 2 是此人沿右下角 45°走一个单位。
第 k 次操作 2 前必须先进行至少 k 次操作 1,就是说明所走出来的折线不能跨越 x 轴走到 y=-1 这条线上! 在进行 n 次操作 1 和 n 此操作 2 后,此人必将到到达(2n,0)!若无跨越 x 轴的限制,折线的种数将为 C(2n,
n),即在 2n 次操作中选出 n 次作为操作 1 的方法数。
现在只要减去跨越了 x 轴的情况数。对于任意跨越 x 轴的情况,必有将与 y=-1 相交。找出第一个与 y=-1 相交的点 k,将k 点以右的折线根据 y=-1 对称(即操作 1 与操作 2 互换了)。可以发现终点最终都会从(2n,0)对称到(2n,-2)。由于对称总是能进行的,且是可逆的。
我们可以得出所有跨越了 x 轴的折线总数是与从(0,0)到
(2n,-2)的折线总数。
而后者的操作 2 比操作 1 要多 0-(-2)=2 次。即操作 1 为
n-1,操作 2 为 n+1。总数为 C(2n,n-1)。
以上就是卡特兰数的内容
若有错误,可以评论