Hướng dẫn giải của Làm bài thi
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 doexam: Làm bài thi
Hiểu bài toán
Bài toán yêu cầu tìm cách chọn đúng k bài trong n bài để làm sao tổng điểm là lớn nhất, với ràng buộc là không được chọn hai bài liên tiếp. Tuy nhiên, có một quy định đặc biệt: 'nếu thí sinh bỏ qua bài thứ i và bài thứ i + 1 thì tất cả các bài từ bài thứ i trở đi đều không được tính điểm'. Điều này có nghĩa là chuỗi các bài được chọn phải chấm dứt bằng một bài được chọn, và không được phép có hai bài bị bỏ qua liên tiếp ở cuối chuỗi. Nói cách khác, ta chỉ cần chọn k bài sao cho giữa hai bài được chọn liên tiếp chỉ được phép có tối đa 1 bài bị bỏ qua. Bài toán có thể giải bằng phương pháp quy hoạch động (Dynamic Programming).
Các cách tiếp cận
Cách Quy hoạch động (Dynamic Programming)
#include <stdio.h>
#include <string.h>
int max(int a, int b) { return a > b ? a : b; }
int main() {
int n, k;
scanf("%d %d", &n, &k);
int a[30]; // n <= 25
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
// dp[i][j][s]: tổng điểm lớn nhất sau khi xét đến bài thứ i,
// đã chọn j bài, và trạng thái của bài i là s.
// s = 0: bài i được chọn.
// s = 1: bài i bị bỏ qua.
// Khởi tạo giá trị âm vô cùng để xử lý các trường hợp không thể.
int dp[30][30][2];
for (int i = 0; i <= n; i++)
for (int j = 0; j <= k; j++)
dp[i][j][0] = dp[i][j][1] = -1e9;
// Base case: trước bài 1, chưa chọn bài nào, coi như bài 0 được chọn (để thỏa mãn điều kiện xét bài 1)
dp[0][0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= k; j++) {
// Trường hợp 1: Chọn bài i
if (j > 0) {
// Có thể chọn bài i nếu bài i-1 được chọn hoặc bị bỏ qua
int prev = max(dp[i-1][j-1][0], dp[i-1][j-1][1]);
if (prev > -1e9)
dp[i][j][0] = prev + a[i];
}
// Trường hợp 2: Bỏ qua bài i
// Chỉ được bỏ qua nếu bài i-1 được chọn (không được bỏ qua 2 bài liên tiếp)
if (dp[i-1][j][0] > -1e9)
dp[i][j][1] = dp[i-1][j][0];
}
}
// Kết quả là max của việc chọn đủ k bài ở bài cuối cùng (n),
// bài cuối cùng phải được chọn (s=0).
int ans = dp[n][k][0];
printf("%d\n", ans);
return 0;
}
- Time Complexity: O(n * k)
- Space Complexity: O(n * k)
Sử dụng quy hoạch động 3 chiều. Trạng thái dp[i][j][s] lưu tổng điểm lớn nhất khi xét xong bài i, đã chọn j bài, và trạng thái của bài i là s (0: chọn, 1: bỏ).
- Nếu chọn bài
i(s=0): Lấy giá trị lớn nhất từ bàii-1(khii-1được chọn hoặc bỏ) cộng với điểma[i]. - Nếu bỏ bài
i(s=1): Chỉ được phép khi bàii-1được chọn (để tránh bỏ 2 bài liên tiếp). Đáp án cuối cùng làdp[n][k][0](phải chọn đúng k bài và bài cuối cùng phải được chọn để đảm bảo kết thúc hợp lệ).
Cách Brute Force (Thử tất cả các trường hợp)
#include <stdio.h>
#include <limits.h>
int n, k;
int a[105];
int max_score = -1e9;
void Try(int current_index, int count, int score) {
if (count == k) {
if (score > max_score) max_score = score;
return;
}
if (current_index >= n) return;
// Thử chọn bài hiện tại
// Điều kiện: chỉ được chọn nếu số lượng bài còn lại đủ để chọn nốt
// (Đây là heuristic đơn giản, thực tế đệ quy sẽ tự xử lý)
// Lưu ý: Nếu chọn bài hiện tại, ta có thể nhảy tới bài tiếp theo hoặc bỏ qua 1 bài
// Tuy nhiên, phương pháp Try này của solution 3 có vẻ chưa xử lý đúng ràng buộc
// 'không được bỏ qua 2 bài liên tiếp'.
// Dưới đây là code đúng cho Brute Force:
// Chỉ xét các trường hợp có thể:
// 1. Chọn bài current_index
Try(current_index + 1, count + 1, score + a[current_index]);
// 2. Bỏ qua bài current_index (chỉ được bỏ 1 bài)
// Nếu bỏ bài current_index, ta phải chọn bài current_index + 1
// Hoặc nếu current_index + 1 vượt quá n thì không làm gì
if (current_index + 1 < n) {
Try(current_index + 2, count + 1, score + a[current_index + 1]);
}
}
int main() {
scanf("%d %d", &n, &k);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
// Khởi tạo giá trị âm vô cùng
if (k > 0) max_score = -1e9;
else max_score = 0;
Try(0, 0, 0);
printf("%d", max_score);
return 0;
}
// Ghi chú: Solution 3 trong bài nộp có lỗi logic (biến cnt và score toàn cục, vòng lặp for(int j=0; j<=1; j++) có thể không bao quát hết).
// Code trên là cách viết lại Brute Force đúng đắn hơn cho bài toán này.
- Time Complexity: O(2^k) hoặc O(N)
- Space Complexity: O(1)
Đây là cách tiếp cận trực quan nhất: thử tất cả các cách chọn bài hợp lệ. Do ràng buộc 'không bỏ 2 bài liên tiếp', ta có thể đệ quy với các lựa chọn: (1) Chọn bài hiện tại, rồi xét bài tiếp theo; (2) Bỏ bài hiện tại, bắt buộc phải chọn bài tiếp theo (nếu có), rồi xét bài tiếp the tiếp theo. Độ phức tạp là O(N) vì mỗi bước ta chỉ có 2 nhánh và duyệt qua N bài.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(n * k) | O(n * k) | Quy hoạch động (Dynamic Programming) |
| 2 | O(2^k) hoặc O(N) | O(1) | Brute Force (Thử tất cả các trường hợp) |
Bài học kinh nghiệm
- Ràng buộc 'không được bỏ qua hai bài liên tiếp' và 'tất cả các bài từ bài thứ i trở đi đều không được tính điểm nếu bỏ i và i+1' về cơ bản có nghĩa là các bài được chọn phải phân bố sao cho giữa hai bài được chọn liên tiếp chỉ có tối đa 1 bài bị bỏ qua, và bài cuối cùng trong danh sách phải được chọn.
- Quy hoạch động với trạng thái bao gồm số lượng bài đã chọn và trạng thái của bài hiện tại (đã chọn hay chưa) là cách tiếp cận chuẩn để giải bài toán có ràng buộc về tính liên tiếp.
Lỗi thường gặp
- Xử lý sai trường hợp 'bỏ qua bài i và i+1'. Cần đảm bảo rằng nếu bỏ bài i thì bài i+1 không được bỏ.
- Quên khởi tạo DP với giá trị âm vô cùng (vô cực) để loại trừ các trường hợp không thể (ví dụ: chọn quá số bài có thể).
- Trong đệ quy/quay lui, nếu không memoization thì có thể bị TLE hoặc MLE dù n nhỏ, nhưng với n=25 thì Brute Force đơn giản vẫn qua được.
Bình luận