Hướng dẫn giải của Bội chung nhỏ nhất "lớn nhất"
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ả: , , ,
Editorial for lcmseq: Bội chung nhỏ nhất "lớn nhất"
Hiểu bài toán
Bài toán yêu cầu tìm một dãy số dương có tổng bằng n (n ≤ 350) sao cho bội chung nhỏ nhất (LCM) của các số trong dãy đó là lớn nhất có thể. Ta cần tối ưu hóa giá trị LCM thay vì chỉ đơn thuarily tối ưu hóa các số trong dãy. Do n nhỏ, ta có thể sử dụng các phương pháp tối ưu hóa tổ hợp.
Các cách tiếp cận
Cách Quy hoạch động (Dynamic Programming)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int MAXN = 355;
int n;
ll dp[MAXN][MAXN]; // dp[i][j]: LCM lớn nhất có thể đạt được khi xét các số từ 2 đến p[j] (số nguyên tố thứ j) với tổng các số là i
vector<int> primes;
void sieve(int n) {
vector<bool> is_prime(n + 1, true);
is_prime[0] = is_prime[1] = false;
for (int i = 2; i * i <= n; i++) {
if (is_prime[i]) {
for (int j = i * i; j <= n; j += i)
is_prime[j] = false;
}
}
for (int i = 2; i <= n; i++) {
if (is_prime[i]) primes.push_back(i);
}
}
int main() {
cin >> n;
sieve(n);
// Khởi tạo
for (int i = 0; i <= n; i++) dp[i][0] = 1;
ll ans = 1;
int m = primes.size();
for (int j = 1; j <= m; j++) {
int p = primes[j-1];
for (int i = 0; i <= n; i++) {
dp[i][j] = dp[i][j-1]; // Bỏ qua số p
// Thêm các bội của p (p, p^2, ...) vào dãy
ll val = p;
while (val <= i) {
dp[i][j] = max(dp[i][j], dp[i - val][j-1] * val);
if (val > (LLONG_MAX / p)) break; // Tránh tràn số
val *= p;
}
if (i == n) ans = max(ans, dp[i][j]);
}
}
cout << ans << endl;
return 0;
}
- Time Complexity: O(n * P * log_n) ~ O(350 * 70 * 8)
- Space Complexity: O(n * P) ~ O(350 * 70)
Gọi dp[i][j] là LCM lớn nhất có thể tạo ra khi xét các số nguyên tố từ 1 đến j và tổng các số là i.
- Nếu không dùng số nguyên tố p_j, dp[i][j] = dp[i][j-1].
- Nếu dùng, ta có thể dùng pj, pj^2, pj^3... với giá trị tương ứng là pj, pj^2, pj^3...
- Khi dùng số pj^k, ta cần tìm LCM lớn nhất với tổng còn lại i - pj^k bằng các số nguyên tố trước đó (j-1).
- Vì LCM của các số là tích các thừa số nguyên tố, và mỗi thừa số chỉ xuất hiện ở số có giá trị lớn nhất, ta có thể xử lý tuần tự từng số nguyên tố.
- Độ phức tạp: Số lượng số nguyên tố ≤ 70. Với mỗi số nguyên tố, ta thử các lũy thừa (logp n) và duyệt tổng n. Tổng cộng O(n * P * logn).
Cách Tối ưu hóa quy hoạch động (DP)
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int N = 355;
int n;
ull dp[N]; // dp[x]: LCM lớn nhất với tổng x
vector<int> primes;
void sieve(int limit) {
vector<bool> is_prime(limit + 1, true);
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= limit; i++) {
if (is_prime[i]) {
primes.push_back(i);
for (int j = i * i; j <= limit; j += i) is_prime[j] = false;
}
}
}
int main() {
cin >> n;
sieve(n);
for (int i = 0; i <= n; i++) dp[i] = 1;
ull ans = 1;
for (int p : primes) {
// Duyệt ngược để đảm bảo mỗi loại số chỉ dùng 1 lần (tương tự bài toán túi đồ)
// Tuy nhiên, do LCM yêu cầu, ta cần xét các hệ số lũy thừa
// Ta dùng mảng temp để lưu kết quả mới
ull temp[N];
for (int i = 0; i <= n; i++) temp[i] = dp[i];
for (int i = n; i >= 0; i--) {
ll val = p;
while (val <= i) {
// Nếu dùng val (là p, p^2, ...), tổng còn lại là i - val
// LCM mới = LCM cũ * val (vì val là lũy thừa của p, và các số trước không chứa p)
// Tuy nhiên, phải chú ý: dp[i - val] có thể đã chứa p?
// Không, vì ta duyệt qua các số nguyên tố tuần tự, mỗi số nguyên tố chỉ xét 1 lần.
// Nên dp[i - val] không chứa thừa số p.
temp[i] = max(temp[i], dp[i - val] * val);
if (val > (1e18 / p)) break;
val *= p;
}
}
// Cập nhật lại dp sau khi xử lý xong số p
for (int i = 0; i <= n; i++) {
dp[i] = temp[i];
if (i == n) ans = max(ans, dp[i]);
}
}
cout << ans << endl;
return 0;
}
- Time Complexity: O(n * P * log_n)
- Space Complexity: O(n)
Cách tiếp cận này sử dụng mảng 1 chiều dp[i] để tối ưu không gian.
- Khởi tạo dp[i] = 1 cho mọi i.
- Duyệt qua từng số nguyên tố p.
- Với mỗi p, tạo mảng temp để lưu kết quả mới (tránh ghi đè).
- Với mỗi tổng i, thử các lũy thừa p, p^2, ... của p.
- Nếu dùng p^k, LCM mới = dp[i - p^k] * p^k.
- Sau khi duyệt hết các lũy thừa của p, cập nhật dp từ temp.
- Ưu điểm: Tiết kiệm bộ nhớ, logic tương tự nhưng implement gọn hơn.
Cách Tối ưu hóa bằng ma trận (Matrix Exponentiation / Math)
// Ý tưởng: Sử dụng các số nguyên tố nhỏ để tạo ra LCM lớn nhất.
// Do n nhỏ, ta có thể liệt kê các số nguyên tố.
// LCM lớn nhất sẽ là tích của các số nguyên tố hoặc lũy thừa của chúng.
// Ví dụ: n=7, ta có thể dùng 3 và 4 (tức 2^2 * 3). Tổng = 7, LCM = 12.
// n=10, ta có thể dùng 2, 3, 5 (tổng 10, LCM 30).
// Hoặc 2*2=4, 3, 3 (tổng 10, LCM 12).
// Cách này về cơ bản là tìm cách phân tách n thành các số sao cho tích (LCM) lớn nhất.
// Do n <= 350, có thể dùng DP.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
int main() {
int n;
cin >> n;
vector<int> primes;
for (int i = 2; i <= n; i++) {
bool ok = true;
for (int j = 2; j * j <= i; j++) {
if (i % j == 0) { ok = false; break; }
}
if (ok) primes.push_back(i);
}
// dp[i] là LCM lớn nhất với tổng i
vector<ull> dp(n + 1, 1);
for (int p : primes) {
vector<ull> next_dp = dp;
for (int i = 0; i <= n; i++) {
ull val = p;
while (i + val <= n) {
next_dp[i + val] = max(next_dp[i + val], dp[i] * val);
val *= p;
}
}
dp = next_dp;
}
cout << dp[n] << endl;
return 0;
}
- Time Complexity: O(n * P * log_n)
- Space Complexity: O(n)
Đây là cách ghi lại của phương pháp quy hoạch động 1 chiều.
- Ta chỉ quan tâm đến các số nguyên tố.
- Với mỗi số nguyên tố p, ta có thể thêm p, p^2, ... vào dãy.
- Việc nhân p, p^2... vào LCM hiện tại tương đương với việc tối ưu hóa giá trị.
- Lưu ý: Phải đảm bảo rằng khi dùng p, ta không dùng lại p từ các số khác (trong cùng 1 nhóm). DP đã giải quyết việc này bằng cách duyệt qua các số nguyên tố tuần tự.
- Kết quả là dp[n].
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(n * P * log_n) ~ O(350 * 70 * 8) | O(n * P) ~ O(350 * 70) | Quy hoạch động (Dynamic Programming) |
| 2 | O(n * P * log_n) | O(n) | Tối ưu hóa quy hoạch động (DP) |
| 3 | O(n * P * log_n) | O(n) | Tối ưu hóa bằng ma trận (Matrix Exponentiation / Math) |
Bài học kinh nghiệm
- LCM của dãy số phụ thuộc vào các thừa số nguyên tố. Để LCM lớn nhất, ta cần các thừa số nguyên tố lớn nhất có thể (ví dụ: 2^a * 3^b * 5^c...).
- Vì tổng n nhỏ (350), ta có thể sử dụng quy hoạch động để thử các cách phân chia tổng.
- Mỗi số nguyên tố chỉ nên được sử dụng một lần trong việc tính LCM (dưới dạng các lũy thừa của nó). Việc này biến bài toán thành bài toán 'túi đồ' (Knapsack) với trọng lượng là giá trị số và giá trị là lũy thừa của nó.
- Có thể tối ưu bộ nhớ bằng cách dùng mảng 1 chiều và duyệt ngược hoặc dùng mảng 2 chiều để dễ hình dung.
Lỗi thường gặp
- Tràn số unsigned long long: Cần kiểm tra điều kiện val > ULLONG_MAX / p trước khi nhân.
- Sai logic LCM: LCM của a và b không phải là a*b (nếu không cùng ước chung), nhưng trong bài này, do ta phân tích theo thừa số nguyên tố và chỉ dùng mỗi thừa số một lần (qua các lũy thừa), phép nhân là chính xác.
- Bỏ qua các số không phải nguyên tố: Chỉ các số nguyên tố (và lũy thừa của chúng) mới đóng góp vào LCM lớn nhất một cách hiệu quả.
Bình luận