Trả tiền ở chợ, ai cũng làm theo phản xạ: lấy tờ lớn nhất còn vừa số tiền, rồi lặp lại. Mỗi bước chọn cái có vẻ tốt nhất lúc ấy, chọn xong không đổi ý. Lối giải đó gọi là tham lam (greedy). Khi đúng, nó gọn tới bất ngờ: thường chỉ là một lần sắp xếp và một vòng lặp, thay cho việc thử khả năng.
Cái khó là biết khi nào nó đúng, vì nhiều quy tắc nghe rất hợp lý lại sai ở một trường hợp nhỏ. Bài này dạy ba việc: tìm phản ví dụ để loại quy tắc sai, chứng minh quy tắc còn lại bằng lập luận đổi chỗ, và các mẫu tham lam hay gặp.
Lấy tờ lớn nhất
Có các mệnh giá với , mỗi mệnh giá dùng bao nhiêu tờ cũng được. Cần trả đúng đồng bằng ít tờ nhất. Quy tắc tham lam chỉ có một dòng: lấy tờ lớn nhất không vượt số tiền còn phải trả, rồi lặp lại. Với các mệnh giá và , ta lấy , hai tờ , một tờ , hai tờ : 6 tờ.
Quy tắc gọn, nhưng gọn chưa phải đúng. Với các mệnh giá và , tham lam lấy , tức 3 tờ, trong khi chỉ cần 2 tờ. Một phản ví dụ nhỏ bác bỏ cả quy tắc, nên tìm phản ví dụ là việc đầu tiên mỗi khi nghĩ ra một lời giải tham lam; chứng minh chỉ đáng viết cho quy tắc đã sống sót qua vài lần thử.
Một điều kiện đủ để quy tắc đúng: mỗi mệnh giá là bội của mệnh giá nhỏ hơn kề nó, . Chứng minh. Một cách trả tối ưu dùng ít hơn tờ với mỗi , vì tờ ấy đổi được lấy một tờ . Do đó các tờ nhỏ hơn gánh tổng không quá
nên số tờ phải đúng bằng thương nguyên S / c_k, chính là số tờ tham lam lấy. Phần còn lại S % c_k là bài toán cùng dạng với ít mệnh giá hơn.
Chứng minh này lộ ra hai mệnh đề đứng sau mọi lời giải tham lam đúng. (A) Có một nghiệm tối ưu chứa lựa chọn tham lam. (B) Sau khi chọn, phần còn lại là bài toán cùng dạng và nhỏ hơn. Công sức thường nằm ở (A). Điều kiện chia hết chỉ là điều kiện đủ: hệ cũng cho tham lam đúng dù không chia hết .
Hỏi nhanh
Máy rút tiền dùng quy tắc “lấy tờ lớn nhất còn vừa số tiền cần trả”. Với hệ mệnh giá nào dưới đây quy tắc này có thể cho kết quả không tối ưu?
Ba hệ còn lại có mỗi mệnh giá là bội của mệnh giá nhỏ hơn kề nó, nên tham lam đúng. Hệ sai ở : tham lam lấy (3 tờ) trong khi chỉ có 2 tờ.
Ví dụRút Tiền Gọn Nhất
Đề. Máy chỉ có các mệnh giá . Phát đúng đồng bằng ít tờ nhất.
Lời giải. Kiểm từng cặp mệnh giá kề nhau: , , , . Mỗi mệnh giá là bội của mệnh giá kề dưới, nên theo chứng minh trên tham lam đúng. Duyệt mệnh giá từ lớn xuống, lấy n / c tờ rồi giữ phần dư.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, c[] = {100, 20, 10, 5, 1};
signed main() {
cin >> n;
ll ans = 0;
for (ll i = 0; i < 5; i++) {
ans += n / c[i];
n %= c[i];
}
cout << ans << '\n';
}
286
8
Chọn hoạt động
Có hoạt động, hoạt động diễn ra từ đến . Mỗi lúc chỉ tham dự được một hoạt động và phải tham dự trọn vẹn; hoạt động mới được bắt đầu đúng lúc hoạt động trước kết thúc. Tham dự được nhiều nhất bao nhiêu hoạt động?
Chưa biết quy tắc nào đúng, ta nêu vài quy tắc rồi tìm phản ví dụ cho từng cái. Ba ứng viên: chọn hoạt động bắt đầu sớm nhất, ngắn nhất, hay kết thúc sớm nhất; chọn xong thì loại những hoạt động chồng lên nó.
Hoạt động bắt đầu sớm nhất có thể kéo dài tới mức chặn mọi hoạt động khác. Hoạt động ngắn nhất có thể vắt ngang hai hoạt động vốn ghép được với nhau: chặn cả lẫn . Quy tắc kết thúc sớm nhất thì không tìm được phản ví dụ nào.
Chứng minh bằng đổi chỗ. Gọi là hoạt động kết thúc sớm nhất, là một cách chọn tối ưu bất kỳ và là hoạt động đầu tiên trong . Vì kết thúc không muộn hơn , mọi hoạt động khác của bắt đầu không sớm hơn lúc kết thúc. Thay bằng cho một cách chọn hợp lệ, cùng số hoạt động, nay chứa : đó là (A). Sau khi chọn , các hoạt động còn dùng được là những hoạt động bắt đầu không sớm hơn lúc kết thúc: bài toán cùng dạng, tức (B).
Chứng minh chặt chẽ tốn thời gian, mà trong phòng thi bạn không có thừa. Cách rẻ nhất để biết một quy tắc có sai không là viết thêm vét cạn cho dữ liệu rất nhỏ rồi chạy cả hai trên vài chục nghìn test ngẫu nhiên. Chương trình dưới kiểm lại cả ba quy tắc trên, gồm bốn phần:
key(i)là khóa sắp xếp của quy tắc đang thử: thời điểm kết thúc (rule = 0), bắt đầu (rule = 1) hay độ dài (rule = 2).cmpso hai chỉ số theo khóa đó.over(i, j)cho biết hai hoạt động có chồng nhau không: phần chung có độ dài dương khi mỗi hoạt động bắt đầu trước lúc hoạt động kia kết thúc. Dấu<chặt cho phép hoạt động sau bắt đầu đúng lúc hoạt động trước kết thúc.greedy()sắp chỉ số theo khóa rồi đi lần lượt, nhận hoạt động nào không chồng với mọi hoạt động đã nhận.brute()thử mọi tập con của tối đa 7 hoạt động và giữ tập lớn nhất không có cặp nào chồng nhau.mainsinh 20000 bộ test ngẫu nhiên với và thời điểm trong , đếm số test mà hai bên cho kết quả khác nhau.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll n, rule, s[10], e[10], ord[10], pick[10];
ll key(ll i) {
if (rule == 0) return e[i];
if (rule == 1) return s[i];
return e[i] - s[i];
}
bool cmp(ll i, ll j) {
return key(i) < key(j);
}
bool over(ll i, ll j) {
return s[i] < e[j] && s[j] < e[i];
}
ll greedy() {
for (ll i = 0; i < n; i++) ord[i] = i;
stable_sort(ord, ord + n, cmp);
ll m = 0;
for (ll k = 0; k < n; k++) {
bool ok = true;
for (ll t = 0; t < m; t++)
if (over(ord[k], pick[t])) ok = false;
if (ok) pick[m++] = ord[k];
}
return m;
}
ll brute() {
ll best = 0;
for (ll mask = 0; mask < (1 << n); mask++) {
bool ok = true;
for (ll i = 0; i < n; i++)
for (ll j = i + 1; j < n; j++)
if ((mask >> i & 1) && (mask >> j & 1) && over(i, j)) ok = false;
if (ok) best = max(best, (ll)__builtin_popcount(mask));
}
return best;
}
signed main() {
mt19937 rng(2024);
string name[] = {"kết thúc sớm nhất", "bắt đầu sớm nhất", "ngắn nhất"};
for (rule = 0; rule < 3; rule++) {
ll wrong = 0;
for (ll t = 1; t <= 20000; t++) {
n = rng() % 7 + 1;
for (ll i = 0; i < n; i++) {
s[i] = rng() % 9 + 1;
e[i] = s[i] + rng() % (10 - s[i]) + 1;
}
if (greedy() != brute()) wrong++;
}
cout << name[rule] << ": sai " << wrong << "/20000 test\n";
}
}
kết thúc sớm nhất: sai 0/20000 test
bắt đầu sớm nhất: sai 4709/20000 test
ngắn nhất: sai 102/20000 test
Quy tắc đúng không bị bắt lần nào, hai quy tắc sai bị bắt hàng trăm, hàng nghìn lần. Không bị bắt chưa phải là chứng minh, nhưng với test đủ nhỏ và đủ nhiều thì lỗi hiếm khi sống sót: phản ví dụ của quy tắc sai thường chỉ cần ba hoạt động.
Hỏi nhanh
Bây giờ mỗi hoạt động có thêm một điểm thưởng, và ta muốn tổng thưởng lớn nhất của các hoạt động không chồng nhau. Bạn vẫn sắp theo thời điểm kết thúc và nhận hoạt động nếu nó không chồng với hoạt động đã nhận. Điều nào đúng?
Lấy thưởng 1, thưởng 1 và thưởng 10: sắp theo kết thúc nhận hai hoạt động đầu, tổng 2, trong khi riêng được 10. Chứng minh đổi chỗ dùng ngầm rằng thay bằng không mất gì, chỉ đúng khi các hoạt động có giá trị như nhau. Sắp theo thưởng giảm dần cũng sai: thưởng 10 cùng và thưởng 6 mỗi cái. Bài có thưởng giải bằng quy hoạch động (bài 12).
Ví dụChọn Ca Tham Dự
Đề. hoạt động , được bắt đầu đúng lúc hoạt động trước kết thúc. Tham dự trọn vẹn được nhiều nhất bao nhiêu hoạt động?
Lời giải. Sắp theo thời điểm kết thúc tăng dần, giữ last là thời điểm kết thúc của hoạt động nhận gần nhất; nhận hoạt động nếu last. Dấu >= vì đề cho bắt đầu đúng lúc kết thúc; nếu chạm đầu mút cũng tính là chồng thì phải dùng >. Sắp các cặp để sort mặc định xếp theo kết thúc. Cả bài .
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n;
pair<ll, ll> p[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> p[i].second >> p[i].first;
sort(p + 1, p + n + 1);
ll last = 0, ans = 0;
for (ll i = 1; i <= n; i++)
if (p[i].second >= last) {
ans++;
last = p[i].first;
}
cout << ans << '\n';
}
6
1 4
3 5
4 7
5 6
6 9
7 8
3
Đổi chỗ hai việc kề
Nhiều bài tham lam không hỏi chọn gì mà hỏi làm theo thứ tự nào. Có thứ tự, nhưng lời giải thường là sắp theo một khóa. Muốn tìm khóa, xét đúng hai phần tử đứng kề nhau và tính xem đổi chỗ chúng làm kết quả thay đổi bao nhiêu.
Bài toán: việc làm lần lượt trên một máy từ thời điểm . Việc cần đơn vị thời gian, có trọng số và hoàn thành lúc . Chọn thứ tự để tổng nhỏ nhất.
Giả sử hai việc (cần , trọng số ) và (cần , trọng số ) đứng kề nhau, việc đứng trước bắt đầu lúc . Dù thứ tự nào, cặp này cũng xong cùng lúc , nên các việc khác giữ nguyên thời điểm. đứng trước thì cặp đóng góp ; đứng trước thì . Hiệu hai biểu thức, phần chứa triệt tiêu, còn
Hiệu không phụ thuộc , nên đúng ở mọi chỗ trong lịch: nên đứng trước khi . Khóa là , nhỏ đứng trước; khi mọi khóa là , việc ngắn làm trước. Nếu một thứ tự tối ưu chưa sắp theo khóa thì có hai việc kề nhau đứng ngược khóa; đổi chỗ chúng không làm tổng tăng, nên ta tới được thứ tự đã sắp mà vẫn tối ưu.
bool cmp(Job x, Job y) {
return x.p * y.w < y.p * x.w;
}
So sánh bằng nhân chéo để khỏi chia, và dùng < chứ không dùng <=: hai việc cùng khóa thì cả hai chiều đều phải trả false. Cùng cách nghĩ cho bài hạn chót: nếu một lịch hợp lệ có ngay trước mà hạn của muộn hơn, đổi chỗ thì xong sớm hơn, còn xong đúng lúc từng xong, vẫn kịp. Vậy sắp theo hạn chót tăng dần không làm hỏng lịch nào.
Hỏi nhanh
Hai việc đứng kề nhau: cần 1 đơn vị thời gian với trọng số 1, cần 10 đơn vị với trọng số 100. Mục tiêu là tổng nhỏ nhất. Nên cho việc nào đứng trước, và vì sao?
Mức tăng khi đứng trước là , không chứa . Khóa của là , của là , nên đứng trước. “Việc ngắn làm trước” chỉ đúng khi trọng số bằng nhau. Cặp việc xong cùng lúc, nhưng từng việc thì không.
Ví dụTổng Thưởng Hoàn Thành
Đề. việc làm lần lượt từ thời điểm , việc cần và có mốc . Hoàn thành lúc thì được thưởng (có thể âm). Phải làm tất cả; tìm tổng thưởng lớn nhất.
Lời giải. Tổng thưởng là tổng các trừ tổng các . Tổng các không phụ thuộc thứ tự, nên bài thành cực tiểu hóa tổng thời điểm hoàn thành: trường hợp mọi trọng số bằng 1 của mục này, làm việc ngắn trước. Thời điểm tới , tổng tới cỡ , cần long long.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 2e5 + 5;
ll n;
pair<ll, ll> job[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 1; i <= n; i++) cin >> job[i].first >> job[i].second;
sort(job + 1, job + n + 1);
ll now = 0, ans = 0;
for (ll i = 1; i <= n; i++) {
now += job[i].first;
ans += job[i].second - now;
}
cout << ans << '\n';
}
4
3 10
1 7
2 9
4 20
26
Cái túi chia nhỏ
Có món, món nặng và giá trị ; túi chứa tối đa . Ở bản chia nhỏ được, ta được lấy một phần của món. Quy tắc: xếp các món theo giảm dần, lấy trọn món nếu còn chỗ, hết chỗ thì lấy phần vừa đủ.
Chứng minh đổi chỗ: gọi món là món có tỉ lệ cao nhất. Nếu một nghiệm tối ưu chưa lấy hết món thì túi đã đầy (còn chỗ thì thêm món chỉ có lợi), nên nó đang chứa một lượng món khác. Đổi một lượng đủ nhỏ sang món thì giá trị thay đổi
Giữ nguyên dữ liệu nhưng cấm cắt món, tức bản nguyên khối (0/1), thì quy tắc vỡ. Với túi và các món , lấy theo tỉ lệ được và , tổng , rồi không món nào vừa chỗ còn lại; trong khi cùng cho . Chứng minh vỡ đúng ở bước đổi một lượng , vì món không cắt được. Túi nguyên khối giải bằng quy hoạch động (bài 12).
Xâu nhỏ nhất
Nhiều bài hỏi xâu nhỏ nhất (hay lớn nhất) theo thứ tự từ điển. Thứ tự từ điển quyết định ở vị trí khác nhau đầu tiên, nên chữ ở vị trí quan trọng hơn mọi chữ phía sau cộng lại. Tham lam tự nhiên là dựng kết quả từ trái sang phải, mỗi vị trí đặt chữ nhỏ nhất còn đặt được mà không làm hỏng khả năng hoàn thành phần sau.
Ví dụ: xóa đúng chữ số của xâu để xâu còn lại nhỏ nhất. Nếu chữ số đứng ngay trước chữ số nhỏ hơn nó, xóa thì tiến lên đúng vị trí của và xâu nhỏ đi ngay ở đó. Một ngăn xếp (stack: như chồng đĩa, chỉ thêm vào và lấy ra ở trên cùng, gọi là đỉnh; bài 09) làm cả việc trong một lượt: với mỗi chữ số , chừng nào còn lượt xóa và đỉnh lớn hơn thì bỏ đỉnh, rồi đẩy vào; hết xâu mà còn lượt thì bỏ các chữ số cuối.
string st;
for (char c : s) {
while (k > 0 && !st.empty() && st.back() > c) {
st.pop_back();
k--;
}
st.push_back(c);
}
st.resize(st.size() - k);
Với và , ngăn xếp lần lượt là 3, 1, 14, 11, 115, 1159, 1152, 11526. Mỗi chữ số được đẩy vào và lấy ra tối đa một lần, cả bài .
Hỏi nhanh
Cho và (xóa đúng một chữ số để được số nhỏ nhất). Đáp án là gì?
Chữ số đứng ngay trước nhỏ hơn nó; xóa thì tiến lên đúng vị trí của , và số nhỏ đi ngay ở chữ số thứ hai. Xóa chữ số lớn nhất là quy tắc tự nhiên nhưng sai: nó chỉ cắt bớt đuôi, trong khi chữ số quan trọng hơn nằm bên trái.
Ví dụTrộn Hai Luồng Chữ Số
Đề. Xâu chữ số , tổng độ dài các test không quá . Được đổi chỗ hai chữ số kề nhau nếu một chẵn một lẻ, bao nhiêu lần tùy ý. Tìm xâu nhỏ nhất theo thứ tự từ điển.
Lời giải. Hai chữ số cùng chẵn lẻ không bao giờ vượt qua nhau, nên thứ tự của các chữ số chẵn và của các chữ số lẻ được giữ nguyên; ngược lại, mọi cách trộn hai dãy ấy đều đạt được. Bài thành: trộn dãy chữ số chẵn và dãy chữ số lẻ (mỗi dãy giữ thứ tự) để được xâu nhỏ nhất. Xây từ trái sang phải: ở mỗi bước lấy chữ nhỏ hơn trong hai chữ đứng đầu và . Hai chữ đầu khác chẵn lẻ nên không bao giờ bằng nhau, lựa chọn luôn rõ ràng; giống bước trộn của sắp xếp trộn.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll t;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
string s, ev, od, ans;
cin >> s;
for (char c : s) {
if ((c - '0') % 2 == 0) ev += c;
else od += c;
}
ll i = 0, j = 0;
while (i < (ll)ev.size() && j < (ll)od.size()) {
if (ev[i] < od[j]) ans += ev[i++];
else ans += od[j++];
}
ans += ev.substr(i) + od.substr(j);
cout << ans << '\n';
}
}
3
7092
86420
135024
0279
86420
012345
Chi phí trừ tiết kiệm
Một dạng tham lam hay gặp: tính chi phí khi chưa làm gì, rồi xem mỗi lựa chọn cho phép tiết kiệm bao nhiêu. Nếu các khoản tiết kiệm độc lập với nhau (chọn khoản này không làm khoản kia đổi giá trị) và chỉ được chọn tối đa khoản, thì chọn khoản lớn nhất là tối ưu: một cách chọn bỏ sót khoản mà lấy khoản thì đổi lấy chỉ có lợi. Việc khó là biến đề về dạng “các khoản độc lập”.
Ví dụ: hành lang có các ô cần che ở vị trí , dùng không quá tấm chắn, mỗi tấm phủ một đoạn ô liên tiếp; tổng độ dài nhỏ nhất là bao nhiêu? Một tấm duy nhất phủ từ đến . Mỗi tấm thêm cho phép bỏ trống một khoảng giữa hai ô kề nhau, tiết kiệm ô. Các khoảng rời nhau nên các khoản độc lập: bỏ khoảng lớn nhất. Với và , một tấm tốn , bỏ hai khoảng và còn .
Hàng đợi ưu tiên
Có những bài mà lựa chọn tham lam ở mỗi bước làm thay đổi tập ứng viên: chọn xong thì có phần tử mới sinh ra, hoặc phần tử mới tới theo thời gian. Khi đó sắp một lần là không đủ; ta cần một cấu trúc luôn trả lại phần tử nhỏ nhất (hay lớn nhất) hiện có. Đó là hàng đợi ưu tiên (priority_queue): push, top, pop mỗi thao tác (bài 09 nói kỹ hơn). Mặc định nó nhả phần tử lớn nhất; khai báo thêm greater<ll> thì nhả phần tử nhỏ nhất.
Ví dụCắt Thanh Theo Danh Sách
Đề. Thanh dài cần cắt thành đúng đoạn dài . Mỗi lần cắt một đoạn thành hai, chi phí bằng độ dài đoạn trước khi cắt. Tìm tổng chi phí nhỏ nhất.
Lời giải. Chạy ngược thời gian: bắt đầu với đoạn , mỗi bước ghép hai đoạn thành một với chi phí bằng tổng của chúng, tới khi còn một thanh. Quy tắc: luôn ghép hai đoạn ngắn nhất đang có. Với : ghép (chi phí ), rồi (chi phí ), rồi (chi phí ), tổng .
Vì sao đúng: vẽ các lần ghép thành sơ đồ như hình, mỗi đoạn ban đầu là một nút ở dưới cùng, mỗi lần ghép nối hai nút thành một nút mới phía trên. Đoạn nằm trong lần ghép thì độ dài của nó bị tính đúng lần, nên tổng chi phí là . Trong một cách ghép tối ưu, đổi chỗ đoạn ngắn nhất với đoạn nằm trong nhiều lần ghép nhất không làm tổng tăng, nên có cách ghép tối ưu mà hai đoạn ngắn nhất được ghép với nhau ngay từ lần đầu: đó là (A). Coi hai đoạn đó là một đoạn bằng tổng của chúng thì được bài cùng dạng ít hơn một đoạn: (B). Đây chính là mã Huffman. Mỗi lần ghép tốn tới mà có tới lần, nên dùng long long.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll x, n;
priority_queue<ll, vector<ll>, greater<ll>> pq;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> x >> n;
for (ll i = 1; i <= n; i++) {
ll d;
cin >> d;
pq.push(d);
}
ll ans = 0;
while (pq.size() > 1) {
ll a = pq.top();
pq.pop();
ll b = pq.top();
pq.pop();
ans += a + b;
pq.push(a + b);
}
cout << ans << '\n';
}
10 4
1 2 3 4
19
Khi vòng lặp không chạy và ta in : thanh đã đúng độ dài cần. Quy tắc này khác với ghép dây chuyền từ ngắn tới dài: với dây chuyền tốn , còn ghép hai đoạn ngắn nhất cho .
multiset cũng lấy được phần tử nhỏ nhất trong , nhưng mỗi phần tử là một nút cấp phát riêng trong cây, còn priority_queue chỉ là một mảng liền:
Cùng độ phức tạp, priority_queue nhanh hơn khoảng 4 lần. Chỉ cần lấy phần tử nhỏ nhất hay lớn nhất thì dùng nó; multiset để dành cho lúc cần xóa một phần tử bất kỳ hay tìm phần tử kế tiếp.
Khi đi thi
- Nghĩ ra quy tắc thì thử ngay trên vài input 3 đến 5 phần tử, nhất là khi có giá trị bằng nhau hoặc một phần tử cực đoan; còn nghi ngờ thì viết vét cạn so sánh trên test nhỏ trước khi nộp.
- Bài xếp thứ tự: rút khóa từ hiệu hai thứ tự của hai phần tử kề nhau, sắp bằng so sánh nhân chéo trong
long long, dùng<chứ không<=; tổng thời điểm và tổng chi phí thường vượtint. - Ràng buộc nguyên khối (món không cắt được, hệ tiền lạ, hoạt động có trọng số) là dấu hiệu tham lam sắp sai: chuyển sang quy hoạch động (bài 12) thay vì sửa quy tắc mãi.
Tóm tắt
- Tham lam chọn phương án tốt nhất lúc này và không đổi ý; nó đúng khi có (A) một nghiệm tối ưu chứa lựa chọn đó và (B) phần còn lại là bài toán cùng dạng nhỏ hơn.
- Tìm quy tắc bằng cách nêu vài ứng viên ở các trường hợp cực đoan rồi tìm phản ví dụ nhỏ; chỉ chứng minh quy tắc còn sống, và kiểm bằng vét cạn khi chưa chắc.
- Chứng minh đổi chỗ: thay một phần tử của nghiệm tối ưu bằng lựa chọn tham lam mà không xấu đi; với bài thứ tự, đổi hai phần tử kề nhau để rút ra khóa.
- Các mẫu: kết thúc sớm nhất cho bài chọn đoạn, cho lập lịch, cho túi chia nhỏ, chi phí gốc trừ khoản tiết kiệm lớn nhất, ghép hai nhỏ nhất bằng
priority_queue, ngăn xếp cho xâu nhỏ nhất. - Bài có ràng buộc nguyên khối hay trọng số thường không tham lam được.
Bài tập
Đọc thêm
- Tham lamVNOI Wiki
- Greedy with SortingUSACO Guide
- 贪心OI Wiki
- Competitive Programmer's Handbook, chương 6
- Johnson's rulecp-algorithms