Thuật toán Ứng dụng — DP, String, Big Data & hơn nữa/Greedy — Tham lam có chứng minh: Interval Scheduling và Huffman
6/66
Bài 6 / 66~22 phútQuyết định dưới constraint — DP, Greedy, BacktrackingMiễn phí lượt xem

Greedy — Tham lam có chứng minh: Interval Scheduling và Huffman

Greedy chọn tốt nhất mỗi bước không nhìn lại. Nắm greedy choice property, exchange argument, interval scheduling theo finish time, Huffman coding, và khi nào greedy SAI.

TL;DR: Greedy algorithm xây dựng nghiệm bằng cách chọn lựa chọn cục bộ tốt nhất (locally optimal) tại mỗi bước, không bao giờ quay lui. Greedy đúng khi bài toán có hai tính chất: greedy choice property (lựa chọn tham lam không chặn nghiệm tối ưu) và optimal substructure (tối ưu toàn cục chứa tối ưu subproblem). Chứng minh bằng exchange argument: giả sử OPT khác greedy, hoán đổi một phần tử → OPT không tệ hơn → mâu thuẫn. Greedy SAI khi thiếu một trong hai tính chất — 0/1 knapsack và coin change với mệnh giá lạ là counter-example kinh điển.

Năm 2003, nhóm kỹ thuật tại Google gặp vấn đề: log file gzip của họ tốn 40% dung lượng hơn dự kiến. Sau khi kiểm tra, họ phát hiện Huffman coding — trái tim của gzip — đang không tối ưu vì frequency table bị tính sai. Huffman coding là thuật toán greedy thuần túy: mỗi bước chọn hai node có frequency nhỏ nhất để gộp. Không cần DP, không cần backtrack, chỉ cần một min-heap và lòng tin rằng lựa chọn tham lam tại mỗi bước dẫn đến tree tối ưu toàn cục. Nhưng lòng tin đó cần được chứng minh.

1. Analogy — Người trả tiền thừa vs người đầu bếp tối ưu

Greedy là người trả tiền thừa hạnh phúc: khi trả 43.000đ bằng tờ lớn nhất có thể (20k + 20k + 2k + 1k), anh ta không bao giờ đổi ý. Với hệ mệnh giá Việt Nam (1k, 2k, 5k, 10k, 20k, 50k, 100k), cách này luôn cho số đồng xu ít nhất. Tính chất này — hệ mệnh giá canonical — là greedy choice property.

Khi greedy thất bại: đất nước khác có mệnh giá {1, 3, 4}, cần trả 6. Greedy: 4 + 1 + 1 = 3 đồng. Tối ưu: 3 + 3 = 2 đồng. Greedy chọn 4 (lớn nhất không vượt 6) nhưng 4 "phá vỡ" kết hợp tốt hơn.

Đặc trưngGreedyDP
Quyết địnhCục bộ tốt nhất, không quay luiXét mọi subproblem, chọn global optimal
Khi đúngCó greedy choice property + optimal substructureLuôn đúng khi overlapping subproblems
ComplexityO(n log n) thường gặpO(n²) hoặc O(n × W) thường gặp
Proof requiredExchange argument bắt buộcChứng minh qua recurrence

2. Hai tính chất bắt buộc

2.1 Greedy Choice Property

Định nghĩa: lựa chọn tham lam tại bước đầu tiên (cục bộ tốt nhất) luôn là thành phần của ít nhất một nghiệm tối ưu toàn cục.

Đây là tính chất không tầm thường. Nó không nói lựa chọn tham lam tốt nhất — nó nói lựa chọn tham lam không loại bỏ khả năng đạt tối ưu. Nếu tính chất này sai, greedy có thể "đi vào ngõ cụt" không thể hồi phục.

2.2 Optimal Substructure

Định nghĩa: nghiệm tối ưu của bài toán lớn chứa nghiệm tối ưu của subproblem còn lại sau khi đã thực hiện lựa chọn tham lam.

Cũng là tính chất của DP. Nhưng trong DP, bạn cần xét tất cả lựa chọn và lấy tối ưu. Trong greedy, bạn chỉ cần xét lựa chọn tham lam duy nhất — và chứng minh nó đủ.

3. Exchange Argument — Phương pháp chứng minh

Exchange argument là kỹ thuật chứng minh greedy choice property. Template:

  1. Giả sử tồn tại nghiệm tối ưu OPT không theo lựa chọn tham lam.
  2. Tìm sự khác biệt đầu tiên giữa OPT và nghiệm greedy — bước nào OPT không chọn như greedy.
  3. Hoán đổi (exchange) phần tử OPT tại bước đó thành lựa chọn của greedy.
  4. Chứng minh sau hoán đổi, nghiệm mới không tệ hơn OPT (objective không giảm hoặc không tăng).
  5. Kết luận: bằng cách hoán đổi từng bước một, OPT có thể biến thành nghiệm greedy mà không tệ hơn → greedy tối ưu.

Ta áp dụng kỹ thuật này cho Interval Scheduling bên dưới.

4. Interval Scheduling — Chọn theo Finish Time

4.1 Bài toán

Cho n hoạt động, mỗi hoạt động i có [start[i], finish[i]). Một phòng họp chỉ có thể tổ chức một hoạt động tại một thời điểm (hai hoạt động không được chồng lấn). Tìm tập con nhiều hoạt động nhất có thể được tổ chức.

gantt
    title Interval Scheduling — 6 hoạt động
    dateFormat X
    axisFormat %s

    section Các lựa chọn
    A [0-6]  : 0, 6
    B [1-4]  : 1, 4
    C [3-5]  : 3, 5
    D [4-7]  : 4, 7
    E [5-9]  : 5, 9
    F [6-10] : 6, 10

4.2 Greedy: chọn theo finish time sớm nhất

IntervalScheduling(activities[]):
    Sắp xếp activities theo finish time tăng dần
    -- O(n log n)

    selected <- []
    last_finish <- 0    -- thời điểm kết thúc của hoạt động được chọn gần nhất

    for each activity a trong activities (đã sort):
        if a.start >= last_finish:    -- không chồng lấn
            selected.append(a)
            last_finish <- a.finish

    return selected

Time: O(n log n)  Space: O(n)

4.3 Trace ví dụ

Sau khi sort theo finish time: B[1-4], C[3-5], A[0-6], D[4-7], E[5-9], F[6-10].

BướcXétĐiều kiệnQuyết địnhlast_finish
Init0
1B[1-4]1 >= 0 ✓Chọn B4
2C[3-5]3 < 4 ✗Bỏ C4
3A[0-6]0 < 4 ✗Bỏ A4
4D[4-7]4 >= 4 ✓Chọn D7
5E[5-9]5 < 7 ✗Bỏ E7
6F[6-10]6 < 7 ✗Bỏ F7

Kết quả: chọn B và D — 2 hoạt động. (Đây là tối ưu: không thể chọn 3 hoạt động không chồng lấn từ 6 hoạt động này.)

4.4 Chứng minh bằng Exchange Argument

Claim: Greedy (sort theo finish time) cho số hoạt động tối đa.

Proof:

  • Gọi nghiệm greedy là G = {g₁, g₂, ..., gₖ} (sắp xếp theo finish time).
  • Gọi nghiệm tối ưu là OPT = {o₁, o₂, ..., oₘ} (sắp xếp theo finish time).
  • Ta chứng minh k = m (greedy chọn không ít hơn OPT).

Lemma: với mọi j ≤ min(k,m), ta có finish(gⱼ) ≤ finish(oⱼ).

Chứng minh lemma bằng induction:

  • j = 1: greedy chọn g₁ là hoạt động finish sớm nhất trong tất cả → finish(g₁) ≤ finish(o₁). (OPT chọn gì đó ≥ min finish time.)
  • j → j+1: giả sử finish(gⱼ) ≤ finish(oⱼ). OPT chọn oⱼ₊₁ với start(oⱼ₊₁) ≥ finish(oⱼ). Vì finish(gⱼ) ≤ finish(oⱼ) ≤ start(oⱼ₊₁), hoạt động oⱼ₊₁ tương thích với greedy sau bước j. Greedy chọn gⱼ₊₁ là hoạt động tương thích finish sớm nhất → finish(gⱼ₊₁) ≤ finish(oⱼ₊₁).

Kết luận: Nếu m > k (OPT tốt hơn), thì oₖ₊₁ tồn tại. Lemma cho finish(gₖ) ≤ finish(oₖ), và oₖ₊₁ tương thích với greedy sau bước k — greedy sẽ chọn thêm ít nhất một hoạt động, mâu thuẫn với k là kết thúc greedy. Do đó m ≤ k. Mà OPT là tối ưu nên m ≥ k. Vậy m = k.

5. Huffman Coding — Greedy xây cây prefix code

5.1 Bài toán

Cho n ký tự với tần suất freq[c]. Tìm prefix-free code (không mã nào là prefix của mã khác) gán chuỗi bit cho mỗi ký tự sao cho tổng chiều dài mã hoá toàn bộ văn bản là nhỏ nhất.

Bài toán tương đương: xây binary tree T sao cho Σ freq[c] × depth(c) nhỏ nhất, trong đó depth(c) là độ sâu của ký tự c trong cây.

5.2 Huffman Algorithm

Huffman(freq[]):
    H <- MinHeap rỗng (keyed by frequency)

    for each ký tự c:
        H.push(LeafNode(c, freq[c]))    -- tạo node lá cho mỗi ký tự

    while H.size() > 1:
        left <- H.pop()     -- node tần suất thấp nhất
        right <- H.pop()    -- node tần suất thấp thứ hai
        merged <- InternalNode(left, right,
                               freq = left.freq + right.freq)
        H.push(merged)      -- gộp thành node mới, đẩy lại heap

    return H.pop()    -- root của Huffman tree

Time: O(n log n)  Space: O(n)

5.3 Ví dụ — 5 ký tự

Ký tự và tần suất: A=45, B=13, C=12, D=16, E=9, F=5.

graph TD
    ROOT["100"] --> L45["A: 45"]
    ROOT --> R55["55"]
    R55 --> R25["25"]
    R55 --> R30["30"]
    R25 --> LC12["C: 12"]
    R25 --> LD13["B: 13"]
    R30 --> LE14["14"]
    R30 --> LD16["D: 16"]
    LE14 --> LF5["F: 5"]
    LE14 --> LE9["E: 9"]

Huffman codes: A=0, B=101, C=100, D=111, E=1101, F=1100.

Chiều dài trung bình: (45×1 + 13×3 + 12×3 + 16×3 + 9×4 + 5×4) / 100 = (45+39+36+48+36+20)/100 = 224/100 = 2.24 bits/ký tự.

So với fixed-length (3 bits/ký tự cho 6 ký tự): tiết kiệm 25%. Đây là nền tảng của gzip, bzip2, DEFLATE.

5.4 Vì sao Huffman đúng

Greedy choice: hai ký tự có tần suất nhỏ nhất luôn nằm ở độ sâu lớn nhất trong cây tối ưu (bài chứng minh qua exchange argument — hoán đổi hai node depth-max với hai node freq-min không làm tăng cost).

Optimal substructure: sau khi gộp hai node frequency nhỏ nhất thành node mới với freq = f₁ + f₂, bài toán còn lại là Huffman cho (n-1) ký tự — tối ưu đệ quy.

6. Khi nào Greedy SAI

6.1 0/1 Knapsack — đã thấy ở bài trước

Items không chia được, nên lựa chọn tham lam (ratio value/weight cao nhất) có thể chiếm hết không gian và chặn kết hợp tốt hơn.

6.2 Coin Change với mệnh giá lạ

Mệnh giá {1, 3, 4}, cần đổi 6.

Greedy (chọn mệnh giá lớn nhất không vượt amount):

  • Chọn 4 (còn 2)
  • Chọn 1 (còn 1)
  • Chọn 1 (còn 0)
  • Kết quả: 3 đồng

DP tối ưu:

  • Chọn 3 + 3
  • Kết quả: 2 đồng

Vì sao greedy sai? Khi chọn 4, ta "phá vỡ" khả năng dùng hai đồng 3. Mệnh giá {1, 3, 4} không có tính canonical — đồng 4 không "bao trùm" được tổ hợp {3, 1} theo cách làm tối ưu. Greedy choice property không thỏa mãn.

6.3 Shortest Path với edge âm

Dijkstra (greedy: luôn expand đỉnh gần nhất) sai khi có edge âm — xem bài Dijkstra. Một edge âm có thể làm path "dài" trở thành path ngắn hơn sau khi greedy đã finalize đỉnh đó.

6.4 Tóm tắt: khi nào greedy đúng vs sai

Bài toánGreedyLý do
Fractional knapsackĐúngItems chia được — ratio tốt nhất không "phá vỡ" kết hợp
0/1 KnapsackSAIItems không chia — greedy chặn kết hợp tốt hơn
Interval schedulingĐúngFinish-time sớm nhất "chiếm ít tương lai nhất"
Huffman codingĐúngHai node min-freq luôn ở depth lớn nhất trong optimal
Coin change (canonical)ĐúngVN/US denomination có tính canonical
Coin change (lạ)SAI{1,3,4}: đồng 4 phá tổ hợp {3,3} tốt hơn
Dijkstra (no negative edge)ĐúngGreedy invariant: dist min = finalized
Dijkstra (negative edge)SAIEdge âm phá greedy invariant

7. Liên hệ các bài khác

  • 0/1 Knapsack: Counter-example trực tiếp cho greedy — bài này giải thích tại sao DP cần thiết thay greedy. Hai bài đọc liên tiếp tạo bức tranh đầy đủ về ranh giới greedy/DP.
  • Backtracking: Khi greedy sai và DP quá tốn không gian/thời gian, backtracking là lựa chọn thứ ba — thử tất cả kết hợp với cắt tỉa. Tam giác greedy/DP/backtracking là framework quyết định thuật toán tối ưu.
  • Hàng đợi ưu tiên: Huffman coding phụ thuộc hoàn toàn vào min-heap — mỗi bước cần pop 2 phần tử min và push 1 phần tử mới. Hiểu heap là hiểu vì sao Huffman O(n log n).
  • Dijkstra: Dijkstra là thuật toán greedy trên graph — mở rộng đỉnh có dist nhỏ nhất. Bài đó chứng minh greedy invariant đúng khi không có negative edge — cùng template exchange argument.

8. Deep Dive

Deep Dive — Matroid và nền tảng lý thuyết của Greedy

Tại sao greedy đúng? Câu trả lời sâu nhất đến từ lý thuyết Matroid (Whitney, 1935).

Matroid là cặp (S, I) trong đó S là tập phần tử, I là tập các "independent sets" (tập con "chấp nhận được"), thoả mãn 3 axiom: hereditary (subset của independent set là independent), augmentation (nếu A và B independent và |A| < |B|, có thể thêm phần tử từ B vào A giữ independent), và others. Greedy algorithm đúng trên mọi weighted matroid: sắp xếp phần tử theo weight giảm dần, tham lam thêm từng phần tử nếu tập kết quả vẫn independent.

Interval scheduling là matroid (graphic matroid). Minimum spanning tree (Kruskal) là matroid (cycle matroid của graph). Huffman không phải matroid thuần túy nhưng thoả greedy vì tính chất tree structure riêng.

Tham khảo:

  • Introduction to Algorithms (CLRS, 4th ed. 2022), Chapter 15 — Greedy Algorithms: exchange argument cho activity selection, Huffman proof.
  • Oxley — Matroid Theory (2011): lý thuyết matroid đầy đủ.
  • Huffman (1952) — "A Method for the Construction of Minimum-Redundancy Codes": bài báo gốc 4 trang.
  • David Huffman viết thuật toán này như bài tập cuối kỳ môn Information Theory tại MIT năm 1951 — thay vì thi. Giáo sư Robert Fano sau đó thừa nhận solution của Huffman tốt hơn cả giải pháp ông tự đề xuất trước đó (Fano coding).

9. Tóm tắt

  • Greedy đúng khi có hai tính chất: greedy choice property (lựa chọn tham lam không chặn tối ưu) và optimal substructure (tối ưu lớn chứa tối ưu con).
  • Exchange argument: chứng minh bằng cách giả sử OPT khác greedy, hoán đổi một phần tử, chứng minh nghiệm mới không tệ hơn → greedy tối ưu.
  • Interval Scheduling: sort theo finish time sớm nhất, chọn tham lam. Chứng minh: greedy "chiếm ít tương lai nhất" — tạo nhiều room nhất cho hoạt động tiếp theo.
  • Huffman Coding: gộp hai node min-frequency mỗi bước qua min-heap, O(n log n). Nền tảng của gzip/DEFLATE.
  • Greedy SAI: 0/1 knapsack (không chia được), coin change với mệnh giá lạ, Dijkstra với negative edge. Greedy đúng với fractional knapsack, interval scheduling, Huffman, Dijkstra (no negative edge).
  • Không bao giờ dùng greedy mà không chứng minh greedy choice property — dù trực giác có vẻ đúng.

10. Tự kiểm tra

Tự kiểm tra
Q1
Phân biệt greedy choice property và optimal substructure. Cần đủ cả hai không?

Greedy choice property: lựa chọn tham lam tại bước đầu tiên (cục bộ tốt nhất theo tiêu chí nào đó) là thành phần của ít nhất một nghiệm tối ưu toàn cục. Tính chất này nói: đi theo greedy không bao giờ "đóng cửa" tối ưu.

Optimal substructure: sau khi thực hiện lựa chọn greedy, bài toán còn lại là subproblem có nghiệm tối ưu độc lập — và tối ưu toàn cục = greedy choice + tối ưu subproblem.

Cần đủ cả hai. Optimal substructure một mình không đủ (DP cũng có nhưng cần xét mọi lựa chọn). Greedy choice property một mình không đủ (cần biết subproblem còn lại có optimal substructure). Ví dụ: 0/1 knapsack có optimal substructure nhưng thiếu greedy choice property — greedy sai. Bài toán coin change với mệnh giá chuẩn có cả hai — greedy đúng.

Q2
Exchange argument cho Interval Scheduling — bước exchange cụ thể là gì? Tại sao sau exchange nghiệm không tệ hơn?

Giả sử OPT chọn hoạt động o₁ ở vị trí đầu tiên, nhưng g₁ (lựa chọn greedy — finish time nhỏ nhất toàn bộ) khác o₁.

Exchange: thay o₁ trong OPT bằng g₁. Nghiệm mới vẫn hợp lệ vì: (1) finish(g₁) ≤ finish(o₁) (greedy chọn finish nhỏ nhất), nên g₁ không chồng lên o₂ (o₂ bắt đầu sau finish(o₁)). (2) Số hoạt động không đổi (vẫn chọn cùng số lượng, chỉ thay o₁ bằng g₁).

Sau exchange, OPT mới đồng ý với greedy ở bước 1. Lặp lại cho bước 2, 3... — cuối cùng OPT biến thành greedy solution với số hoạt động bằng nhau → greedy tối ưu.

Q3
Coin change với mệnh giá [1, 3, 4], amount = 6. Greedy cho gì? Tại sao sai? DP cho gì?

Greedy (chọn mệnh giá lớn nhất không vượt remaining amount): chọn 4 (còn 2) → chọn 1 (còn 1) → chọn 1 (còn 0). Tổng: 3 đồng.

DP tối ưu: chọn 3 + 3 = 6. Tổng: 2 đồng. Greedy kém hơn 1 đồng.

Nguyên nhân: khi chọn đồng 4, ta cần thêm 2 — và 2 không có mệnh giá, phải dùng 2 đồng 1. Tổng là 3. Nhưng nếu dùng hai đồng 3 (3+3=6), chỉ cần 2 đồng. Greedy "commit" vào đồng 4 sớm quá, phá vỡ khả năng dùng tổ hợp {3,3}. Greedy choice property không thoả — lựa chọn 4 tại bước đầu không thuộc nghiệm tối ưu nào.

Q4
Huffman coding — tại sao gộp hai node min-frequency? Điều gì xảy ra nếu gộp hai node max-frequency?

Huffman tối thiểu hoá Σ freq[c] × depth(c). Ký tự tần suất thấp nên có depth lớn (chuỗi bit dài hơn) — ít xuất hiện trong văn bản nên cost cao ít ảnh hưởng. Ký tự tần suất cao nên có depth nhỏ (chuỗi bit ngắn) — xuất hiện nhiều nên tiết kiệm tổng cost.

Gộp hai node min-frequency đặt chúng ở depth lớn nhất trong cây — đúng với mục tiêu. Chứng minh qua exchange argument: trong cây tối ưu, hai node depth-max có thể "hoán đổi" với hai node min-frequency mà không tăng cost (vì freq-min nhỏ × depth-lớn ≤ freq-max × depth-max nếu node max-freq đang ở depth-lớn).

Nếu gộp hai node max-frequency: chúng sẽ có depth lớn trong cây — ký tự xuất hiện nhiều nhất lại có mã dài nhất. Tổng cost tăng vọt. Ví dụ với A=45: nếu A ở depth 4 (mã 4 bit), cost từ A là 45×4 = 180 thay vì 45×1 = 45 trong Huffman tối ưu.

Q5
Interval Scheduling hỏi số hoạt động tối đa. Nếu bài toán đổi thành: mỗi hoạt động có weight và ta muốn tổng weight tối đa (Weighted Interval Scheduling), greedy theo finish time còn đúng không?

Không. Weighted Interval Scheduling phá vỡ greedy choice property khi có trọng lượng.

Counter-example: A[0-10] weight=100, B[0-5] weight=1, C[5-10] weight=1. Sort theo finish: B[0-5], C[5-10], A[0-10]. Greedy chọn B (finish 5 sớm nhất) → chọn C (start 5 ≥ finish 5) → A bị loại. Total weight = 2. Nhưng chọn chỉ A: weight = 100.

Weighted Interval Scheduling cần DP: dp[i] = tổng weight tối đa khi xét i activities đầu, có thể chọn hoặc bỏ activity i. Transition: dp[i] = max(dp[i-1], weight[i] + dp[p(i)]) trong đó p(i) là activity cuối cùng không chồng lấn với i. Precompute p(i) bằng binary search — O(n log n) tổng.

Q6
Matroid và greedy — Kruskal's MST là ví dụ greedy đúng. Tính chất matroid nào Kruskal dựa vào?

Kruskal xây MST bằng greedy: sort edges theo weight tăng dần, lần lượt thêm edge nếu không tạo cycle (kiểm tra bằng Union-Find). Greedy đúng vì graph có cấu trúc matroid.

Cụ thể: graphic matroid của graph G = (V, E) là (E, I) trong đó I = tập các acyclic subgraphs (forests). Tập acyclic thỏa mãn: (1) hereditary — subset của forest là forest; (2) augmentation — nếu F1 và F2 là forests với |F1| nhỏ hơn |F2|, có edge trong F2 thêm vào F1 mà không tạo cycle.

Greedy trên weighted matroid (chọn phần tử weight nhỏ nhất mà vẫn independent) cho nghiệm tối ưu — đây là định lý matroid greedy. Kruskal là trường hợp đặc biệt của weighted matroid greedy trên graphic matroid. Prim cũng greedy nhưng theo cách khác (expand từ một đỉnh).

Bài tiếp theo: Backtracking và cắt tỉa

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên