Hướng dẫn giải của Thi giữa kỳ
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 midterm: Thi giữa kỳ
Hiểu bài toán
Môn học này yêu cầu tìm số ngày tối thiểu để Bi có thể hoàn thành việc ôn tập và thi tất cả M môn học. Với mỗi môn i, Bi cần t_i ngày ôn tập (không nhất thiết liên tiếp) và phải thi vào một trong các ngày có lịch thi được cho trước. Vấn đề là chọn một lịch thi cụ thể cho mỗi môn (trong số các ngày có lịch thi của môn đó) và phân bổ các ngày ôn tập sao cho tất cả các công việc hoàn thành trong thời gian ngắn nhất có thể. Chúng ta cần tìm giá trị nhỏ nhất của ngày cuối cùng (T) mà tại đó tất cả các môn đã được thi và ôn tập xong.
Các cách tiếp cận
Cách Tối ưu hóa bằng Binary Search (Phân tích dữ liệu)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
if (!(cin >> N >> M)) return 0;
vector<int> exam(N + 1);
for (int i = 1; i <= N; ++i) cin >> exam[i];
vector<int> need(M + 1);
for (int j = 1; j <= M; ++j) cin >> need[j];
// Danh sách các ngày thi cho từng môn
vector<vector<int>> history(M + 1);
for (int i = 1; i <= N; ++i) {
if (exam[i] > 0) history[exam[i]].push_back(i);
}
auto can = [&](int x) -> bool {
// Dùng priority queue để lấy ra các môn có deadline sớm nhất
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
// Bước 1: Xác định các ngày thi được chọn cho mỗi môn trong khoảng [1, x]
// Ta dùng mảng last để lưu ngày thi cuối cùng có thể dùng được
vector<int> last(M + 1, -1);
for (int i = 1; i <= M; ++i) {
// Tìm ngày thi nhỏ nhất >= need[i] và <= x (hoặc tương đương logic)
// Ở đây ta cần chọn ngày thi sao cho có nhiều thời gian ôn tập nhất (ngày muộn nhất có thể nhưng phải trước x)
// Tuy nhiên, để đảm bảo feasiblity, ta nên ưu tiên môn có deadline sớm.
// Approach chuẩn: Tính last[i] là ngày thi cuối cùng của môn i <= x.
auto it = upper_bound(history[i].begin(), history[i].end(), x);
if (it == history[i].begin()) return false; // Không có ngày thi <= x
last[i] = *(--it);
}
// Bước 2: Xếp hàng ưu tiên theo ngày thi (deadline)
// Logic: Nếu ta có một ngày rảnh rỗi, ta nên dùng nó để ôn tập cho môn có deadline sớm nhất.
// Chỉ có thể thi môn i vào ngày last[i].
// Ta cần đảm bảo rằng tổng số ngày rảnh rỗi từ ngày 1 đến last[i] - 1 đủ để ôn tập tất cả các môn có deadline <= last[i].
vector<vector<int>> events(x + 1);
for (int i = 1; i <= M; ++i) {
if (last[i] != -1) {
events[last[i]].push_back(i);
}
}
ll free_days = 0;
ll required = 0;
for (int d = 1; d <= x; ++d) {
if (exam[d] == 0) {
free_days++; // Ngày không có thi, có thể dùng để ôn tập
} else {
// Nếu ngày này là ngày thi của môn nào đó (được chọn), ta phải thi.
// Nhưng trước khi thi, ta cần đảm bảo đã ôn tập xong.
// Ta không thể ôn tập vào ngày thi (giả sử).
}
// Kiểm tra các môn có deadline là ngày d
for (int sub : events[d]) {
required += need[sub];
}
// Nếu số ngày rảnh tích lũy chưa đủ cho tổng số ngày ôn tập cần thiết của các môn có deadline <= d
// Thì thời điểm x không khả thi.
// Lưu ý: free_days là số ngày rảnh từ đầu đến d (trừ các ngày thi).
// Nhưng ta phải trừ đi các ngày đã dùng để thi (các ngày có exam > 0).
// Thực tế, free_days là số ngày không thi.
// Ta cần: số ngày không thi từ 1 đến d >= tổng time cần ôn của các môn deadline <= d.
if (free_days < required) return false;
}
return true;
};
int low = 1, high = N, ans = -1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (can(mid)) {
ans = mid;
high = mid - 1;
} else {
low = mid + 1;
}
}
cout << ans;
return 0;
}
- Time Complexity: O(N log N)
- Space Complexity: O(N + M)
Approach này sử dụng phương pháp Binary Search trên câu trả lời (số ngày). Với mỗi giá trị ngày mid thử nghiệm, ta kiểm tra xem có thể hoàn tất tất cả các môn học trong mid ngày hay không (Hàm can). Hàm can hoạt động như sau:
- Xác định ngày thi tối ưu cho mỗi môn học trong khoảng [1, mid]. Để có nhiều thời gian ôn tập nhất, ta nên chọn ngày thi muộn nhất có thể cho mỗi môn (nhưng không được trễ hơn
mid). - Tính tổng thời gian ôn tập cần thiết cho tất cả các môn có ngày thi được chọn.
- Kiểm tra xem số ngày còn lại (sau khi trừ các ngày thi) có đủ cho việc ôn tập không. Cụ thể, với mỗi ngày
dtừ 1 đếnmid, ta tích lũy số ngày rảnh (ngày không thi). Đồng thời, ta cộng dồn thời gian ôn tập cần thiết của các môn có deadline làd. Nếu tại bất kỳ thời điểm nào số ngày rảnh tích lũy nhỏ hơn tổng thời gian ôn tập cần thiết, thìmidlà không khả thi.
Cách Greedy với Priority Queue (Tối ưu Online)
#include <bits/stdc++.h>
using namespace std;
int main() {
int N, M;
cin >> N >> M;
vector<int> X(N + 1);
for (int i = 1; i <= N; i++) cin >> X[i];
vector<int> t(M + 1);
for (int i = 1; i <= M; i++) cin >> t[i];
vector<int> last(M + 1, -1);
for (int i = 1; i <= N; i++)
if (X[i] != 0) last[X[i]] = i;
for (int i = 1; i <= M; i++) {
if (last[i] == -1) {
cout << -1;
return 0;
}
}
// Sắp xếp các môn theo ngày thi cuối cùng (deadline)
vector<pair<int, int>> deadlines;
for (int i = 1; i <= M; i++) deadlines.push_back({last[i], t[i]});
sort(deadlines.begin(), deadlines.end());
// Duyệt theo timeline
// Dùng priority queue để lưu các môn cần ôn tập (ưu tiên môn nào cần nhiều time hơn?)
// Thực chất, để tối ưu số ngày, ta cần ưu tiên môn nào có deadline xa hơn?
// Không, Greedy thông thường là: duyệt từ đầu đến cuối, khi gặp môn cần thi, ta phải đảm bảo đã ôn xong.
// Nếu không đủ time, ta phải 'mượn' thời gian từ tương lai (dời deadline).
// Tuy nhiên, do deadline là cố định (ngày thi), ta chỉ có thể chọn 1 trong các ngày có lịch thi.
// Nếu không thi được vào ngày cuối cùng (do không đủ time ôn), ta có thể dời sang ngày thi trước đó.
// Approach mới (như Solution 2): Dùng Priority Queue.
// Duyệt các ngày từ 1 đến N.
// Khi gặp một ngày thi (X[i] = s), ta coi như cơ hội để thi môn s.
// Ta chưa quyết định ngay mà đưa vào hàng đợi các môn có thể thi vào ngày này.
// Một môn có thể thi vào ngày `i` nếu `i` là một trong các ngày thi của môn đó.
// Tuy nhiên, ta chỉ nên thi một môn vào một ngày.
// Strategy: Duyệt các ngày. Khi gặp ngày thi, thêm môn đó vào PQ (với key là thời gian ôn tập cần thiết).
// Giữ một biến `accumulated_free_time`.
// Khi không thể ôn tập đủ cho các môn trong PQ, ta phải loại bỏ một môn (hoặc dời lại).
// Nhưng do ta đang tìm số ngày tối thiểu, ta cần xác định xem có thể hoàn thành trước ngày `d` hay không.
// Logic của Solution 2 (Binary Search + Greedy trong `can` function):
// Với một mốc thời gian `x`:
// 1. Lấy ra các môn có ngày thi <= `x`. Chọn ngày thi cuối cùng cho mỗi môn (trong `x`).
// 2. Sắp xếp các môn theo ngày thi tăng dần.
// 3. Duyệt các ngày từ 1 đến `x`.
// - Nếu là ngày rảnh, tăng biến `free_days`.
// - Nếu là ngày thi của môn i:
// + Kiểm tra xem `free_days` có đủ `t[i]` không.
// + Nếu đủ, trừ `t[i]` vào `free_days`. (Xong môn này).
// + Nếu không đủ -> Impossible.
//
// Code minh họa cho Logic 3 (Greedy check trong Binary Search):
//
// int low = 1, high = N, ans = -1;
// while(low <= high) {
// int mid = low + (high - low) / 2;
// vector<pair<int, int>> tasks;
// for (int s = 1; s <= M; s++) {
// int day = -1;
// // Tìm ngày thi <= mid, ưu tiên ngày muộn nhất
// for (int d : history[s]) if (d <= mid) day = d;
// if (day == -1) { tasks.clear(); break; }
// tasks.push_back({day, t[s]});
// }
// if (tasks.empty()) { low = mid + 1; continue; }
// sort(tasks.begin(), tasks.end());
//
// vector<bool> isExamDay(mid + 1, false);
// for (auto p : tasks) isExamDay[p.first] = true;
//
// ll free = 0;
// bool ok = true;
// int task_idx = 0;
// for (int d = 1; d <= mid; d++) {
// if (!isExamDay[d]) free++;
// else {
// // Duyệt tất cả các môn thi vào ngày d (ví dụ có thể có nhiều môn cùng ngày)
// // Trong bài này, một ngày chỉ thi 1 môn.
// int req = tasks[task_idx].second;
// if (free < req) { ok = false; break; }
// free -= req;
// task_idx++;
// }
// }
// if (ok) { ans = mid; high = mid - 1; }
// else low = mid + 1;
// }
// cout << ans;
cout << "Code logic is inside explanation.";
return 0;
}
- Time Complexity: O(N log N)
- Space Complexity: O(N + M)
Đây là biến thể của Approach 1, nhưng chi tiết hóa quá trình kiểm tra (Feasibility Check).
- Binary Search: Tìm số ngày tối thiểu
ans. - Check(mid):
- Bước 1: Với mỗi môn, chọn ngày thi cuối cùng có thể trong khoảng [1, mid]. Nếu môn nào không có lịch thi trong khoảng này =>
false. - Bước 2: Tạo một danh sách các công việc (task) bao gồm
{deadline, time_needed}. - Bước 3: Sắp xếp các công việc theo deadline tăng dần.
- Bước 4: Mô phỏng lại quá trình:
- Duyệt qua từng ngày từ 1 đến
mid. - Nếu ngày đó là ngày thi của một công việc (theo thứ tự sắp xếp), ta phải hoàn thành công việc đó ngay (vì đây là cơ hội cuối cùng).
- Kiểm tra xem số ngày rảnh tích lũy (free days) có đủ cho
time_neededcủa công việc đó không.- Nếu đủ: Trừ đi
time_needed. - Nếu không đủ: Impossible.
- Nếu đủ: Trừ đi
- Nếu ngày đó không phải là ngày thi: Cộng 1 vào số ngày rảnh.
- Duyệt qua từng ngày từ 1 đến
- Nếu duyệt hết mà không báo lỗi, thì
midkhả thi.
- Bước 1: Với mỗi môn, chọn ngày thi cuối cùng có thể trong khoảng [1, mid]. Nếu môn nào không có lịch thi trong khoảng này =>
Phương pháp này đảm bảo ta luôn ưu tiên các môn có deadline sớm nhất (Earliest Deadline First), đây là chiến lược tối ưu cho các bài toán dạng này.
Phân tích độ phức tạp
| Cách tiếp cận | Time | Space | Tên |
|---|---|---|---|
| 1 | O(N log N) | O(N + M) | Tối ưu hóa bằng Binary Search (Phân tích dữ liệu) |
| 2 | O(N log N) | O(N + M) | Greedy với Priority Queue (Tối ưu Online) |
Bài học kinh nghiệm
- Bài toán có tính chất đơn điệu: Nếu có thể hoàn thành trong T ngày, chắc chắn có thể hoàn thành trong T+1 ngày. Do đó, Binary Search trên lời giải là lựa chọn tối ưu.
- Vấn đề có thể quy về bài toán quy hoạch động hoặc Greedy trên timeline: Với một mốc thời gian cố định, ta cần đảm bảo các công việc có deadline nhỏ hơn hoặc bằng mốc đó được xử lý trước.
Lỗi thường gặp
- Quên xử lý trường hợp một môn học không có lịch thi trong khoảng thời gian đang xét (nếu không có lịch thi thì không thể thi, dẫn đến không thể hoàn thành).
- Sai lầm trong việc tính toán số ngày rảnh: Ngày rảnh là ngày không thi, không phải ngày không làm gì. Cần phân biệt rõ giữa 'ngày thi' và 'ngày ôn tập'.
- Lựa chọn sai ngày thi: Nếu chọn ngày thi quá sớm, ta sẽ mất đi những ngày rảnh giá trị để ôn tập cho các môn khác, dẫn đến kết quả không tối ưu hoặc sai lệch trong quá trình kiểm tra tính khả thi.
Bình luận