hdu 3240
題目連結
題目概述
給出\(n\)個結點,最多\(n\)個結點可以組成的不同形態的二叉樹的數目.結果很大,所以輸出的結果要對\(m\)取模,其中\((1 \leq n \leq 100000), (1 \leq m \leq 100000000)\).
一點思路
因為是最多用\(n\)個結點可以構成的總數,實際是1個結點,2個結點,\(\dots,n\)個結點可以構成的不同形態的二叉樹的數目之和.所以問題就轉化成求\(n\)個相同的結點可以構成的不同的二叉樹形態的數目.假設\(n\)個結點可以構成的二叉樹的\(f(n)\)個.對於\(n\)個相同的結點,首先有一個結點是根節點,然後對於剩下的\(n-1\)
\[\begin{aligned} f(n) &= f(0)f(n-1)+f(1)f(n-2)+\cdots+f(n-1)f(0) \\ &= \sum_{i=0}^{n-1}{f(i)f(n-1-i)},\qquad f(0) = 1 \end{aligned} \]
這個恰好就是\(Catalan\)數的一個計算表示的方法.所以,\(n\)個相同結點可以構成的不同形態的二叉樹的數目恰好是\(C_n\).
然後是解決計算\(Catalan\)數取模的問題.\(C_n\)的計算有三個公式:
\[\begin{aligned} C_n &= C_0C_{n-1}+C_1C_{n-2}+\cdots+C_{n-1}C_0 = \sum_{i=0}^{n-1}C_iC_{n-1-i}, \qquad C_0 = 1\\ C_n &= \frac{4*n-2}{n+1}C_{n-1}, \qquad C_0=1\\ C_0 &= \frac{1}{n+1}C_{2n}^n=\frac{(2n)!}{(n+1)!n!} \end{aligned} \]
最初我用的是第一個式子計算提交的,然後TLE.然後第二個因為有一個分式,並且不能總保證\((4*n-2)\%(n+1)=0\)總成立,而\(C_{n-1}\)在前面的計算中已經取模了,很明顯如果直接分子分母取模再相除結果一定是不對的,所以我最初的想法是通過通過逆元把\(\frac{4*n-2}{n+1}\%m\)轉成\(((4*n-2)k)\%m\)來進行計算,其中的\(k\)是\((n+1)\)模\(m\)的逆,但是發現這個會出問題,因為\((n+1)k\equiv 1\, (mod \ m)\),只有當\(m\)是素數的條件下才成立,比如當\(n=1,m=100\)時,顯然是沒法求出這個逆元的.然後就一直卡在這裡了.┭┮﹏┭┮
進一步的想法
對於模數\(m\),來說,滿足\(m=p_1^{a_1}p_2^{a_2}\cdots p_k^{a_k},\, p_i\)是素數\(,a_i\ge1.\)所以可以先從\((4*n-2)\)和\((n+1)\)中先剔除掉\(m\)的素因子,如果此時的\((n+1)\ !=1\)那麼可以通過求逆的方法,把原來的除法取模變成乘法逆元取模了.然後這個然後再乘上哪些提出掉的素因子再對\(m\)取模得到的就是\(C_n\%m\).
程式碼實現
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+5;
// ll S[N];
// 擴充套件gcd
void extend_gcd(ll a, ll b, ll& x, ll& y) {
if(b == 0){
x = 1;
y = 0;
return;
}
extend_gcd(b, a%b, x, y);
ll temp = x;
x = y;
y = temp - (a / b) * y;
}
// 計算a模m的逆
ll mod_inverse(ll a, ll m){
ll x, y;
extend_gcd(a, m, x,y);
return (m + x % m) % m;
}
// // 前項和乘積類加求解,TLE
// void calculate(int n, int m){
// memset(S, 0, sizeof(S));
// S[0] = 1;
// ll ans = 0;
// for (int i = 1; i <= n; ++i){
// for (int j = 0; j < i; ++j){
// S[i] += S[j]*S[i-1-j];
// S[i] %= m;
// }
// ans += S[i];
// ans %= m;
// }
// printf("%lld\n", ans);
// }
// 利用遞推式和逆元取模計算
void calculate(int n, int m){
ll primes[N] = {0};
int cnt = 0;
ll nums[N] = {0};
// 計算m的素因子
ll temp = m;
for (ll i = 2; i * i <= temp;++i){
if(temp% i == 0){
primes[cnt++] = i;
// 剔除調m中的所有素因子i
while(temp % i == 0){
temp /= i;
}
}
}
// 判斷最後剩下m是不是一個素數
if( temp != 1)
primes[cnt++] = temp;
ll pre = 1;
ll ans = 0;
ll cur = 1;
for (int i = 1; i <= n; ++i){
//去掉(4*i-2)中m的素因子,記錄這些素因子的個數
ll k = 4 * i - 2;
for (int j = 0; j < cnt; j++){
// (4*i-2)中包含了m的某個素因子不止一次.
while( k % primes[j] == 0){
++nums[j];
k /= primes[j];
}
}
pre *= k;
pre %= m;
// 去掉(i+1)中m的素因子,把這些素因子的個數從前面(4*i-2)中剔除
k = i + 1;
for (int j = 0; j < cnt; j++){
// (4*i-2)中包含了m的某個素因子不止一次.
while( k % primes[j] == 0){
--nums[j];
k /= primes[j];
}
}
// 計算剔除素因子後(i+1)的逆元
if( k != 1)
k = mod_inverse(k, m);
pre *= k;
pre %= m;
cur = pre;
for (int j = 0; j < cnt; j++){
// 乘以之前剔除的素因子
for (int k = 0; k < nums[j]; ++k)
cur = (cur * primes[j]) % m;
}
ans += cur;
ans %= m;
}
printf("%lld\n", ans);
}
int main(int argc, const char** argv) {
int n = 0, m = 0;
while(scanf("%d%d", &n,&m) && (n+m != 0)) {
calculate(n,m);
}
return 0;
}
這裡面我覺得可能用一忽略的一個坑點就是那在求\(m\)的素因子,不僅僅求的是這一個素因子\(p_i\),還要把\(m\)中的\(p_i^{a_i}\)剔除掉,所以要不斷的除以這個素因子\(p_i\),直到不再包含.然後要分清楚這裡的cur
也就是乘以當前剔除掉的素因子(分子分母相同的素因子會剔除掉一部分).