学习卡特兰数的一点心得(组合数学/知识总结+例题总结+板子整理)

思路来源

https://baike.baidu.com/item/%E5%8D%A1%E7%89%B9%E5%85%B0%E6%95%B0/6125746?fr=aladdin

https://blog.youkuaiyun.com/duanruibupt/article/details/6869431

https://www.cnblogs.com/COLIN-LIGHTNING/p/8450053.html

https://blog.youkuaiyun.com/qq_36523667/article/details/78590002

https://blog.youkuaiyun.com/doc_sgl/article/details/8880468

https://www.luogu.com.cn/problemnew/solution/P1044

https://blog.youkuaiyun.com/qq_37025443/article/details/79952573(折线法的证明)

例题

在讲解正文之前,让我们一起来看两道例题吧!

 

花花放羽毛球

花花有若干种同样大小不同颜色的羽毛球,一种颜色一个,一个足够长的羽毛球筒,

放在底下的羽毛球被上面的羽毛球压住之后,只能在上面的取出之后,才能被取出来。

妈妈把没放入筒之前的羽毛球的顺序给花花排好,

并告诉花花,羽毛球得先被放在筒里,从筒里取出之后,才能让你玩。

现在,花花想拿出所有的羽毛球玩,她不情愿地把羽毛球一个一个塞到筒里,

这时妈妈问花花,取出羽毛球的顺序共有多少种可能呢?

 

比如,红、黄两种颜色的羽毛球,

可以先放入红色,取出红色,放入黄色,取出黄色,取出顺序为红黄

也可以先放入红色,再放入黄色,取出黄色,再取出红色,取出顺序为黄红

 

看着妈妈手里的红橙黄绿青蓝紫灰粉黑白棕色的羽毛球,花花不禁陷入了沉思

所以,花花想向你求助。

 

牛牛排座位

牛牛是幼儿园大班的小锦囊,平时老师问他一些数学问题,他都对答如流。

这一天,老师让他排座位。

在他所在的幼儿园大班里,共有12位身高不同的小朋友。

他们要排成两排,每排6人,大家坐在一起看动画片。

第一排的小朋友,不能挡住第二排小朋友,所以后排的小朋友身高要更高。

牛牛脱口而出:我身高最矮,坐在第一排,比我略高的明明坐在第二排,比明明略高的天天坐在第一排……

老师:真是什么都难不倒你呀,那我要是问,有多少种可行的排座方案呢。

“十五种,二十七种,四十八种……”牛牛很快就数乱了。

动画片已经放一半了,你是牛牛的好朋友,快帮帮他吧。

 

正文部分

先讲一下第一题的答案,至于第二题的答案,或许就在文中呢。

上述的羽毛球筒,就是数据结构中栈的粗略概念啦,是一个先入后出的结构。

我们不妨把n种羽毛球分成两堆来处理,对于每种颜色标序号,1,2,...,n

考虑颜色x是最后一个被拿出的,则前面的x-1个先放入再拿出(否则就会被压在底下拿不出),

后面的n-x个在放入x之后先放入再拿出,最后拿出颜色x,

对应的方案数为h(x-1)*h(n-x),考虑到x可以取[1,n]的整数,

即得h(0)=h(1)=1h(n)= h(0)*h(n-1)+h(1)*h(n-2) + ... + h(n-1)*h(0) (n\geqslant 2)

那它怎么求呢?

是的,它就是接下来要讲的,卡特兰数。

 

卡特兰数

①递推公式

h(0)=1h(1)=1

Catalan数h(n)

满足递推式:

h(n)= h(0)*h(n-1)+h(1)*h(n-2) + ... + h(n-1)*h(0) (n\geqslant 2)

也满足递推式:

h(n)=h(n-1)*\frac{4n-2}{n+1},可用于大数下的卡特兰数计算

(n-3)h_{n}=\frac{n}{2}(h_{3}*h_{n-1}+h_{4}*h_{n-2}+...+h_{n-2}*h_{4}+h_{n-1}*h_{3})

 

②通项公式

h(n)=\frac{C_{2n}^{n}}{n+1} (n=0,1,2,...)

h(n)=C_{2n}^{n}-C_{2n}^{n-1}(n=0,1,2,...)

 

③应用

在接下来的应用中,你可能会看到刚才看到的例题的影子。

是的,它们无一例外,全部是卡特兰数。

 

①长度为2n项的序列a_{1},a_{2},...,a_{2n},其中有n个+1和n个-1

对于任意0\leq k\leq 2n,满足前k项和a_{1}+a_{2}+...+a_{k}\geq 0的序列的个数等于第n个Catalan数h_{n}

证明:

假设不满足条件的序列个数为u_{n},那么就有h_{n}+u_{n}=C_{2n}^{n}

在不满足条件的某个序列中,设k是最小的值,满足a_{1}+a_{2}+...+a_{k}<0

由于这里k是最小的,所以必有a_{1}+a_{2}+...+a_{k-1}=0a_{k}=-1且k是一个奇数。

这表明前k项中,-1比+1多且恰好多一个,

那我们将前k项中的+1变为-1,将-1变为+1,后面的项保持不变,

就得到了一个有着n+1个+1和n-1个-1的,长度为2n的序列了。

这样的序列个数就是我们要求的u_{n}

显然,u_{n}分为两部分,

先出现-1的序列,其前缀和小于0,将序列最末一个+1改为-1,即符合题意;

先出现+1的序列,按上述原则翻转回去,则符合原题意。

故只需在2n个数里任取n+1个数为1即可,数值大小为 u_{n}=C_{2n}^{n+1}

那么我们就得到了h_{n}=C_{2n}^{n}-u_{n}=C_{2n}^{n}-C_{2n}^{n+1},就是我们前面的公式。

 

合法的出栈序列,可以理解成n个入栈,n个出栈,任意时刻入栈个数>=出栈个数,则为卡特兰数

 

(重要)

推导过程参考自:https://blog.youkuaiyun.com/qq_37025443/article/details/79952573

 

推广A:

hdu3398 String

n个+1和m个-1的情形,使任意前缀1的个数不少于-1的个数,求方案数,答案为C_{n+m}^{n}-C_{n+m}^{n+1}

该公式的推导过程,详见思路来源

推广B:

2018年长沙理工大学第十三届程序设计竞赛 J.杯子

n个数(1,2,...,n)入栈,可以一边入栈一边出栈,最后栈中剩k个没出栈,求不同的方案数(n>=k)

n个入栈,n-k个出栈,任意时刻入栈>=出栈,相当于到从(0,0)到(2n-k,k),不过y=-1的方案数,

该点经y=-1可对称到(2n-k,-k-2),

从(0,0)到(2n-k,k)的方案数,选n个入栈,C(2n-k,n)

从(0,0)到(2n-k,-k-2)的方案数,与从(0,0)到(2n-k,k+2)的方案数相等,选n+1个入栈,n-k-1个出栈,C(2n-k,n+1)

即答案为C_{2*n-k}^{n}-C_{2*n-k}^{n+1},方案数一一对应了栈中现有k个数都出栈所构成的序列,

至于比n个数的卡特兰数方案数少,是因为后者可能存在出栈序列,使得出入栈过程中,栈中的元素数始终<k

 

③若将+1改为左括号,-1改为右括号,则原问题变为括号的匹配问题。

括号的合法序列是指,对于任意长度的前缀,左括号数都不少于右括号数,

去统计合法的括号序列的方案数,不难发现,也是卡特兰数。

证明:

(1)空串是合法序列

(2)若A是合法序列且B是合法序列,则AB是合法序列

(3)若S为合法序列,且a为长度为k的左括号,b为长度为k的右括号,则aSb是合法序列

规定A是一个合法序列,B也是一个合法序列。

那么对于n对的括号序列,一定是A(B)的形式构成。

这里我们分别讨论假设,

B有0对括号,则A有(n-1)对,

B有1对,则B有(n-2)对,

……

B有(n-1)对,则A有0对。

f(n)=f(0)*f(n-1)+...+f(n-1)*f(0)

根据卡特兰数的定义,显然是卡特兰数h_{n}

 

④求01串的个数:

n个0与n个1构成的序列方案数,使得任何一个前缀0的个数不少于1的个数。

把0改为左括号,1改为右括号,就变成了合法括号序列匹配问题。

 

我是彩蛋我是彩蛋我是彩蛋~

到这里,我们也不难发现,上文提到的牛牛排座位的问题,也可用该思想解决。

把12位小朋友的身高按从小到大顺序依次排列,并用长为十二的01串来代表具体的选择情况。

用0来代表该小朋友选择了第一排,用1来代表选择了第二排。

让我们模拟排座的具体过程,一个合法的排座方案,

对于排座的任意一个时刻,第一排的人数不少于第二排,

且最终时刻,第一排的人数等于第二排。

那与上述01串的方案数有什么区别呢?这便又是卡特兰数了。

 

⑤n个节点的二叉树,所有可能形态数为卡特兰数h_{n}

我们考虑随便取一个节点作为根,那么他左边和右边的儿子节点个数就确定了。

假定根节点标号为x,

那么左子树的标号就从1到x-1,共x-1个,右子树的标号就从x+1到n,共n-x个。

那么我们的x从1取到n,就获得了所有的情况数h_{n}=\sum_{i=0}^{n-1}h_{i}*h_{n-i-1}

 

⑥求解合法路径方案数:

对于一个n*n的正方形网格,每次我们能向右或者向上移动一格,

那么从左下角到右上角的所有在副对角线右下方的路径总数为h_{n}

我们记录移动路径,将走了一条水平边记为+1,走了一条垂直边记为-1,

则一条路径对应一个由n个+1和n个-1的组成的长度为2n的序列,

我们所要保证的是,前k步中水平边的个数不小于垂直边的个数,

换句话说,前k个元素的和非负,即上述卡特兰数的定义。

 

⑦求凸边形进行三角剖分的不同方案数:

(上图对应六边形的所有三角剖分方案)

即求在一个有n+3条边的凸多边形中,求通过若干条互不相交的对角线,把这个多边形划分成若干个三角形的不同方案数。

因为每一条边都一定是剖分后的三角形的一条边,任意一条边都会把多边形分成两个小多边形,

那么根据乘法原理,解即为划分成不同多边形的方案数对应小多边形的划分方案数之和

 

④总结

卡特兰数用于求方案数,笔者认为其具有以下特征,

①可以以某位置为划分,将原情况划分为两个更小的子情况。

②类似前缀和非负的操作,如左括号数不少于右括号数等。

 

⑤代码实现

以下大数代码,可用于求卡特兰数的第n+1(n<6000)项的值

#include <iostream>
#include <algorithm> 
#include <string>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <set>
#include <map>
#include <vector>
#include <stack>
#include <queue>
#include <bitset> 
const int INF=0x3f3f3f3f;
const int MOD=1e9+7;
const double eps=1e-7;
typedef long long ll;
#define vi vector<int> 
#define si set<int>
#define pii pair<int,int> 
#define pi acos(-1.0)
#define pb push_back
#define mp make_pair
#define lowbit(x) (x&(-x))
#define sci(x) scanf("%d",&(x))
#define scll(x) scanf("%lld",&(x))
#define sclf(x) scanf("%lf",&(x))
#define pri(x) printf("%d",(x))
#define rep(i,j,k) for(int i=j;i<=k;++i)
#define per(i,j,k) for(int i=j;i>=k;--i)
#define mem(a,b) memset(a,b,sizeof(a)) 
using namespace std;
const int maxn=1500;
struct BigInt
{
    const static int mod=10000;
    const static int LEN=4;
    int a[maxn],len;
    BigInt()
    {
        memset(a,0,sizeof(a));
        len=1;
    }
    void init(int x)
    {
        memset(a,0,sizeof(a));
        len=0;
        do//四位一存 如123456789存为 6789 2345 1 
        {
            a[len++]=x%mod;
            x/=mod;
        }while(x);
    }
    void Init(const char s[])//重点 123 4567
    {
        memset(a,0,sizeof(a));
        int l=strlen(s),res=0;
        len=l/LEN;
        if(l%LEN)len++;//l/LEN向上取整 len=2 表明需要两个字节能存下 
        for(int i=l-1;i>=0;i-=LEN)
        {
            int t=0,k=max(i-LEN+1,0);//k是找到当前字节的最高位 回退len-1长 防越界 
            for(int j=k;j<=i;j++)t=t*10+s[j]-'0';//从4开始 t临时存储4567 
            a[res++]=t;//将低位存入 
        }
    } 
    int Compare(const BigInt &b)//位多的大 
    {
        if(len<b.len)return -1;
        if(len>b.len)return 1;
        for(int i=len-1;i>=0;i--)//从高位比较 
            if(a[i]<b.a[i])return -1;
            else if(a[i]>b.a[i])return 1;
        return 0;//完全相等的情况 
    }
    BigInt operator +(const BigInt &b)const
    {
        BigInt ans;
        ans.len=max(len,b.len);
        for(int i=0;i<=ans.len;i++)ans.a[i]=0;
        for(int i=0;i<ans.len;i++)
        {
            ans.a[i]+=((i<len)?a[i]:0)+((i<b.len)?b.a[i]:0);//防止越位现象 
            ans.a[i+1]+=ans.a[i]/mod;//向高位进位 
            ans.a[i]%=mod;
        }
        if(ans.a[ans.len]>0)ans.len++;//产生了9999+9999=9998 1的数组高位进位 
        return ans;
    }
    BigInt operator -(const BigInt &b)const//确保被减数大 差为正 
    {
        BigInt ans;
        ans.len=len;
        int k=0;
        for(int i=0;i<ans.len;i++)
        {
            ans.a[i]=a[i]+k-b.a[i];
            if(ans.a[i]<0)ans.a[i]+=mod,k=-1;//向a[i]高位借10000,k=-1下轮生效 
            else k=0;          
        }
        while(ans.a[ans.len-1]==0&&ans.len>1)ans.len--;//把前缀0去掉 如果ans.len=1时说明a=b差为0 
        return ans;
    }
    BigInt operator *(const BigInt &b)const
    {
        BigInt ans;
        for(int i=0;i<len;i++)
        {
            int k=0;
            for(int j=0;j<b.len;j++)
            {
                int temp=a[i]*b.a[j]+ans.a[i+j]+k;//模拟竖式乘法 i*j加到i+j上去 k为低位来的进位 
                ans.a[i+j]=temp%mod;
                k=temp/mod;//k为向高位进的位 下一轮生效 
            }
            if(k!=0)ans.a[i+b.len]=k;//高位进位 99*99=9801 右起第1位*右起第1位还是能到右起第3位的 
        }
        ans.len=len+b.len;//4位数*4位数不会超过8位数  
        while(ans.a[ans.len-1]==0&&ans.len>1)ans.len--;//查出实际长度 
        return ans;
    }
    BigInt operator /(const int &n)const//被确保被除数大 商为正 
    {
        BigInt ans;
        ans.len=len;
        int k=0;
        for(int i=ans.len-1;i>=0;i--)
        {
            k=k*mod+a[i];//k=上一位来的退位*10000+这一位 
            ans.a[i]=k/n;//这一位除以n 
            k=k%n;//这一位除以n的余数送给下一位,i=0最后一位如57/28余的1直接丢掉 取整  
        }
        while(ans.a[ans.len-1]==0&&ans.len>1)ans.len--;
        return ans;
    }
    void output()
    {
        printf("%d",a[len-1]);//是多少就是多少 没有前缀0 
        for(int i=len-2;i>=0;i--) 
            printf("%04d",a[i]);//包含前缀0 如0001 
        printf("\n");
    }
};
BigInt a[6000];
int main()
{
	int n;
	scanf("%d",&n);
	a[0].init(1),a[1].init(1);
	rep(k,2,n+1)
	{
		int t=4*k-2;
		BigInt temp1,temp2;
		temp1.init(t); 
		a[k]=a[k-1]*temp1;
	    temp2=a[k]/(k+1);
	    a[k]=temp2;
	    //a[k].output();
    }
	a[n+1].output();
    return 0;
}

代码后续

①h(0)=h(1)=1,h(n)= h(0)*h(n-1)+h(1)*h(n-2) + ... + h(n-1)*h(0) (n>=2),O(n^2)且多次计算乘积,不常用

②h(n)=h(n-1)*(4*n-2)/(n+1),因为有除法,取模时难以保证逆元存在,O(n),不常用

③h(n)=C(2n,n)/(n+1) (n=0,1,2,...),同上,高精度用一用还可,取模难以保证逆元,O(n), 不常用

④h(n)=c(2n,n)-c(2n,n-1) (n=0,1,2,...),减法可以用来取模,预处理组合数,O(n)比较常用

⑤f[i][j]表示当前栈内有i个左括号,j个右括号的方案数(i>=j),分j==i和j<i两种情况转移,O(n^2),空间O(n^2)可以用

	c[0][0]=1;
	for(int i=1;i<=n;++i){
		c[i][0]=1;
		for(int j=1;j<=i;++j){
			if(i==j)c[i][j]=c[i][j-1];
			else c[i][j]=c[i-1][j]+c[i][j-1];
		}
	}
	printf("%d\n",c[n][n]);

⑤f[i][j]表示当前未操作元素还有i个,栈内元有j个的方案数,记忆化搜索,O(n^2),空间O(n^2)可以用

#include<iostream>
using namespace std;
typedef long long ll;
ll n,f[20][20];
ll dfs(int x,int y){
    if(f[x][y])return f[x][y];
    if(x==0)return 1;
    if(y)f[x][y]+=dfs(x,y-1);
    f[x][y]+=dfs(x-1,y+1);
    return f[x][y];
}
int main(){
	scanf("%lld",&n);
    printf("%lld\n",dfs(n,0));
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小衣同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值