Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Bài 18: Thuật toán tham lam trong C++ và Python: đổi tiền, chọn hoạt động, xếp lịch và khi nào tham lam sai

Bài 18 – Thuật toán tham lam
Series: Thuật toán cơ bản Độ khó: Trung bình Bài 18 / Thuật toán cơ bản Tham lam
// Thuật toán cơ bản · Bài 18
Thuật toán tham lam: đổi tiền, chọn hoạt động, xếp lịch và cách nhận biết khi nào tham lam sai
Mỗi bước, chọn phương án tốt nhất trước mắt và không bao giờ quay lại sửa. Đơn giản, nhanh, nhưng chỉ đúng khi bài toán có cấu trúc phù hợp, và em phải biết kiểm tra điều đó.

Lời mở đầu

Đi siêu thị trả tiền thối 380 nghìn, cô thu ngân sẽ đưa ngay một tờ 200, một tờ 100, một tờ 50, một tờ 20 và một tờ 10: luôn chọn tờ lớn nhất còn dùng được. Không ai ngồi thử mọi cách kết hợp các tờ tiền, vậy mà kết quả luôn là ít tờ nhất. Cách nghĩ “mỗi bước chọn cái tốt nhất ngay lúc đó” gọi là tham lam (greedy).

Tham lam là chiến lược có code ngắn nhất và chạy nhanh nhất (thường chỉ là sắp xếp rồi duyệt một lượt, $O(n \log n)$). Nhưng nó cũng là chiến lược dễ sai nhất: rất nhiều bài trông như tham lam được, thật ra lại không. Bài này dạy em cả hai mặt: những bài tham lam kinh điển, và cách tìm phản ví dụ để không bị “lừa”.

Trong bài này bạn sẽ học: tư tưởng tham lam và hai câu hỏi cần trả lời trước khi dùng; bài đổi tiền và phản ví dụ khi mệnh giá “xấu”; bài chọn nhiều hoạt động nhất (sắp xếp theo thời điểm kết thúc); bài xếp hàng giảm thời gian chờ và lập luận đổi chỗ để chứng minh; cái túi phân số và vì sao cái túi 0/1 không tham lam được. Có khung chạy tay, mô phỏng chọn hoạt động trên trục thời gian và 8 bài tập kèm gợi ý.

Lộ trình bài viết, bấm vào từng phần để nhảy tới:

Phần 1Tham lam là gì?

Một thuật toán tham lam xây dựng lời giải từng bước. Ở mỗi bước, nó chọn phương án có vẻ tốt nhất ngay lúc đó theo một tiêu chí nào đó (lớn nhất, nhỏ nhất, kết thúc sớm nhất…), chốt luôn lựa chọn ấy và đi tiếp, không bao giờ quay lại xem xét lại. So với quay lui (bài 15) thử mọi nhánh, tham lam chỉ đi đúng một nhánh duy nhất của cây, nên cực nhanh.

Hai câu hỏi trước khi tham lam
1. Tiêu chí chọn là gì? Cùng một bài có thể tham lam theo nhiều kiểu (chọn hoạt động ngắn nhất? bắt đầu sớm nhất? kết thúc sớm nhất?), thường chỉ một kiểu đúng.
2. Vì sao lựa chọn đó không làm mất lời giải tối ưu? Nếu chưa trả lời được, hãy thử tìm phản ví dụ nhỏ: viết thêm một chương trình vét cạn chắc chắn đúng, sinh nhiều test nhỏ ngẫu nhiên và so sánh (kỹ thuật “chấm trâu” ở bài 22). Chạy vài trăm test không thấy khác nhau thì mới yên tâm nộp.

Phần 2Bài toán đổi tiền

Bài toán: có các mệnh giá tiền $c_1 > c_2 > \dots > c_m$ (mỗi loại không giới hạn số tờ). Cần trả đúng số tiền $N$ với ít tờ nhất. Tham lam: lấy càng nhiều tờ mệnh giá lớn nhất càng tốt, phần dư chuyển sang mệnh giá tiếp theo.

Code C++ và Python
#include <bits/stdc++.h>
using namespace std;

int main(){
    int m; long long N;
    cin >> m >> N;
    vector<long long> c(m);
    for(auto &v : c) cin >> v;         // mệnh giá giảm dần
    long long soTo = 0;
    for(int i = 0; i < m; i++){
        long long k = N / c[i];
        soTo += k;
        N -= k * c[i];
    }
    if(N == 0) cout << soTo;
    else cout << -1;
    return 0;
}
m, N = map(int, input().split())
c = list(map(int, input().split()))
so_to = 0
for v in c:
    k = N // v
    so_to += k
    N -= k * v
print(so_to if N == 0 else -1)
C++▶ Chạy tay từng dòng codeĐổi tiền tham lam: với mỗi mệnh giá từ lớn tới nhỏ, lấy nhiều tờ nhất có thể. Thử ví dụ thứ ba để thấy tham lam trả về 3 tờ trong khi chỉ cần 2.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int m; long long N;
6 cin >> m >> N;
7 vector<long long> c(m);
8 for(auto &v : c) cin >> v; // mệnh giá giảm dần
9 long long soTo = 0;
10 for(int i = 0; i < m; i++){
11 long long k = N / c[i];
12 soTo += k;
13 N -= k * c[i];
14 }
15 if(N == 0) cout << soTo;
16 else cout << -1;
17 return 0;
18}
Phản ví dụ: tham lam không phải lúc nào cũng đúng
Với mệnh giá $\{4, 3, 1\}$ và $N = 6$: tham lam lấy $4 + 1 + 1$ (3 tờ), nhưng tối ưu là $3 + 3$ (2 tờ). Tờ 4 “ngon” trước mắt lại làm hỏng phần sau. Tham lam chỉ đúng với các hệ mệnh giá “đẹp” như tiền Việt Nam (1, 2, 5, 10, 20, 50, …), nơi mỗi mệnh giá đủ lớn so với các mệnh giá nhỏ hơn. Với mệnh giá bất kỳ, bài đổi tiền phải giải bằng quy hoạch động (bài 19).

Mô phỏng trực quan. Đổi tiền là ví dụ dễ hiểu nhất của tham lam, nhưng cũng là chỗ dễ "tin nhầm" nhất. Em chạy với mệnh giá tiền Việt Nam trước, sau đó tự đổi sang bộ 1 3 4 và số tiền 6: mô phỏng sẽ so với cách đổi tốt nhất để em thấy tận mắt khi nào tham lam sai.

MÔ PHỎNGĐổi tiền tham lam và cách bắt lỗi bằng phản ví dụNhập các mệnh giá (tối đa 10) và số tiền cần đổi (tối đa 5000). Cuối mô phỏng có so sánh với số tờ ít nhất.
mệnh giá đang xéttờ tham lam đã lấycách đổi tối ưukhông lấy được tờ nào

Phần 3Chọn nhiều hoạt động nhất

Bài toán: hội trường nhận $n$ yêu cầu tổ chức sự kiện, sự kiện $i$ diễn ra trong $[s_i, e_i)$. Hai sự kiện không được chồng nhau (sự kiện kết thúc lúc 9 giờ thì sự kiện khác bắt đầu lúc 9 giờ được). Chọn nhiều sự kiện nhất.

Thử ba tiêu chí tham lam: bắt đầu sớm nhất (sai: một sự kiện bắt đầu sớm nhưng kéo dài cả ngày chặn hết các sự kiện khác), ngắn nhất (sai: một sự kiện ngắn nằm vắt ngang có thể chặn hai sự kiện dài), và kết thúc sớm nhất: đúng! Lý do: sự kiện kết thúc sớm nhất “trả lại” hội trường sớm nhất, để lại nhiều thời gian nhất cho phần còn lại. Nếu một lời giải tối ưu nào đó không chọn nó, ta thay sự kiện đầu tiên của lời giải đó bằng nó mà không gây chồng chéo, số sự kiện không đổi.

Thuật toán: sắp xếp theo $e_i$ tăng dần, duyệt lần lượt, sự kiện nào bắt đầu không sớm hơn thời điểm kết thúc của sự kiện vừa chọn thì chọn.

Code C++ và Python
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<pair<int,int>> a(n);        // (kết thúc, bắt đầu)
    for(auto &p : a) cin >> p.second >> p.first;
    sort(a.begin(), a.end());
    int dem = 0, ketThuc = INT_MIN;
    for(int i = 0; i < n; i++){
        if(a[i].second >= ketThuc){
            dem++;
            ketThuc = a[i].first;
        }
    }
    cout << dem;
    return 0;
}
n = int(input())
a = []
for _ in range(n):
    s, e = map(int, input().split())
    a.append((e, s))
a.sort()                      # theo thời điểm kết thúc
dem, ket_thuc = 0, float('-inf')
for e, s in a:
    if s >= ket_thuc:
        dem += 1
        ket_thuc = e
print(dem)
C++▶ Chạy tay từng dòng codeSau khi sắp xếp theo thời điểm kết thúc, chỉ cần một lượt duyệt: sự kiện nào bắt đầu từ ketThuc trở đi thì chọn.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n;
6 cin >> n;
7 vector<pair<int,int>> a(n); // (kết thúc, bắt đầu)
8 for(auto &p : a) cin >> p.second >> p.first;
9 sort(a.begin(), a.end());
10 int dem = 0, ketThuc = INT_MIN;
11 for(int i = 0; i < n; i++){
12 if(a[i].second >= ketThuc){
13 dem++;
14 ketThuc = a[i].first;
15 }
16 }
17 cout << dem;
18 return 0;
19}

Xem ba tiêu chí tham lam “đấu” với nhau: chọn tiêu chí rồi bấm Tự chạy. Với bộ dữ liệu mẫu, tiêu chí “bắt đầu sớm nhất” và “ngắn nhất” cho kết quả kém hơn “kết thúc sớm nhất”.

MÔ PHỎNGChọn hoạt động trên trục thời gianMỗi dòng nhập một hoạt động “bắt đầu kết thúc” (thời gian từ 0 đến 24). Xanh: được chọn · Đỏ: bị loại vì chồng chéo.
Đang tải mô phỏng…

Phần 4Xếp hàng và lập luận đổi chỗ

Bài toán: $n$ người xếp hàng chờ một quầy duy nhất, người $i$ cần $t_i$ phút. Thời gian chờ của mỗi người là tổng thời gian phục vụ những người đứng trước. Xếp thứ tự sao cho tổng thời gian chờ nhỏ nhất. Trực giác: người làm nhanh đứng trước. Chứng minh bằng lập luận đổi chỗ (exchange argument), công cụ chứng minh tham lam quan trọng nhất:

Giả sử trong một cách xếp có hai người đứng liền nhau $A$ trước $B$ mà $t_A > t_B$. Đổi chỗ hai người: những người khác không bị ảnh hưởng; trước đây $B$ phải chờ thêm $t_A$, giờ $A$ chờ thêm $t_B$. Tổng giảm $t_A - t_B > 0$. Vậy cách xếp tối ưu không thể có cặp “nghịch thế” nào, tức là phải sắp xếp tăng dần.

Code C++ và Python
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<long long> t(n);
    for(auto &v : t) cin >> v;
    sort(t.begin(), t.end());
    long long dangCho = 0, tong = 0;
    for(int i = 0; i < n; i++){
        tong += dangCho;
        dangCho += t[i];
    }
    cout << tong;
    return 0;
}
n = int(input())
t = sorted(map(int, input().split()))
dang_cho = tong = 0
for v in t:
    tong += dang_cho      # người này chờ hết những người trước
    dang_cho += v
print(tong)
C++▶ Chạy tay từng dòng codeSau khi sắp xếp tăng dần, người thứ i chờ đúng bằng tổng thời gian của những người đứng trước (biến dangCho).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 int n;
6 cin >> n;
7 vector<long long> t(n);
8 for(auto &v : t) cin >> v;
9 sort(t.begin(), t.end());
10 long long dangCho = 0, tong = 0;
11 for(int i = 0; i < n; i++){
12 tong += dangCho;
13 dangCho += t[i];
14 }
15 cout << tong;
16 return 0;
17}

Phần 5Cái túi: khi nào tham lam đúng, khi nào sai?

Có $n$ món hàng, món $i$ nặng $w_i$, giá trị $v_i$; túi chở tối đa $W$. Hai phiên bản của bài toán cho kết quả hoàn toàn khác nhau:

  • Cái túi phân số (được cắt nhỏ món hàng, ví dụ gạo, vàng cám): tham lam theo giá trị trên một đơn vị khối lượng $\frac{v_i}{w_i}$ giảm dần là tối ưu. Lấy hết món “đắt” nhất, rồi món tiếp theo, món cuối lấy một phần cho vừa túi.
  • Cái túi 0/1 (mỗi món lấy cả hoặc không lấy): tham lam sai. Ví dụ $W = 10$, các món $(w, v) = (6, 30), (5, 20), (5, 20)$: tham lam theo $\frac{v}{w}$ lấy món đầu ($\frac{30}{6} = 5$ lớn nhất), còn 4 kg không lấy thêm được gì, tổng 30; trong khi lấy hai món sau được 40. Bài này cần quy hoạch động (bài 19).
Cái túi phân số: tham lam theo giá trị trên mỗi kg
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n; double W;
    cin >> n >> W;
    vector<pair<double,double>> a(n);           // (w, v)
    for(auto &p : a) cin >> p.first >> p.second;
    sort(a.begin(), a.end(), [](auto &x, auto &y){
        return x.second / x.first > y.second / y.first;   // v/w giảm dần
    });
    double tong = 0;
    for(auto &p : a){
        if(W <= 0) break;
        double lay = min(p.first, W);             // lấy hết, hoặc phần vừa túi
        tong += lay * p.second / p.first;
        W -= lay;
    }
    cout << fixed << setprecision(2) << tong;
    return 0;
}
n, W = map(float, input().split())
a = [tuple(map(float, input().split())) for _ in range(int(n))]
a.sort(key=lambda p: p[1] / p[0], reverse=True)
tong = 0.0
for w, v in a:
    if W <= 0:
        break
    lay = min(w, W)
    tong += lay * v / w
    W -= lay
print(f"{tong:.2f}")
Dấu hiệu nhận biết bài tham lam
Đề thường có thao tác sắp xếp ẩn bên trong (theo thời gian kết thúc, theo hạn chót, theo tỉ lệ…), giới hạn $n$ lớn ($10^5$ – $10^6$) khiến quy hoạch động $O(n^2)$ không chạy nổi, và lời giải tối ưu có “cấu trúc đơn giản”. Nếu nghĩ ra một tiêu chí tham lam, hãy tự hỏi: “có trường hợp nào chọn thứ kém hơn trước mắt lại tốt hơn về sau không?”. Tìm được phản ví dụ thì bỏ ngay ý tưởng đó.
Luyện tập trên web
Các bài dưới đây trên blog đều có lời giải chi tiết bằng C++ và Python, xếp từ dễ tới khó. Em hãy tự làm trước rồi mới xem lời giải nhé.Xem toàn bộ bài cùng chủ đề: nhãn Tham lam.

Mô phỏng trực quan. Cùng một chiến lược "món nào giá/kg cao thì lấy trước", vì sao túi cắt được thì đúng mà túi 0/1 lại sai? Em xem mô phỏng chạy lần lượt cả hai kiểu túi trên cùng một bộ món hàng, cuối cùng so với đáp án tối ưu thật sự.

MÔ PHỎNGTham lam theo giá/kg: đúng với túi cắt được, sai với túi 0/1Nhập sức chứa W và tối đa 8 món dạng "khối lượng giá trị", cách nhau bởi dấu phẩy.
món đang xétđược lấy (cả hoặc một phần)bị bỏ

Bài tậpLuyện tham lam

Bài 1Trả tiền ít tờ nhấtDễ

Tiền Việt Nam có các mệnh giá 1, 2, 5, 10, 20, 50, 100, 200, 500 (nghìn đồng). Cho số tiền $N$ ($N \le 10^{18}$, đơn vị nghìn đồng), in số tờ ít nhất để trả đúng $N$.

Input
1999
Output
11
💡 Gợi ý
Tham lam từ mệnh giá lớn nhất như phần 2; với hệ mệnh giá này tham lam luôn đúng. $1999 = 3 \cdot 500 + 2 \cdot 200 + 50 + 2 \cdot 20 + 5 + 2 \cdot 2$, tổng 11 tờ.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    long long N;
    cin >> N;
    long long c[] = {500, 200, 100, 50, 20, 10, 5, 2, 1}, dem = 0;
    for(long long v : c){ dem += N / v; N %= v; }
    cout << dem;
    return 0;
}
N = int(input())
dem = 0
for v in (500, 200, 100, 50, 20, 10, 5, 2, 1):
    dem += N // v
    N %= v
print(dem)
Bài 2Xếp hàng ở quầyDễ

Có $n$ khách ($n \le 10^5$), khách $i$ cần $t_i$ phút. Xếp thứ tự để tổng thời gian chờ (không tính thời gian được phục vụ của chính mình) nhỏ nhất. In tổng đó.

Input
4
5 2 8 1
Output
12
💡 Gợi ý
Sắp xếp tăng dần (phần 4). Thứ tự 1, 2, 5, 8: chờ 0 + 1 + 3 + 8 = 12. Dùng long long.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<long long> t(n);
    for(auto &v : t) cin >> v;
    sort(t.begin(), t.end());
    long long cho = 0, tong = 0;
    for(long long v : t){ tong += cho; cho += v; }
    cout << tong;
    return 0;
}
n = int(input())
t = sorted(map(int, input().split()))
cho = tong = 0
for v in t:
    tong += cho
    cho += v
print(tong)
Bài 3Hội trườngTrung bình

Có $n$ yêu cầu ($n \le 10^5$), yêu cầu $i$ dùng hội trường trong $[s_i, e_i)$. Chọn nhiều yêu cầu nhất không chồng nhau.

Input
5
1 3
2 5
4 7
6 9
8 10
Output
3
💡 Gợi ý
Sắp xếp theo $e_i$ tăng dần và chọn như phần 3. Ví dụ chọn [1, 3), [4, 7), [8, 10).
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<pair<long long,long long>> a(n);
    for(auto &p : a) cin >> p.second >> p.first;
    sort(a.begin(), a.end());
    int dem = 0; long long kt = LLONG_MIN;
    for(auto &p : a)
        if(p.second >= kt){ dem++; kt = p.first; }
    cout << dem;
    return 0;
}
n = int(input())
a = sorted((e, s) for s, e in (map(int, input().split()) for _ in range(n)))
dem, kt = 0, float('-inf')
for e, s in a:
    if s >= kt:
        dem += 1
        kt = e
print(dem)
Bài 4Xóa chữ sốTrung bình

Cho số nguyên dương $N$ dưới dạng xâu (độ dài $\le 10^5$, không có chữ số 0 ở đầu) và $k$. Xóa đúng $k$ chữ số để số còn lại (giữ nguyên thứ tự) là lớn nhất.

Input
1924 2
Output
94
💡 Gợi ý
Tham lam từ trái sang: chữ số đứng trước càng lớn càng tốt. Dùng một ngăn xếp (bài 11): với mỗi chữ số mới $d$, chừng nào còn quyền xóa và đỉnh ngăn xếp nhỏ hơn $d$ thì xóa đỉnh. Cuối cùng nếu chưa xóa đủ $k$ thì xóa bớt ở cuối. Mỗi chữ số vào và ra ngăn xếp tối đa một lần: $O(n)$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    string s; int k;
    cin >> s >> k;
    string st;
    for(char d : s){
        while(k > 0 && !st.empty() && st.back() < d){ st.pop_back(); k--; }
        st.push_back(d);
    }
    st.resize(st.size() - k);
    cout << st;
    return 0;
}
s, k = input().split()
k = int(k)
st = []
for d in s:
    while k > 0 and st and st[-1] < d:
        st.pop()
        k -= 1
    st.append(d)
if k:
    st = st[:-k]
print(''.join(st))
Bài 5Cái túi phân sốTrung bình

Có $n$ bao hàng ($n \le 10^5$), bao $i$ nặng $w_i$ kg và trị giá $v_i$, được phép lấy một phần bao (giá trị tỉ lệ theo khối lượng). Túi chở tối đa $W$ kg. In tổng giá trị lớn nhất, làm tròn 2 chữ số thập phân.

Input
3 50
10 60
20 100
30 120
Output
240.00
💡 Gợi ý
Phần 5: sắp xếp theo $\frac{v_i}{w_i}$ giảm dần, lấy lần lượt. Ví dụ: lấy hết bao 1 (6/kg) và bao 2 (5/kg), lấy 20 kg của bao 3 (4/kg): $60 + 100 + 80 = 240$.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n; double W;
    cin >> n >> W;
    vector<pair<double,double>> a(n);
    for(auto &p : a) cin >> p.first >> p.second;
    sort(a.begin(), a.end(), [](auto &x, auto &y){ return x.second * y.first > y.second * x.first; });
    double tong = 0;
    for(auto &p : a){
        if(W <= 0) break;
        double lay = min(p.first, W);
        tong += lay * p.second / p.first;
        W -= lay;
    }
    printf("%.2f", tong);
    return 0;
}
n, W = input().split()
n, W = int(n), float(W)
a = [tuple(map(float, input().split())) for _ in range(n)]
a.sort(key=lambda p: p[1] / p[0], reverse=True)
tong = 0.0
for w, v in a:
    if W <= 0:
        break
    lay = min(w, W)
    tong += lay * v / w
    W -= lay
print(f"{tong:.2f}")
Bài 6Phủ điểm bằng đoạnTrung bình

Có $n$ điểm trên trục số ($n \le 10^5$, tọa độ nguyên). Mỗi tấm bạt phủ được một đoạn dài $L$ (phủ các điểm có tọa độ thuộc $[x, x + L]$). Cần ít nhất bao nhiêu tấm bạt để phủ hết các điểm?

Input
6 3
1 2 5 6 9 4
Output
3
💡 Gợi ý
Sắp xếp các điểm. Điểm trái nhất chưa được phủ bắt buộc phải nằm trong một tấm bạt nào đó; đặt tấm bạt bắt đầu đúng tại điểm này là có lợi nhất, vì nó phủ được xa nhất về bên phải. Bỏ qua mọi điểm $\le x + L$ rồi lặp lại. Ví dụ các điểm 1 2 4 5 6 9 với $L = 3$: bạt [1, 4], [5, 8], [9, 12], tổng 3 tấm.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n; long long L;
    cin >> n >> L;
    vector<long long> x(n);
    for(auto &v : x) cin >> v;
    sort(x.begin(), x.end());
    int dem = 0, i = 0;
    while(i < n){
        long long bat = x[i];
        dem++;
        while(i < n && x[i] <= bat + L) i++;
    }
    cout << dem;
    return 0;
}
n, L = map(int, input().split())
x = sorted(map(int, input().split()))
dem = i = 0
while i < n:
    bat = x[i]
    dem += 1
    while i < n and x[i] <= bat + L:
        i += 1
print(dem)
Bài 7Thuyền cứu hộTrung bình

Có $n$ người cân nặng $w_i$ ($n \le 10^5$). Mỗi thuyền chở tối đa hai người với tổng cân nặng không quá $W$ (mỗi người nặng không quá $W$). Cần ít nhất bao nhiêu thuyền?

Input
4 100
70 50 80 50
Output
3
💡 Gợi ý
Sắp xếp, hai con trỏ (bài 17) kết hợp tham lam: người nặng nhất còn lại chắc chắn phải đi một thuyền; cho người nhẹ nhất đi cùng nếu vừa, không thì người nặng đi một mình. Ví dụ: (50, 50), (70), (80).
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n; long long W;
    cin >> n >> W;
    vector<long long> w(n);
    for(auto &v : w) cin >> v;
    sort(w.begin(), w.end());
    int i = 0, j = n - 1, thuyen = 0;
    while(i <= j){
        if(i < j && w[i] + w[j] <= W) i++;   // người nhẹ đi cùng
        j--;                                  // người nặng luôn đi
        thuyen++;
    }
    cout << thuyen;
    return 0;
}
n, W = map(int, input().split())
w = sorted(map(int, input().split()))
i, j, thuyen = 0, n - 1, 0
while i <= j:
    if i < j and w[i] + w[j] <= W:
        i += 1
    j -= 1
    thuyen += 1
print(thuyen)
Bài 8Việc có hạn chótKhá

Có $n$ công việc ($n \le 10^5$), mỗi việc làm mất đúng 1 ngày, việc $i$ phải xong không muộn hơn ngày $d_i$ thì mới được trả $p_i$ đồng (làm trễ thì không được gì). Mỗi ngày làm một việc, bắt đầu từ ngày 1. Tìm tổng tiền lớn nhất.

Input
5
2 100
1 19
2 27
1 25
3 15
Output
142
💡 Gợi ý
Sắp xếp theo hạn chót tăng dần, duy trì một hàng đợi ưu tiên (min-heap) chứa tiền công của các việc đang định làm. Thêm việc $i$ vào heap; nếu số việc trong heap vượt quá $d_i$ (không đủ ngày), bỏ đi việc rẻ nhất. Tham lam “bỏ việc rẻ nhất khi quá tải” luôn đúng theo lập luận đổi chỗ. Ví dụ: làm các việc 27 (ngày 1), 100 (ngày 2), 15 (ngày 3): 142.
Xem code tham khảo (C++ / Python)
#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;
    vector<pair<int,long long>> a(n);       // (hạn chót, tiền)
    for(auto &p : a) cin >> p.first >> p.second;
    sort(a.begin(), a.end());
    priority_queue<long long, vector<long long>, greater<long long>> pq;
    for(auto &p : a){
        pq.push(p.second);
        if((int)pq.size() > p.first) pq.pop();   // quá số ngày → bỏ việc rẻ nhất
    }
    long long tong = 0;
    while(!pq.empty()){ tong += pq.top(); pq.pop(); }
    cout << tong;
    return 0;
}
import heapq
n = int(input())
a = sorted(tuple(map(int, input().split())) for _ in range(n))
pq = []
for d, p in a:
    heapq.heappush(pq, p)
    if len(pq) > d:
        heapq.heappop(pq)
print(sum(pq))
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇
Bản quyền thuộc về Thầy Nguyễn Việt Nam Sơn. Bài viết, lời giải, hình minh hoạ và mã nguồn trên trang là công sức biên soạn của thầy qua nhiều năm giảng dạy. Vui lòng không sao chép, đăng lại hay sử dụng dưới mọi hình thức khi chưa được sự đồng ý của tác giả. Nếu thấy hữu ích, hãy chia sẻ bằng đường link bài gốc nhé. Cảm ơn bạn!
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
  • Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
  • Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
  • Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
  • Dễ nản giữa chừng khi không có ai theo sát.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
  • Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
  • Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
  • Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.

Nhận xét