Hướng dẫn giải của Tính số CATALAN
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.
Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.
Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.
Bạn phải đăng nhập để mở lời giải này.
Đăng nhậpTác giả: , , ,
Hiểu bài toán
Bài toán yêu cầu tính số Catalan Cn theo công thức Cn = (1/(n+1)) * C(2n, n) = (2n)! / ((n+1)! * n!) cho n trong khoảng [0, 1000]. Với n lớn, C_n sẽ là một số rất lớn (vượt quá khả năng lưu trữ của các kiểu dữ liệu nguyên cơ bản), do đó cần sử dụng các kỹ thuật xử lý số lớn (big integer).
Các cách tiếp cận
Cách Quy hoạch động dựa trên công thức truy hồi
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
// Hàm cộng hai số lớn dạng string
string add(string a, string b) {
string res = "";
int carry = 0;
int i = a.length() - 1, j = b.length() - 1;
while (i >= 0 || j >= 0 || carry) {
int sum = carry;
if (i >= 0) sum += a[i--] - '0';
if (j >= 0) sum += b[j--] - '0';
res += (sum % 10) + '0';
carry = sum / 10;
}
reverse(res.begin(), res.end());
return res;
}
int main() {
int n;
cin >> n;
vector<string> dp(n + 1);
dp[0] = "1";
for (int i = 1; i <= n; i++) {
dp[i] = "0";
for (int j = 0; j < i; j++) {
dp[i] = add(dp[i], multiply(dp[j], dp[i - 1 - j])); // Need multiply function
}
}
cout << dp[n] << endl;
return 0;
}
- Time Complexity: O(n^2 * M) với M là độ dài của số lớn (có thể lên tới ~2000 chữ số cho n=1000)
- Space Complexity: O(n * M)
Sử dụng công thức truy hồi của số Catalan: Cn = sum(Ci * C_{n-1-i}) từ i=0 đến n-1. Ta xây dựng dãy C từ 0 đến n. Cần các hàm cộng và nhân số lớn. Phương pháp này dễ hiểu nhưng độ phức tạp cao do phải thực hiện nhiều phép nhân số lớn.
Cách Tính trực tiếp theo công thức kết hợp Phân tích thừa số nguyên tố (Optimized)
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
const int MAX = 2500; // Đủ để chứa các số từ 2n (2000)
int prime[MAX];
int exponent[MAX];
void sieve() {
for (int i = 2; i < MAX; i++) prime[i] = i;
for (int i = 2; i * i < MAX; i++) {
if (prime[i] == i) {
for (int j = i * i; j < MAX; j += i)
if (prime[j] == j) prime[j] = i;
}
}
}
void add_exponent(int n, int factor) {
while (n > 1) {
int p = prime[n];
while (n % p == 0) {
exponent[p] += factor;
n /= p;
}
}
}
void multiply(vector<int> &res, int x) {
int carry = 0;
for (int i = 0; i < res.size(); i++) {
long long cur = 1LL * res[i] * x + carry;
res[i] = cur % 10;
carry = cur / 10;
}
while (carry) {
res.push_back(carry % 10);
carry /= 10;
}
}
int main() {
int n;
cin >> n;
if (n == 0) {
cout << 1 << endl;
return 0;
}
sieve();
// Tính số mũ cho (2n)! / ((n+1)! * n!)
// C_n = (2n)! / ((n+1)! * n!)
// Ta có thể viết lại: C_n = (2n)! / (n! * (n+1)!) = (2n)! / (n! * n! * (n+1))
// Hay dùng Suy biến: C_n = Prod_{k=2}^{2n} k / (Prod_{k=2}^{n} k * Prod_{k=2}^{n} k * (n+1))
// Thêm/Sớt số mũ nguyên tố
// Tăng mũ cho (2n)!
for (int i = 2; i <= 2 * n; i++) add_exponent(i, 1);
// Giảm mũ cho n!
for (int i = 2; i <= n; i++) add_exponent(i, -1);
// Giảm mũ cho (n+1)!
for (int i = 2; i <= n + 1; i++) add_exponent(i, -1);
vector<int> result;
result.push_back(1);
// Tích tất cả các số nguyên tố với số mũ tương ứng
// Tối ưu: nhân từ nhỏ đến lớn
for (int i = 2; i < MAX; i++) {
if (exponent[i] > 0) {
for (int j = 0; j < exponent[i]; j++) {
multiply(result, i);
}
}
}
for (int i = result.size() - 1; i >= 0; i--) cout << result[i];
cout << endl;
return 0;
}
- Time Complexity: O(N log N + M^2) hoặc O(N log N * M) (M là độ dài số)
- Space Complexity: O(M)
Cách này phân tích Cn theo các thừa số nguyên tố. Công thức Cn = (2n)! / ((n+1)! n!). Ta tính số mũ của mỗi số nguyên tố p trong phân số này bằng cách cộng/trừ số lượng lần p chia hết cho các giai thừa. Cuối cùng, nhân các số nguyên tố lại. Đây là phương pháp tối ưu nhất về độ phức tạp tính toán.
Cách Quy hoạch động theo công thức nhân
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void multiply(vector<int> &a, int x) {
int carry = 0;
for (int i = 0; i < a.size(); i++) {
long long cur = 1LL * a[i] * x + carry;
a[i] = cur % 10;
carry = cur / 10;
}
while (carry) {
a.push_back(carry % 10);
carry /= 10;
}
}
void divide(vector<int> &a, int x) {
long long rem = 0;
for (int i = a.size() - 1; i >= 0; i--) {
long long cur = rem * 10 + a[i];
a[i] = cur / x;
rem = cur % x;
}
while (a.size() > 1 && a.back() == 0) a.pop_back();
}
int main() {
int n;
cin >> n;
vector<int> res;
res.push_back(1);
// Dựa trên C_n = C_{n-1} * (4n - 2) / (n + 1)
for (int i = 1; i <= n; i++) {
multiply(res, 4 * i - 2);
divide(res, i + 1);
}
for (int i = res.size() - 1; i >= 0; i--) cout << res[i];
cout << endl;
return 0;
}
- Time Complexity: O(n * M)
- Space Complexity: O(M)
Sử dụng công thức truy hồi nhân: Cn = C{n-1} * (4n - 2) / (n + 1). Ta bắt đầu với C_0 = 1 và lặp từ 1 đến n, nhân số hiện tại với (4i - 2) rồi chia cho (i + 1). Phương pháp này thực hiện các phép nhân/chia số lớn với số nguyên nhỏ, rất hiệu quả và dễ code.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(n^2 * M) với M là độ dài của số lớn (có thể lên tới ~2000 chữ số cho n=1000) | O(n * M) | Quy hoạch động dựa trên công thức truy hồi |
| 2 | O(N log N + M^2) hoặc O(N log N * M) (M là độ dài số) | O(M) | Tính trực tiếp theo công thức kết hợp Phân tích thừa số nguyên tố (Optimized) |
| 3 | O(n * M) | O(M) | Quy hoạch động theo công thức nhân |
Bài học kinh nghiệm
- Số Catalan tăng trưởng rất nhanh, n=1000 cho ra số có khoảng 600 chữ số, bắt buộc phải dùng big integer.
- Công thức truy hồi Cn = C{n-1} * (4n - 2) / (n + 1) là cách tính hiệu quả nhất về mặt lập trình thi đấu nhờ sự đơn giản và độ phức tạp ổn định O(n * độdàisố).
- Phân tích thừa số nguyên tố là phương pháp tối ưu nhất về mặt lý thuyết độ phức tạp toán học nhưng đòi hỏi nhiều bước tiền xử lý và quản lý số mũ.
Lỗi thường gặp
- Lỗi tràn số nguyên khi tính giai thừa hoặc tích trực tiếp nếu không dùng big integer.
- Sai công thức quy hoạch động (ví dụ tính sai chỉ số i, j trong công thức tổng Ci * Cj).
- Quên xử lý số 0 ở đầu khi thực hiện phép chia hoặc khi in kết quả.
- Biến số và kích thước mảng không đủ để lưu trữ số lớn (cần ước tính độ dài trước).
Bình luận