Hướng dẫn giải của Các tấm bìa
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ìm cách ghép các cặp số nguyên dương (trên mỗi tấm bìa) sao cho số lượng lớn nhất các cặp tạo thành số chia hết cho 3. Số mới được tạo thành bằng cách nối chuỗi của hai số bất kỳ (ví dụ 123 và 99 có thể tạo thành 12399 hoặc 99123). Tuy nhiên, tính chất chia hết cho 3 của một số không phụ thuộc vào thứ tự các chữ số mà chỉ phụ thuộc vào tổng các chữ số. Vì vậy, một số chia hết cho 3 khi và chỉ khi tổng các chữ số của nó chia hết cho 3. Khi ghép hai số A và B, tổng các chữ số của số mới là tổng các chữ số của A cộng với tổng các chữ số của B. Do đó, bài toán quy về việc tìm số lượng lớn nhất các cặp số sao cho tổng các chữ số của hai số trong cặp chia hết cho 3.
Các cách tiếp cận
Cách Quy hoạch động (Dynamic Programming)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// dp[i][j] là số cặp tối đa có thể tạo từ i phần tử đầu tiên để lại j phần tử (0 hoặc 1)
// j là số lượng phần tử chưa ghép (số dư mod 3)
// Tuy nhiên, do chỉ quan tâm đến số cặp chia hết 3, ta có thể tối ưu state
// State: dp[i][r] = số cặp tối đa từ i phần tử, với r phần tử chưa ghép?
// Hoặc đơn giản hơn: đếm số lượng các số theo số dư mod 3.
// Approach này thực ra là đếm trực tiếp thay vì DP.
// Tính tổng các chữ số modulo 3 cho từng số
long long cnt[3] = {0, 0, 0};
for (int x : a) {
int s = 0;
while (x > 0) {
s += x % 10;
x /= 10;
}
cnt[s % 3]++;
}
// Các cặp có tổng chia hết cho 3:
// 1. (0, 0) -> tổng chia hết 3
// 2. (1, 2) -> tổng chia hết 3
// 3. (0, 1) hoặc (0, 2) -> không chia hết
// 4. (1, 1) -> tổng chia hết 2
// 5. (2, 2) -> tổng chia hết 1
// Đếm số cặp (0, 0)
long long pairs_0 = cnt[0] / 2;
// Đếm số cặp (1, 2)
long long pairs_12 = min(cnt[1], cnt[2]);
cout << pairs_0 + pairs_12 << "\n";
}
return 0;
}
- Time Complexity: O(n * log(max_value))
- Space Complexity: O(1)
Dù gọi là Quy hoạch động, cách tiếp cận hiệu quả nhất thực chất là đếm (counting). Chúng ta tính tổng các chữ số của mỗi số và lấy modulo 3. Sau đó, chỉ có hai loại cặp tạo ra tổng chia hết cho 3: cặp hai số cùng có tổng chữ số chia hết cho 3 (dư 0 + dư 0) và cặp một số dư 1 và một số dư 2 (1 + 2 = 3). Ta chỉ cần đếm số lượng các số thuộc từng loại (dư 0, 1, 2) rồi tính toán số cặp tối đa có thể tạo thành từ các loại này.
Cách Greedy (Tham lam)
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
long long cnt[3] = {0, 0, 0};
for (int i = 0; i < n; i++) {
long long x;
cin >> x;
// Tính tổng các chữ số
long long sum_digits = 0;
while (x > 0) {
sum_digits += x % 10;
x /= 10;
}
cnt[sum_digits % 3]++;
}
// Phép toán tham lam:
// 1. Luôn ghép các cặp (0, 0) trước, mỗi cặp tốn 2 số dư 0.
// 2. Luôn ghép các cặp (1, 2) trước, mỗi cặp tốn 1 số dư 1 và 1 số dư 2.
// Đây là lựa chọn tối ưu vì nó không ảnh hưởng đến các cặp còn lại.
long long res = (cnt[0] / 2) + min(cnt[1], cnt[2]);
cout << res << "\n";
}
return 0;
}
- Time Complexity: O(n * log(max_value))
- Space Complexity: O(1)
Đây là cách tiếp cận trực quan và tối ưu nhất. Ta chia các số thành 3 nhóm dựa trên tổng các chữ số của chúng khi chia cho 3 (dư 0, 1, hoặc 2).
- Nhóm dư 0: Có thể ghép với nhau để tạo tổng chia hết 3. Mỗi cặp cần 2 số.
- Nhóm dư 1 và dư 2: Ghép một số từ nhóm 1 với một số từ nhóm 2 sẽ tạo tổng chia hết 3.
Tối ưu là ta nên ghép hết các cặp có thể theo 2 cách trên. Số lượng cặp tối đa là số cặp (0,0) tạo được cộng với số cặp (1,2) tạo được. Sử dụng
long longcho biến đếm để tránh tràn số nếu n lớn (n=10^4).
Cách Giải thích chi tiết (Kiến thức cơ bản)
#include <iostream>
#include <vector>
using namespace std;
int main() {
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
// Khai báo mảng đếm 3 phần tử cho 3 loại
int count_mod[3] = {0, 0, 0};
for (int i = 0; i < n; i++) {
int num;
cin >> num;
// Tính tổng các chữ số của số đó
int sum = 0;
int temp = num;
while (temp > 0) {
sum += temp % 10;
temp /= 10;
}
// Đếm số lượng theo phần dư khi chia 3
count_mod[sum % 3]++;
}
// Tính toán số cặp
// Cặp (0,0): có count_mod[0] / 2 cặp
// Cặp (1,2): có min(count_mod[1], count_mod[2]) cặp
int ans = count_mod[0] / 2 + min(count_mod[1], count_mod[2]);
cout << ans << endl;
}
return 0;
}
- Time Complexity: O(n * log(max_value))
- Space Complexity: O(1)
Đây là cách trình bày chi tiết nhất của thuật toán tham lam. Bước đầu tiên là nhận ra rằng tính chia hết cho 3 của một số lớn được ghép từ A và B phụ thuộc vào (Tổng chữ số của A + Tổng chữ số của B) % 3 == 0. Do đó, ta chỉ cần quan tâm đến (Tổng chữ số) % 3 của từng tấm bìa. Ta dùng mảng 3 phần tử để đếm số lượng tấm bìa có tổng chữ số chia hết cho 3, có dư 1, và có dư 2. Cuối cùng, lấy số cặp tối đa có thể tạo từ các nhóm này.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(n * log(max_value)) | O(1) | Quy hoạch động (Dynamic Programming) |
| 2 | O(n * log(max_value)) | O(1) | Greedy (Tham lam) |
| 3 | O(n * log(max_value)) | O(1) | Giải thích chi tiết (Kiến thức cơ bản) |
Bài học kinh nghiệm
- Tính chia hết cho 3 của số ghép từ A và B chỉ phụ thuộc vào tổng các chữ số của A và B. Do đó, ta có thể thay thế mỗi số bằng phần dư của tổng các chữ số của nó khi chia cho 3.
- Chỉ có hai loại cặp số tạo ra tổng chia hết cho 3: (dư 0 + dư 0) và (dư 1 + dư 2).
Lỗi thường gặp
- Lỗi tính chia hết cho 3 dựa trên giá trị số nguyên (n % 3 == 0) thay vì tổng các chữ số. Ví dụ: 123 có giá trị 123 chia hết cho 3, nhưng tổng chữ số là 6 (chia hết). Tuy nhiên, 124 có giá trị 124 không chia hết cho 3, tổng chữ số là 7 (không chia hết). Nhưng ví dụ quan trọng: 12 và 3. 12 có tổng chữ số 3, 3 có tổng chữ số 3. Ghép lại thành 123 hoặc 312 đều chia hết 3. Ta cần dùng tổng chữ số.
- Sử dụng biến kiểu
intcho biến đếm có thể gây tràn số nếu n lớn và nhiều cặp được tạo ra (trong bài này n <= 10^4 nênintan toàn, nhưnglong longlà chuẩn mực tốt hơn).
Bình luận