又雙叒叕來水數論了
今天來學習\(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;
}