Catalan Number & Lucas定理 & 中国剩余定理(CRT)
又双叒叕来水数论了
今天来学习\(Lucas \:\ \& \:\ Catalan Number\)
两者有着密切的联系(当然还有CRT),所以放在一起学习一下
Catalan Number
定义
卡特兰数(Catalan Number)又称卡塔兰数,是组合数学中一个经常出现在各种计数问题中的数列
\(1,2,5,14,42,132,429,1430,4862,16796,58786,208010,742900\)
啊这
其实跟\(Lucas\)没有什么太大的联系
只有其中一个求\(Catalan Number\)的公式可能会用到\(Lucas\)定理
性质
上面提到说其中一个公式,也就是说求\(Catalan Number\)有很多方法
- 递归公式1
- 递归公式2
- 组合公式1(就是这个的分子可能会用到\(Lucas\)忽然找不到手头做过的那道题了)当然这个式子也是它的通项公式
- 组合公式2(同上)
应用背景
一般用来解决一些现实问题,很复杂的题目被看破之后直接用公式算出来即可
举个例子,合法的入栈出栈序列有多少种就是卡特兰数。我们可以把0看成入栈操作,1看成出栈操作,即0的累计个数不小于1的排列有多少种。
还有很多其他的问题都是卡特兰数,如括号匹配,二叉树的个数,有序树的个数,多边形分成三角形的个数等。
-
n+1 个叶子节点的二叉树的数量
-
n*n的方格地图中,从一个角到另外一个角,不跨越对角线的路径数
-
n+2条边的多边形,能被分割成三角形的方案数
实现过程
来看一道例题做入门吧
题目描述
众所周知,在中国古代算筹中,红为正,黑为负……
给定一个\(1*(2n)\)的矩阵,现让你自由地放入红色算筹和黑色算筹,使矩阵平衡[即对于所有的\(i(1<=i<=2n)\),使第\(1-i\)格中红色算筹个数大于等于黑色算筹]
问有多少种方案满足矩阵平衡。
输入格式
正整数 n
输出格式
方案数t对100取模
输入输出样例
Input
2
Output
2
样例解释
红 黑 红 黑
红 红 黑 黑
solution
- 将红看做入栈操作,黑看为出栈操作,问题即为求不同的合法的入栈出栈序个数
- 我们可以考虑用递推式(1)求卡特兰数,取模更容易
code
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main(){
long long ctl[110],i,j,k,n;
memset(ctl,0,sizeof(ctl));
ctl[0]=ctl[1]=1;
ctl[2]=2;
scanf("%lld",&n);
for(i=3;i<=n;++i)
for(j=0;j<i;++j){
ctl[i]+=ctl[j]*ctl[i-j-1];
ctl[i]%=100;
}
printf("%lld\n",ctl[n]);
return 0;
}
这里只给出了第一个式子的应用,更多的请自行查找题进行思维练习。
Lucas
定义&性质
\(Lucas\)定理是用来求 $ C_n^m mod p $ 的值。
其中\(n\)和\(m\)是非负整数,\(p\)是素数。
一般用于\(m,n\)很大而\(p\)很小,抑或是\(n,m\)不大但是大于\(p\)的情况下来求结果。
用处&背景
目前我们学过几个用来求组合数的方法
- 最暴力的杨辉三角,用二维数组解决,复杂度 \(O(n^2)\) 效率低下而且由于数组的原因只能处理很小的数据
- 将组合式\(C_{n+m}^n \% mod\)转换为\(\frac {(m+n)!} {n! m!} \%mod\),由于除法不可以边除边取\(mod\)所以在处理分母的时候选用了费马小定理,运用逆元的乘法解决。
看分子的话,\((m+n)!\)在遍历的时候若有元素\(i>mod\),那么取\(mod\)之后整个式子的值就会变成0,而\(Lucas\)的出现,就是为了解决这一问题
引入
针对上述第二种情况,我们先来看一道例题
旗木双翼
题目描述
菲菲和牛牛在一块n行m列的棋盘上下棋,菲菲执黑棋先手,牛牛执白棋后手。
棋局开始时,棋盘上没有任何棋子,两人轮流在格子上落子,直到填满棋盘时结束。落子的规则是:一个格子可以落子当且仅当这个格子内没有棋子且这个格子的左侧及上方的所有格子内都有棋子。
_Itachi听说有不少学弟在省选现场AC了D1T1,解决了菲菲和牛牛的问题,但是_Itachi听说有的人认为复杂度玄学,_Itachi并不想难为学弟学妹,他想为大家节约时间做剩下的题,所以将简化版的D1T1带给大家。
_Itachi也在一块n行m列的棋盘上下棋,不幸的是_Itachi只有黑棋,不过幸好只有他一个人玩。现在,_Itachi想知道,一共有多少种可能的棋局(不考虑落子顺序,只考虑棋子位置)。
_Itachi也不会为难学弟学妹们去写高精度,所以只需要告诉_Itachi答案mod 998244353(一个质数)的结果。
输入格式
第一行包括两个整数n,m表示棋盘为n行m列。
输出格式
一个整数表示可能的棋局种数。
样例输入
10 10
样例输出
184756
数据范围与提示
对于 \(20\%\)的数据$n,m \leq10 $
对于 \(30\%\)的数据$n,m\leq20 $
另有 \(20\%\)的数据$n\leq5 $
另有 \(20\%\)的数据$m\leq5 $
对于\(100\%\)的数据$n,m\leq100000 $
solution
可以看到题目当中直接给出了\(mod\)为一个大质数
而且\(m+n\)最大也才二十万,远远小于\(mod\)的数值
所以不会出现取完\(mod\)为\(0\)的情况,所以运用逆元求解即可
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
inline int read(){
int x = 0, w = 1;
char ch = getchar();
for(; ch > '9' || ch < '0'; ch = getchar()) if(ch == '-') w = -1;
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
return x * w;
}
const int mod = 998244353;
inline int power(int a, int b){
int ans = 1;
while(b){
if(b & 1) ans = (ans % mod) * (a % mod) % mod;
a = (a % mod) * (a % mod) % mod;
b >>= 1;
}
ans %= mod;
return ans;
}
signed main(){
int n = read(), m = read();
int tmp = 1;
for(int i = m + n; i >= 1; i--){
tmp *= i;
tmp %= mod;
}
int n1 = 1;
int m1 = 1;
for(int i = 1; i <= n; i++){
n1 *= i;
n1 %= mod;
}
for(int i = 1; i <= m; i++){
m1 *= i;
m1 %= mod;
}
int ans = (tmp % mod) * power(n1 ,mod - 2) % mod * power(m1, mod - 2) % mod;
ans %= mod;
cout << ans << endl;
return 0;
}
如果\(mod\)很小
上述算法显然在求阶乘的时候会变成\(0\)
这就需要\(Lucas\)定理了
性质
性质1
其中
$ :\ :\ :\ :\ :\ :\ :\ :\ :\ $ \((a-b)!^{p-2}\)为\(a-b\)的逆元,所以可以除一下把式子变成
性质2
就是一个\(a,b\)可以拆成\(P\)进制下的乘积
证明
目标方程:
显然右侧可以递归处理,我们只需要推左边就好
证明过程:
设
则目标方程变成
下面利用二项式定理&费马小证明一个引理
根据上面的那个性质
则有
左边\(x^{tp+r}\)的系数为\(\binom {sp+q}{tp+r}\)
右边\(x^{tp+r}\)的系数为\(\binom s t\binom q r\)
故系数相等,原命题得证
即
即
(以上证明结束)
实现过程
- 目标运算结果\(C_n^m \:\ \%p\)
- 注意输入的\(n,m\)中,可能会出现\(n\%p<m%p\),那么直接输出0即可。因为若\(n<m\),则\(C_n^m=0\)
- 在求逆元时,运用快速幂求其\(p-2\)次方
- 如果要大量运用\(Lucas\)定理,建议使用杨辉三角打组合数表或将阶乘以及阶乘逆元打表
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
inline int read(){
int x = 0, w = 1;
char ch = getchar();
for(; ch > '9' || ch < '0'; ch = getchar()) if(ch == '-') w = -1;
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + ch - '0';
return x * w;
}
int n, m, p;
inline int power(int a, int b){
int ans = 1;
while(b){
if(b & 1) ans = (ans % p) * (a % p) % p;
a = (a % p) * (a % p) % p;
b >>= 1;
}
ans %= p;
return ans;
}
inline int getc(int n, int m){
if(n < m) return 0;
if(m > n - m) m = n - m;
int s1 = 1, s2 = 1;
for(int i = 0; i < m; i++){
s1 = s1 * (n - i) % p;//(n-m)!/n!
s2 = s2 * (i + 1) % p;//m!
}
return s1 * power(s2, p - 2) % p;
}
inline int lucas(int n, int m){
if(m == 0) return 1;
return getc(n % p, m % p) * lucas(n / p, m / p) % p;
}
signed main(){
int t = read();
while(t--){
n = read(), m = read(), p = read();
cout << lucas(n + m, m) << endl;
}
return 0;
}