思路来源
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,
对应的方案数为,考虑到x可以取[1,n]的整数,
即得,
那它怎么求呢?
是的,它就是接下来要讲的,卡特兰数。
卡特兰数
①递推公式
令,
,
Catalan数
满足递推式:
也满足递推式:
,可用于大数下的卡特兰数计算
②通项公式
③应用
在接下来的应用中,你可能会看到刚才看到的例题的影子。
是的,它们无一例外,全部是卡特兰数。
①长度为2n项的序列,其中有n个+1和n个-1
对于任意,满足前k项和
的序列的个数等于第n个Catalan数
。
证明:
假设不满足条件的序列个数为,那么就有
。
在不满足条件的某个序列中,设k是最小的值,满足。
由于这里k是最小的,所以必有且
且k是一个奇数。
这表明前k项中,-1比+1多且恰好多一个,
那我们将前k项中的+1变为-1,将-1变为+1,后面的项保持不变,
就得到了一个有着n+1个+1和n-1个-1的,长度为2n的序列了。
这样的序列个数就是我们要求的。
显然,分为两部分,
先出现-1的序列,其前缀和小于0,将序列最末一个+1改为-1,即符合题意;
先出现+1的序列,按上述原则翻转回去,则符合原题意。
故只需在2n个数里任取n+1个数为1即可,数值大小为 。
那么我们就得到了,就是我们前面的公式。
合法的出栈序列,可以理解成n个入栈,n个出栈,任意时刻入栈个数>=出栈个数,则为卡特兰数
②(重要)
推导过程参考自:https://blog.youkuaiyun.com/qq_37025443/article/details/79952573
推广A:
n个+1和m个-1的情形,使任意前缀1的个数不少于-1的个数,求方案数,答案为
该公式的推导过程,详见思路来源
推广B:
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)
即答案为,方案数一一对应了栈中现有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对。
故
根据卡特兰数的定义,显然是卡特兰数。
④求01串的个数:
n个0与n个1构成的序列方案数,使得任何一个前缀0的个数不少于1的个数。
把0改为左括号,1改为右括号,就变成了合法括号序列匹配问题。
我是彩蛋我是彩蛋我是彩蛋~
到这里,我们也不难发现,上文提到的牛牛排座位的问题,也可用该思想解决。
把12位小朋友的身高按从小到大顺序依次排列,并用长为十二的01串来代表具体的选择情况。
用0来代表该小朋友选择了第一排,用1来代表选择了第二排。
让我们模拟排座的具体过程,一个合法的排座方案,
对于排座的任意一个时刻,第一排的人数不少于第二排,
且最终时刻,第一排的人数等于第二排。
那与上述01串的方案数有什么区别呢?这便又是卡特兰数了。
⑤n个节点的二叉树,所有可能形态数为卡特兰数。
我们考虑随便取一个节点作为根,那么他左边和右边的儿子节点个数就确定了。
假定根节点标号为x,
那么左子树的标号就从1到x-1,共x-1个,右子树的标号就从x+1到n,共n-x个。
那么我们的x从1取到n,就获得了所有的情况数。
⑥求解合法路径方案数:
对于一个n*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),且多次计算乘积,不常用
②h(n)=h(n-1)*(4*n-2)/(n+1),因为有除法,取模时难以保证逆元存在,,不常用
③h(n)=C(2n,n)/(n+1) (n=0,1,2,...),同上,高精度用一用还可,取模难以保证逆元,, 不常用
④h(n)=c(2n,n)-c(2n,n-1) (n=0,1,2,...),减法可以用来取模,预处理组合数,,比较常用
⑤f[i][j]表示当前栈内有i个左括号,j个右括号的方案数(i>=j),分j==i和j<i两种情况转移,,空间
时可以用
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个的方案数,记忆化搜索,,空间
时可以用
#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;
}