Thuật Toán Tham Lam (Greedy)
1. Khái niệm & bản chất của lựa chọn tối ưu cục bộ
Thuật toán Tham lam (Greedy Algorithm) là chiến lược giải quyết bài toán tối ưu bằng cách thực hiện một chuỗi các lựa chọn tối ưu cục bộ (locally optimal choice) ở từng bước, với hy vọng dẫn đến nghiệm tối ưu toàn cục (globally optimal solution) mà không cần phải quay lui (backtracking) hay tính toán lại các trạng thái trước đó.
Để một bài toán giải được bằng thuật toán tham lam, nó bắt buộc phải thỏa mãn 2 điều kiện toán học khắt khe:
- Tính chất lựa chọn tham lam (Greedy Choice Property): Tồn tại ít nhất một nghiệm tối ưu toàn cục chứa lựa chọn tham lam đầu tiên.
- Cấu trúc con tối ưu (Optimal Substructure): Sau khi thực hiện lựa chọn tham lam, bài toán thu hẹp về một bài toán con đồng dạng có quy mô nhỏ hơn mà việc giải bài toán con đó cũng dẫn đến tối ưu toàn cục.
2. Các mô hình bài toán tham lam
2.1. Mô hình 1: Lựa chọn khoảng không giao nhau nhiều nhất (Interval Scheduling)
Cho $N$ sự kiện, mỗi sự kiện diễn ra trong khoảng thời gian $[L_i, R_i]$. Hãy chọn số lượng sự kiện nhiều nhất sao cho không có hai sự kiện nào bị trùng lấn thời gian.
- Chiến lược tham lam đúng đắn: Luôn ưu tiên chọn sự kiện có thời điểm kết thúc sớm nhất ($R_i$ nhỏ nhất).
- Chứng minh đổi chỗ (Exchange Argument): Giả sử tồn tại một phương án tối ưu $OPT$ không chọn sự kiện $k$ kết thúc sớm nhất mà chọn sự kiện $x$ kết thúc muộn hơn ($R_x > R_k$). Nếu ta thay thế sự kiện $x$ bằng sự kiện $k$, sự kiện $k$ kết thúc sớm hơn nên khoảng thời gian còn lại sau $k$ sẽ rộng hơn hoặc bằng khoảng thời gian sau $x$, do đó không làm ảnh hưởng đến bất kỳ sự kiện nào chọn sau đó $\implies$ Phương án mới sau khi đổi chỗ có số lượng sự kiện ít nhất bằng $OPT$.
bool cmp(const vector<long long> &a, const vector<long long> &b) {
return a[1] < b[1]; // Sắp xếp theo thời điểm kết thúc tăng dần
}
int max_events(vector<vector<long long>> &events) {
sort(events.begin(), events.end(), cmp);
int count = 0;
long long last_end = -1e18;
for (const auto &e : events) {
if (e[0] >= last_end) { // Nếu thời điểm bắt đầu >= thời điểm kết thúc của sự kiện trước
count++;
last_end = e[1];
}
}
return count;
}
3. Mẫu cài đặt chuẩn thi đấu: Tham lam xếp hàng phục vụ (SJF — Shortest Job First)
Bài toán: Có $N$ khách hàng, khách hàng thứ $i$ cần thời gian phục vụ là $T_i$. Tìm thứ tự phục vụ để tổng thời gian chờ đợi của tất cả khách hàng là nhỏ nhất.
- Chiến lược: Khách hàng có thời gian phục vụ ngắn nhất đứng đầu tiên.
- Công thức tổng thời gian chờ: $\sum_{i=0}^{N-1} (N - 1 - i) \times T_i$.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<long long> t(n);
for (int i = 0; i < n; ++i) cin >> t[i];
sort(t.begin(), t.end()); // Sắp xếp tăng dần
long long total_wait_time = 0;
long long current_time = 0;
for (int i = 0; i < n; ++i) {
total_wait_time += current_time;
current_time += t[i];
}
cout << total_wait_time << "\n";
return 0;
}
4. Ranh giới áp dụng: Khi nào dùng Greedy vs Quy hoạch động (DP)?
| Bài Toán | Dùng Tham Lam (Greedy) Khi Nào? | Buộc Phải Dùng Quy Hoạch Động (DP) Khi Nào? |
|---|---|---|
| Cái túi (Knapsack) | Các đồ vật có thể chia nhỏ (Fractional Knapsack) $\implies$ Sắp xếp theo đơn giá giá trị/khối lượng $V_i / W_i$ giảm dần. | Các đồ vật nguyên vẹn không được chia nhỏ (0/1 Knapsack) $\implies$ Buộc dùng DP $\mathcal{O}(NW)$. |
| Đổi tiền (Coin Change) | Hệ mệnh giá là hệ chính quy (Canonical / bội số như $1, 2, 5, 10, 20$). | Hệ mệnh giá tùy ý (ví dụ: mệnh giá $1, 3, 4$ với $S = 6$, Greedy chọn $4+1+1=3$ tờ, nhưng tối ưu là $3+3=2$ tờ). |