cv.cm

Lập trình · Thuật toán

Thuật toán tham lam là gì: cách hoạt động, code và ví dụ

7 phút đọc

Trả lời nhanh

Thuật toán tham lam (greedy) xây dựng lời giải từng bước và ở mỗi bước chọn phương án trông tốt nhất ngay lúc đó, không quay lại sửa các lựa chọn trước. Nó nhanh và đơn giản, nhưng chỉ đúng với bài toán có tính chất lựa chọn tham lam (greedy-choice property), tức lựa chọn tốt nhất cục bộ luôn nằm trong một lời giải tối ưu nào đó. Các thuật toán tham lam kinh điển và đúng gồm lập lịch khoảng thời gian, mã hóa Huffman, thuật toán Dijkstra và các thuật toán cây khung nhỏ nhất Kruskal, Prim.

Thuật toán tham lam giải bài toán gì

Thuật toán tham lam nhắm tới các bài toán tối ưu: chọn được nhiều hoạt động nhất, dùng ít đồng xu nhất, xây mạng rẻ nhất, nén tệp thành ít bit nhất. Thay vì duyệt mọi tổ hợp, thuật toán tham lam sắp xếp hoặc ưu tiên các phương án và chốt phương án tốt nhất ở mỗi bước. Điều đó thường cho thời gian O(n log n), thường chỉ là chi phí sắp xếp.

Phần khó không nằm ở code mà ở chứng minh. Nhiều quy tắc tham lam tự nhiên là sai, nên bạn phải chứng minh quy tắc đúng hoặc kiểm thử nó với lời giải vét cạn.

Ý tưởng bằng lời đơn giản

  1. Tìm một quy tắc xếp hạng các lựa chọn, ví dụ “kết thúc sớm nhất trước” hoặc “cạnh nhẹ nhất trước”.
  2. Sắp xếp các phương án theo quy tắc đó, hoặc giữ chúng trong hàng đợi ưu tiên.
  3. Duyệt qua các phương án và lấy từng phương án vẫn được ràng buộc cho phép.
  4. Không bao giờ hoàn tác một lựa chọn.

Thuật toán tham lam đúng khi hai tính chất sau cùng thỏa. Tính chất lựa chọn tham lam: có một lời giải tối ưu chứa lựa chọn tham lam đầu tiên. Cấu trúc con tối ưu: sau khi đưa ra lựa chọn đó, phần còn lại của bài toán là một bản nhỏ hơn của cùng bài toán. Cách chứng minh thường dùng là lập luận hoán đổi (exchange argument): lấy một lời giải tối ưu bất kỳ và chỉ ra rằng đổi một lựa chọn của nó sang lựa chọn tham lam không làm nó tệ đi.

Ví dụ: lập lịch khoảng thời gian

Bạn có một phòng họp và các yêu cầu sau (bắt đầu, kết thúc): A (1, 4), B (3, 5), C (0, 6), D (5, 7), E (3, 9), F (5, 9), G (6, 10), H (8, 11). Hãy đặt được nhiều cuộc họp không chồng nhau nhất có thể. Quy tắc tham lam đúng là: luôn chọn cuộc họp kết thúc sớm nhất trong số những cuộc bắt đầu sau khi cuộc họp đã đặt gần nhất kết thúc.

Cuộc họp (sắp theo giờ kết thúc)Bắt đầu, kết thúcBắt đầu sau giờ kết thúc gần nhất?Quyết địnhGiờ kết thúc gần nhất
A1, 4có (chưa đặt gì)Đặt4
B3, 5không (3 < 4)Bỏ qua4
C0, 6khôngBỏ qua4
D5, 7cóĐặt7
E3, 9khôngBỏ qua7
F5, 9khôngBỏ qua7
G6, 10khôngBỏ qua7
H8, 11cóĐặt11

Kết quả: A, D, H, ba cuộc họp, là mức tối đa. Vì sao đúng: cuộc họp kết thúc sớm nhất để lại nhiều thời gian nhất cho mọi việc khác, nên thay cuộc họp đầu tiên của bất kỳ lịch tối ưu nào bằng nó không bao giờ gây xung đột.

Cài đặt bằng Python

def max_meetings(meetings):
    """meetings: list of (name, start, end). Returns the booked names."""
    booked, last_end = [], float("-inf")
    for name, start, end in sorted(meetings, key=lambda m: m[2]):
        if start >= last_end:
            booked.append(name)
            last_end = end
    return booked

requests = [("A", 1, 4), ("B", 3, 5), ("C", 0, 6), ("D", 5, 7),
            ("E", 3, 9), ("F", 5, 9), ("G", 6, 10), ("H", 8, 11)]
print(max_meetings(requests))  # ['A', 'D', 'H']

Một ví dụ kinh điển thứ hai là bài toán cái túi phân số (fractional knapsack): khi bạn được lấy một phần của vật, lấy các vật theo thứ tự giá trị trên mỗi đơn vị khối lượng là tối ưu.

def fractional_knapsack(items, capacity):
    """items: list of (value, weight). Returns the best total value."""
    total = 0.0
    for value, weight in sorted(items, key=lambda i: i[0] / i[1], reverse=True):
        take = min(weight, capacity)
        total += value * take / weight
        capacity -= take
        if capacity == 0:
            break
    return total

print(fractional_knapsack([(60, 10), (100, 20), (120, 30)], 50))  # 240.0

Khi tham lam thất bại: các phản ví dụ

Bài toánQuy tắc tham lam hấp dẫnPhản ví dụPhương pháp đúng
Đổi tiền xu, các đồng 1, 3, 4Lấy đồng lớn nhất trướcSố tiền 6: tham lam 4 + 1 + 1 (3 đồng), tối ưu 3 + 3 (2 đồng)Quy hoạch động
Cái túi 0/1 (chỉ lấy nguyên vật)Giá trị trên khối lượng cao nhất trướcSức chứa 50, các vật (60, 10), (100, 20), (120, 30): tham lam lấy 60 + 100 = 160, tối ưu 100 + 120 = 220Quy hoạch động
Lập lịch khoảng thời gianCuộc họp ngắn nhất trước(0, 5), (4, 7), (6, 11): tham lam chỉ lấy (4, 7), tối ưu lấy hai cuộc còn lạiKết thúc sớm nhất trước
Đường đi ngắn nhất với cạnh âmDijkstra: nút gần nhất trướcBất kỳ đồ thị nào mà cạnh âm làm một nút đã chốt trở nên rẻ hơnBellman-Ford

Độ phức tạp thời gian và không gian

Thuật toánThời gianLý do
Lập lịch khoảng thời gianO(n log n)Sắp xếp theo giờ kết thúc; lượt quét là O(n).
Cái túi phân sốO(n log n)Sắp xếp theo giá trị trên khối lượng.
Mã hóa HuffmanO(n log n)n − 1 lần gộp, mỗi lần có thao tác heap.
MST KruskalO(E log E)Sắp xếp cạnh cộng union-find gần hằng số.
Đường đi ngắn nhất DijkstraO((V + E) log V)Hàng đợi ưu tiên dùng heap.

Không gian thường là O(n) cho bản sao đã sắp xếp hoặc heap. Nếu đầu vào đã được sắp xếp, nhiều thuật toán tham lam chạy trong O(n).

Khi nào dùng tham lam và ứng dụng thực tế

  • Dùng tham lam khi bạn chứng minh được tính chất lựa chọn tham lam, hoặc khi chấp nhận đáp án xấp xỉ nhanh. Nếu không, hãy dùng quy hoạch động hoặc quay lui.
  • Nén dữ liệu: mã hóa Huffman xây các mã tiền tố tối ưu bằng cách liên tục gộp hai ký hiệu ít xuất hiện nhất; nó được dùng trong các định dạng như DEFLATE (ZIP, gzip, PNG).
  • Mạng: cây khung nhỏ nhất (Kruskal, Prim) và định tuyến đường đi ngắn nhất (Dijkstra).
  • Lập lịch: hạn chót sớm nhất trước (earliest deadline first), và đặt phòng hoặc máy.
  • Xấp xỉ: với bài toán phủ tập hợp (set cover), quy tắc tham lam “chọn tập phủ nhiều phần tử chưa được phủ nhất” không tối ưu nhưng đảm bảo nằm trong hệ số logarit của tối ưu.
  • Bài phỏng vấn: jump game, gas station, assign cookies, non-overlapping intervals, minimum number of arrows to burst balloons, task scheduler.

Câu hỏi thường gặp

Thuật toán tham lam là gì, nói đơn giản?

Đó là thuật toán luôn chọn phương án trông tốt nhất ở thời điểm hiện tại và không bao giờ xem xét lại. Nó chỉ đúng với những bài toán mà các lựa chọn cục bộ này luôn dẫn đến đáp án tốt nhất toàn cục.

Làm sao chứng minh một thuật toán tham lam là đúng?

Cách thường dùng là lập luận hoán đổi: bắt đầu từ một lời giải tối ưu bất kỳ và chỉ ra rằng bạn có thể thay lựa chọn đầu tiên của nó bằng lựa chọn tham lam mà không làm nó tệ đi, rồi lặp lại trên phần bài toán còn lại.

Tham lam hay quy hoạch động?

Hãy thử tham lam trước nếu một quy tắc đơn giản có vẻ đúng, rồi tìm phản ví dụ. Nếu có, bài toán cần quy hoạch động, vốn so sánh mọi lựa chọn cho từng bài toán con.

Thuật toán Dijkstra có phải tham lam không?

Có. Nó luôn chốt nút chưa chốt gần nhất. Lựa chọn tham lam này đúng vì trọng số cạnh không âm.

Tất cả hướng dẫn lập trình