卡特兰数
卡特兰数
又称明安图数。Catalan 是国外命名的,而最先发现的人应该是清代数学家明安图。
问题 1:
给定 \(n\) 个 \(+1\) 和 \(m\) 个 \(-1\),求将其组成排列满足 \(\forall 1\leq i\leq n+m,\sum\limits_{1\leq j\leq i}a_j\ge -k\) 的方案数。
对于 \(n-m<-k\) 的情况,符合条件的排列数 \(0\),接下来考虑 \(n-m\ge -k\) 的情况:
排列总个数为 \({n+m\choose n}\),减去不符合条件的数量即可。
考虑排列 \(A\),它不符合条件,即 \(\exist 1\leq i< n+m,\sum\limits_{1\leq j\leq i}a_j<-k\),我们找到最小的 \(i\),显然它满足 \(\sum\limits_{1\leq j\leq i}a_j=-(k+1)\),也就是说,前 \(i\) 个数中 \(1\) 的个数比 \(-1\) 的个数少 \(k+1\) 个。
我们将排列 \(A\) 的前 \(i\) 项取相反数,得到序列 \(B\)。可以发现序列 \(B\) 满足前 \(i\) 个数中 \(1\) 的个数比 \(-1\) 的个数多 \(k+1\) 个,即:有 \(n+k+1\) 个 \(1\),有 \(m-k-1\) 个 \(-1\)。
对于排列 \(A\),有唯一的序列 \(B\) 与之对应;对于序列 \(B\),有唯一的排列 \(A\) 与之对应。
证明可以考虑排列 \(A\) 第一个前缀和小于 \(-k\) 的位置,以及序列 \(B\) 第一个前缀和大于 \(k\) 的位置唯一,即在规定的变换方式下,两者有唯一的对应关系。
排列 \(A\) 的个数即为序列 \(B\) 的个数,那么序列 \(B\) 除了有 \(n+k+1\) 个 \(1\),有 \(m-k-1\) 个 \(-1\) 以外,还是否受到其他限制呢?即是否对于每一个不同的序列 \(B\),都有不同的排列 \(A\) 与之对应?这是显然的,上文已经证过,序列 \(B\) 必然满足 \(\exist 1\leq i\leq n+m,\sum\limits_{1\leq j\leq i}B_j>k\),所以每一个序列 \(B\) 都可以派上用场。
综上,序列 \(B\) 的个数为 \({n+m\choose n+k+1}\),则符合条件的方案数为:
问题 2:
\(n\) 个元素有多少种出栈顺序。
入栈看成 \(+1\),出栈看成 \(-1\),取 \(k=0\),即可计算出答案:
问题 3:
\(n\) 对括号能组成多少个有效的括号序列。
一个括号序列有效的条件是它的所有前缀中,左括号数不少于右括号数。
将左括号看成 \(+1\),右括号看成 \(-1\),取 \(k=0\),即可计算出答案:
问题 4:
\(n+1\) 个节点能构成多少个形状不同的满二叉树。
这里的满二叉树指的是对于任意一个节点,都满足为叶子节点或是有两个儿子。
可以发现,这样的满二叉树作为左儿子和作为右儿子的节点数目是一样的。
考虑一棵树的 dfs
序,将左儿子看成 \(+1\),右儿子看成 \(-1\),遍历的过程中累加而不小于 \(0\),记为一棵合法的满二叉树。
根节点不用考虑,略过。
所以答案还是:
问题 5:
上点难度。
有 \(n\) 个人排队买电影票,分为三种类型:有一张 \(50\) 元的,有一张 \(100\) 元的,还有无需付钱的 VIP 客户。起初电影院没有零钱,求有多少种排队的方法,使得每个人都买到电影票后,工作人员手上的 \(50\) 元的张数在 \(l\sim r\) 之间,结果对 \(p\) 取模。只要两种方案里的其中一人的类别不同,就代表两种方案不同。
考虑先排除 VIP 客户的影响,枚举 VIP 客户的个数,对于 \(i\) 个 VIP 客户,贡献为 \({n \choose i}\)。
枚举有 \(50\) 元人的个数 \(j\),则有 \(100\) 元人的个数为 \(n-i-j\),可以发现 \(j\) 满足:\(l\leq j-(n-i-j)\leq r\),意为 \(50\) 元最终的张数范围。
解得:\(\lceil\frac{l+n-i}{2}\rceil\leq j\leq \lfloor\frac{r+n-i}{2}\rfloor\),又由于对于每个 \(100\) 元的付款,都要找钱 \(50\),所以对于任意前缀都满足有 \(50\) 元的人的个数大于等于有 \(100\) 元的人的个数。
可得答案计算式:
发现可以消掉其中大部分,化简为:
做到这里,有关卡特兰数的部分就结束了,但是题目还没有解决。
题目给出的模数非质数,也不保证互质,不能使用费马小定理或扩展欧几里得求逆元。
这时候一种方法是使用 \(\rm{exLucas}\),另一种方法是从 \(p\) 入手:
发现 \(p\leq 2\times 10^9\) 最多有 \(9\) 个不同的质因子,我们将 \(p\) 分解质因数。
在预处理组合数的过程中,我们需要用到乘法和除法,不妨将 \(fac[i]=i!\) 表示成有关 \(p\) 的质因子的形式:
可以发现,此时 \(\gcd(p_0,p)=1\),可以使用扩展欧几里得算法求逆元。
对于每一个阶乘,我们记录 \(p_0\) 以及 \(\{\beta_i\}\)。重载乘法和除法,关于乘法,我们将两个数的 \(\{\beta_i\}\) 相加,\(p_0\) 相乘并对 \(p\) 取模;关于除法,我们将两个数的 \(\{\beta_i\}\) 相减,\(p_0\) 求逆元相乘并对 \(p\) 取模,最后将答案还原即可。
注意开 long long
,代码如下(个人感觉还是较为简洁明了的)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){
if(c=='-')f=-1;
c=getchar();
}
while(c<='9'&&c>='0'){
x=(x<<1)+(x<<3)+(c^48);
c=getchar();
}
return x*f;
}
void print(int x){
if(x<0)putchar('-'),x=-x;
if(x>9)print(x/10);
putchar(x%10^48);
}
const int N=1e5+5;
int mod,num,p[10],a[10];
struct node{
int p0=1,a0[10]={0};
}fac[N],invfac[N];
int calc(node x){
int ans=x.p0;
for(int i=1;i<=num;++i)while(x.a0[i]--)ans=ans*p[i]%mod;
return ans;
}
node work(int x){
node ans;
for(int i=1;i<=num;++i)while(x%p[i]==0)x/=p[i],ans.a0[i]++;
ans.p0=x;
return ans;
}
void exgcd(int a,int b,int &x,int &y){
if(!b){x=1,y=0;return;}
int x0,y0;
exgcd(b,a%b,x0,y0);
x=y0,y=x0-(a/b)*y0;
}
node mul(node x,node y){
node ans;
ans.p0=x.p0*y.p0%mod;
for(int i=1;i<=num;++i)ans.a0[i]=x.a0[i]+y.a0[i];
return ans;
}
node div(node x,node y){
node ans;
int X,Y;
exgcd(y.p0,mod,X,Y);
ans.p0=x.p0*X%mod;
for(int i=1;i<=num;++i)ans.a0[i]=x.a0[i]-y.a0[i];
return ans;
}
void init(int n){
int tmp=mod;
for(int i=2;i*i<=tmp;++i){
if(tmp%i==0){
p[++num]=i;
int cnt=0;
while(tmp%i==0)tmp/=i,cnt++;
a[num]=cnt;
}
}
if(tmp>1){
p[++num]=tmp;
a[num]=1;
}
for(int i=2;i<=n;++i)fac[i]=mul(work(i),fac[i-1]);
}
int C(int n,int m){
if(m>n)return 0;
return calc(div(fac[n],mul(fac[m],fac[n-m])));
}
signed main(){
int n=read();mod=read();int l=read(),r=read(),ans=0;
init(n);
for(int i=0;i<=n;++i){
int tmp=((C(n-i,(l+n-i+1)/2)-C(n-i,(r+n-i)/2+1))%mod+mod)%mod;
if(tmp<=0)continue;
ans=(ans+C(n,i)*tmp%mod)%mod;
}
print((ans+mod)%mod);
return 0;
}
到这里,卡特兰数的学习先告一段落,之后碰见有关的好题会进行补充。