Thuật toán tham lam (Greedy) là một trong những chủ đề quan trọng nhất để hình thành tư duy lựa chọn trong Competitive Programming, bởi một lời giải Greedy tốt không đến từ việc thấy dữ liệu rồi “sort và lấy lớn nhất”, mà từ khả năng nhận ra một lựa chọn cục bộ có thể được thực hiện ngay mà vẫn bảo toàn một nghiệm tối ưu cho phần còn lại. Khi học chắc chủ đề này, học sinh sẽ dần nhận ra nhiều mẫu tư duy xuất hiện lặp lại như dominance, exchange argument, stays-ahead, chọn cực trị bằng hai con trỏ, earliest finish trong interval scheduling, farthest reach trong interval covering, replace-worst và retroactive greedy với Heap, constructive greedy giữ invariant, tối ưu thứ tự từ điển bằng monotonic deletion, các bài phân bổ tài nguyên theo coverage invariant, cho tới những ứng dụng sâu hơn trong Kruskal, Dijkstra và hàm kiểm tra của Binary Search Answer. Giá trị lớn nhất của Greedy không phải là thuộc nhiều mẹo, mà là rèn được thói quen luôn tự hỏi vì sao quyết định hiện tại là an toàn, có thể đổi một nghiệm tối ưu để chứa quyết định đó hay không, và nếu không chứng minh được thì phải biết dừng đúng lúc để chuyển sang DP, shortest path, matching hoặc một mô hình phù hợp hơn.

Đăng nhập để tham gia lộ trình luyện tập

LỘ TRÌNH THUẬT TOÁN THAM LAM

Thuật toán tham lam (Greedy) xây dựng lời giải bằng cách liên tiếp chọn một phương án tốt ở thời điểm hiện tại. Điểm khó không nằm ở việc sort, dùng Heap hay lấy phần tử lớn nhất, mà ở chỗ phải chứng minh rằng lựa chọn cục bộ đó không làm mất nghiệm tối ưu toàn cục.

Một lời giải Greedy thường có ba phần:

  • xác định lựa chọn cục bộ;
  • xác định bất biến (invariant) sau mỗi bước;
  • chứng minh lựa chọn đó an toàn bằng đổi chỗ (exchange argument), đi trước (stays-ahead), ưu thế (dominance), quy nạp (induction) hoặc tính chất lát cắt (cut property).

Nếu không thể chỉ ra vì sao lựa chọn hiện tại luôn an toàn, cần nghi ngờ Greedy và thử tìm phản ví dụ nhỏ.

Giai đoạn 1 - NỀN TẢNG GREEDY: LOCAL CHOICE, DOMINANCE, PHẢN VÍ DỤ

Greedy cơ bản xuất hiện khi một lựa chọn cục bộ luôn không tệ hơn các lựa chọn khác.

Ví dụ, nếu mục tiêu là lấy được nhiều phần tử nhất dưới một ngân sách và mọi phần tử đóng góp như nhau, phần tử rẻ hơn có ưu thế hơn phần tử đắt hơn. Nếu đang chọn phần tử giá bb nhưng tồn tại phần tử giá aa với:

a≤ba \le b

thì thay bb bằng aa không làm giảm số lượng phần tử được chọn và không làm tăng chi phí.

Đó là ý tưởng ưu thế (dominance).

Một khung chứng minh thường dùng là:

  1. Lấy một nghiệm tối ưu bất kỳ.
  2. Nếu nghiệm đó đã dùng lựa chọn Greedy thì giữ nguyên.
  3. Nếu chưa dùng, thay một lựa chọn của nghiệm tối ưu bằng lựa chọn Greedy.
  4. Chứng minh nghiệm vẫn hợp lệ và giá trị không xấu hơn.
  5. Sau khi cố định bước đầu, bài toán còn lại có cùng dạng.

Mã giả tổng quát:

sắp xếp hoặc chuẩn bị dữ liệu theo tiêu chí đã chứng minh

khởi tạo trạng thái hiện tại

duyệt từng ứng viên:
    nếu ứng viên hiện tại là lựa chọn an toàn:
        chọn ứng viên
        cập nhật trạng thái

Greedy không đồng nghĩa với heuristic. Một heuristic chỉ “có vẻ hợp lý”, còn Greedy đúng phải có tính chất lựa chọn tham lam (greedy-choice property).

Một kỹ năng quan trọng là chủ động tạo phản ví dụ. Chẳng hạn với hệ tiền:

{1,3,4}\{1,3,4\}

và cần đổi:

66

chiến lược lấy đồng lớn nhất trước cho:

4+1+14+1+1

dùng 33 đồng, nhưng nghiệm tối ưu là:

3+33+3

chỉ dùng 22 đồng. Vì vậy Greedy đổi tiền không đúng cho hệ tiền tổng quát.

Giai đoạn 2 - SORTING, EXCHANGE ARGUMENT, CUSTOM COMPARATOR

Rất nhiều bài Greedy thực chất là bài tìm một thứ tự xử lý tối ưu.

Ta xét hai phần tử AA và BB đứng cạnh nhau. So sánh hai thứ tự:

A→BA \rightarrow B

và:

B→AB \rightarrow A

Nếu chứng minh được thứ tự A→BA \rightarrow B không bao giờ tệ hơn, ta có thể dùng điều kiện đó làm comparator khi sắp xếp.

Đây là lập luận đổi chỗ (exchange argument).

Mã giả:

xây dựng comparator từ việc so sánh hai thứ tự A trước B và B trước A

sắp xếp toàn bộ phần tử theo comparator

xử lý theo thứ tự vừa sắp xếp

Một dạng thường gặp là so sánh tỉ lệ. Nếu điều kiện lý thuyết là:

aibi<ajbj\frac{a_i}{b_i} < \frac{a_j}{b_j}

không nên dùng số thực. So sánh bằng tích chéo:

aibj<ajbia_i b_j < a_j b_i

Cần dùng kiểu số đủ lớn để tránh tràn.

Với bài ghép chuỗi để tạo chuỗi lớn nhất, thứ tự giữa hai chuỗi aa và bb thường được quyết định bởi:

a+b>b+aa+b > b+a

Mã giả:

đổi mỗi phần tử thành chuỗi nếu cần

sắp xếp sao cho:
    a đứng trước b khi a + b lớn hơn b + a

ghép các chuỗi theo thứ tự đã sắp xếp

Comparator phải tạo ra thứ tự nhất quán. Không nên dùng điều kiện kiểu >= nếu nó phá tính chặt của phép so sánh.

Tie-break chỉ nên thêm khi đề yêu cầu hoặc khi đã chứng minh nó không ảnh hưởng tối ưu.

Giai đoạn 3 - GHÉP CẶP, HAI ĐẦU, MATCHING TUYẾN TÍNH

Sau khi sắp xếp, nhiều bài ghép cặp trở thành Greedy hai con trỏ.

Một mẫu rất quan trọng là xét phần tử lớn nhất còn lại.

Giả sử dãy đã tăng:

a1≤a2≤⋯≤ana_1 \le a_2 \le \dots \le a_n

và hai phần tử có thể ghép nếu tổng không vượt WW.

Ta xét phần tử lớn nhất ara_r.

  • Nếu al+ar≤Wa_l+a_r\le W, ghép ara_r với phần tử nhỏ nhất ala_l.
  • Nếu al+ar>Wa_l+a_r>W, thì ara_r không thể ghép với bất kỳ phần tử nào khác, nên phải xử lý riêng.

Mã giả:

sắp xếp tăng

l = đầu dãy
r = cuối dãy

trong khi l <= r:
    nếu l == r:
        xử lý phần tử cuối cùng
        kết thúc

    nếu a[l] + a[r] thỏa điều kiện:
        ghép a[l] với a[r]
        tăng l
        giảm r
    ngược lại:
        xử lý a[r] riêng
        giảm r

Một mẫu khác là phần tử khả thi nhỏ nhất (smallest feasible).

Nếu mỗi yêu cầu cần một tài nguyên đủ lớn, nên ghép yêu cầu nhỏ nhất với tài nguyên nhỏ nhất vẫn đáp ứng được nó. Làm vậy giữ các tài nguyên lớn hơn cho các yêu cầu khó hơn phía sau.

Mã giả:

sắp xếp các yêu cầu tăng
sắp xếp các tài nguyên tăng

i = 0
j = 0

trong khi còn yêu cầu và tài nguyên:
    nếu tài nguyên j chưa đủ:
        tăng j
    ngược lại:
        ghép yêu cầu i với tài nguyên j
        tăng i
        tăng j

Không nên mặc định “ghép gần nhất” là đúng. Khoảng cách nhỏ cục bộ có thể làm mất một tài nguyên hiếm cần cho phần tử khác.

Giai đoạn 4 - INTERVAL SCHEDULING, DEADLINE, PHÂN TÀI NGUYÊN THEO THỜI GIAN

Với bài chọn nhiều đoạn không giao nhau nhất, Greedy chuẩn là:

Chọn đoạn kết thúc sớm nhất.

Nếu hai phương án cùng chọn một đoạn đầu tiên, đoạn kết thúc sớm hơn để lại nhiều không gian hơn cho phần còn lại.

Mã giả:

sắp xếp các đoạn theo thời điểm kết thúc tăng

last_end = thời điểm rất nhỏ
answer = 0

duyệt từng đoạn [l, r]:
    nếu l không nhỏ hơn last_end:
        chọn đoạn này
        last_end = r
        tăng answer

Độ phức tạp chủ yếu là:

O(nlog⁡n)O(n\log n)

do sắp xếp.

Không nên thay earliest finish bằng earliest start hoặc shortest interval nếu chưa có chứng minh.

Nếu mỗi interval có trọng số khác nhau và mục tiêu là tối đa tổng trọng số, bài thường chuyển sang DP thay vì Greedy thuần.

Với bài phân phòng hoặc tài nguyên cho các interval, thường cần Heap hoặc Multiset.

Nếu có nhiều tài nguyên tương đương, khi một interval mới bắt đầu, nên tái sử dụng tài nguyên vừa kết thúc phù hợp nhất.

Mã giả:

sắp xếp công việc theo thời điểm bắt đầu

tạo cấu trúc lưu thời điểm kết thúc của các tài nguyên

duyệt từng công việc:
    tìm tài nguyên đã kết thúc và phù hợp nhất

    nếu tìm được:
        tái sử dụng tài nguyên đó
    ngược lại:
        mở tài nguyên mới

    cập nhật thời điểm kết thúc mới

Trong bài deadline đơn vị thời gian, một chiến lược thường gặp là xếp công việc vào slot muộn nhất còn trống trước deadline để giữ các slot sớm cho công việc khác.

Nếu thời lượng công việc khác nhau và mục tiêu là tối đa số công việc hoàn thành, có thể dùng mẫu sort deadline + replace worst.

Giai đoạn 5 - INTERVAL COVERING, STABBING, SWEEP LINE

Cần phân biệt rõ hai bài toán:

  • Interval Scheduling: chọn nhiều đoạn không giao nhau.
  • Interval Covering: dùng ít đoạn nhất để phủ một miền.

Với bài phủ đoạn [L,R][L,R], Greedy chuẩn là:

Tại điểm trái nhất chưa phủ, trong tất cả các đoạn bắt đầu không sau điểm đó, chọn đoạn vươn xa nhất về bên phải.

Mã giả:

sắp xếp interval theo đầu trái tăng

covered = L
i = 0

trong khi covered < R:
    farthest = covered
    best = không có

    trong khi i còn hợp lệ
    và interval[i].left <= covered:
        nếu interval[i].right > farthest:
            farthest = interval[i].right
            best = interval[i]

        tăng i

    nếu farthest == covered:
        kết luận không thể phủ

    chọn best
    covered = farthest

Bất biến là: sau cùng số interval đã chọn, Greedy luôn phủ xa nhất có thể.

Với bài đâm đoạn (interval stabbing), mục tiêu là chọn ít điểm nhất sao cho mỗi interval chứa ít nhất một điểm.

Chiến lược:

Sắp xếp theo đầu phải tăng. Khi gặp một interval chưa được điểm hiện tại phủ, đặt một điểm tại đầu phải của interval đó.

Mã giả:

sắp xếp interval theo đầu phải tăng

chưa có điểm được chọn

duyệt từng interval:
    nếu điểm hiện tại không nằm trong interval:
        chọn điểm tại đầu phải của interval

Nhiều bài hình học có thể biến đổi mỗi đối tượng thành một interval rồi áp dụng đúng hai mẫu trên.

Sweep Line chỉ là cách quét sự kiện. Quyết định chọn/xóa phần tử vẫn cần một invariant Greedy riêng.

Giai đoạn 6 - PRIORITY QUEUE, HUFFMAN, REPLACE-WORST, MARGINAL GAIN

Heap thường dùng khi “ứng viên tốt nhất hiện tại” thay đổi theo thời gian.

Optimal Merge và Huffman

Khi chi phí gộp hai phần tử là tổng của chúng và phần tử mới tiếp tục tham gia các lần gộp sau, luôn gộp hai phần tử nhỏ nhất.

Mã giả:

đưa mọi trọng số vào Min-Heap

answer = 0

trong khi Heap còn hơn một phần tử:
    lấy hai phần tử nhỏ nhất a và b

    c = a + b
    answer += c

    đưa c trở lại Heap

Độ phức tạp:

O(nlog⁡n)O(n\log n)

Không thể chỉ sort một lần, vì tổng mới sinh ra phải quay lại tập ứng viên.

Replace-Worst

Một mẫu rất quan trọng:

  1. xử lý ứng viên theo một thứ tự đã chứng minh;
  2. tạm nhận ứng viên;
  3. nếu ràng buộc bị vi phạm, loại phần tử tệ nhất đang giữ.

Ví dụ với deadline và duration:

sắp xếp công việc theo deadline tăng

total_time = 0
tạo Max-Heap lưu duration đã chọn

duyệt từng công việc:
    nhận công việc
    total_time += duration
    đưa duration vào Heap

    nếu total_time vượt deadline hiện tại:
        bỏ duration lớn nhất trong Heap
        trừ duration đó khỏi total_time

Bất biến là: trên mỗi prefix deadline, ta giữ được số lượng công việc lớn nhất; trong số các tập có cùng số lượng, tổng thời gian đang dùng là nhỏ nhất có thể.

Retroactive Greedy

Đôi khi không nên quyết định ngay.

Ta trì hoãn lựa chọn đến khi bị bắt buộc, rồi chọn phương án tốt nhất trong toàn bộ lịch sử đã khả dụng.

Mã giả tổng quát:

tạo Heap chứa các lựa chọn đã đi qua nhưng chưa dùng

duyệt theo thời gian hoặc vị trí:
    thêm các lựa chọn mới vào Heap

    nếu trạng thái hiện tại không còn khả thi:
        nếu Heap rỗng:
            không có lời giải

        lấy lựa chọn tốt nhất trong Heap
        áp dụng nó để phục hồi tính khả thi

Marginal Gain

Nếu một đối tượng có thể được chọn nhiều lần và lợi ích biên thay đổi sau mỗi lần chọn, Heap phải lưu lợi ích hiện tại.

Sau khi chọn, cần tính lại key rồi đưa trở lại Heap.

Giai đoạn 7 - PHÂN BỔ TÀI NGUYÊN, EXTREME CHOICE, COVERAGE INVARIANT

Một Greedy điển hình của nhóm này là bất biến phủ (coverage invariant).

Giả sử sau khi xử lý một số phần tử, ta tạo được mọi tổng trong:

[1,x][1,x]

Nếu phần tử tiếp theo là aa và:

a≤x+1a\le x+1

thì sau khi thêm aa, ta tạo được mọi tổng trong:

[1,x+a][1,x+a]

Nếu:

a>x+1a>x+1

thì x+1x+1 là tổng nhỏ nhất không thể tạo.

Mã giả:

sắp xếp tăng

covered = 0

duyệt từng a:
    nếu a > covered + 1:
        dừng

    covered += a

đáp án là covered + 1

Chọn gap lớn nhất

Nếu có các điểm đã sắp xếp và cần dùng KK đoạn để phủ chúng, ban đầu một đoạn phủ toàn bộ có độ dài:

an−a1a_n-a_1

Mỗi lần cắt tại gap:

gi=ai+1−aig_i=a_{i+1}-a_i

ta tiết kiệm đúng gig_i.

Vì vậy chọn K−1K-1 gap lớn nhất.

Mã giả:

sắp xếp các điểm

tính mọi gap giữa hai điểm liên tiếp

sắp xếp gap giảm dần

answer = toàn bộ span

trừ khỏi answer K-1 gap lớn nhất

Fractional Knapsack

Nếu tài nguyên có thể chia nhỏ, lấy theo giá trị trên một đơn vị giảm dần là đúng.

Nếu vật không thể chia, chiến lược theo tỉ lệ không đúng tổng quát. Khi đó thường phải dùng DP 0/1 Knapsack.

Bảo toàn tài nguyên linh hoạt

Nếu có hai loại tài nguyên và một loại dùng được trong nhiều tình huống hơn, thường nên giữ loại linh hoạt cho tương lai và tiêu loại ít linh hoạt khi cả hai đều dùng được.

Điều này chỉ đúng khi chứng minh được phép đổi tài nguyên ở hiện tại không làm giảm tập lựa chọn tương lai.

Giai đoạn 8 - CONSTRUCTIVE GREEDY I: INVARIANT, PARITY, PATTERN

Constructive Greedy không nhất thiết tối ưu một giá trị số. Mục tiêu là xây được một cấu hình hợp lệ.

Điểm quan trọng là bất biến có thể hoàn tất (completion invariant):

Sau mỗi bước, phần prefix đã xây vẫn có thể mở rộng thành một nghiệm đầy đủ.

Mã giả:

answer = rỗng

duyệt từng vị trí:
    xét các lựa chọn theo thứ tự Greedy

    chọn lựa chọn đầu tiên
    mà sau khi đặt vào
    phần còn lại vẫn còn khả năng hoàn tất

    nếu không có lựa chọn nào:
        kết luận không thể xây

Dùng parity

Nhiều construction chỉ phụ thuộc chẵn/lẻ.

Nếu hai phần tử được ghép khi có cùng parity, ta chia các chỉ số thành hai nhóm:

  • chẵn;
  • lẻ.

Sau đó chỉ ghép trong cùng nhóm, đồng thời xử lý số phần tử dư sao cho mỗi nhóm còn số lượng chẵn.

Pattern lặp

Nếu ràng buộc chỉ mang tính cục bộ, một mẫu chu kỳ ngắn đôi khi đủ:

abcabcabc…abcabcabc\dots

Nhưng phải kiểm tra toàn bộ cửa sổ/ràng buộc, không chỉ vài vị trí đầu.

Minimal Disruption

Nếu bài yêu cầu sửa cấu hình càng ít càng tốt, thường giữ nguyên mọi phần đang hợp lệ và chỉ tác động vào các vị trí bắt buộc.

Giai đoạn 9 - CONSTRUCTIVE GREEDY II: REPAIR, RECONSTRUCTION, MULTISET

Ở mức cao hơn, ta có thể xây một cấu hình gần đúng rồi sửa các vi phạm cục bộ.

Đây là Greedy repair.

Repair bằng xoay vòng

Nếu một nhóm vị trí bị fixed point hoặc vi phạm cùng kiểu, có thể gom chúng lại rồi xoay các giá trị trong nhóm.

Mã giả:

xây lời giải tạm

thu thập các vị trí còn vi phạm

nếu số vị trí vi phạm đủ để xoay:
    xoay giá trị giữa các vị trí đó
ngược lại:
    xử lý trường hợp biên riêng

Multiset và complement

Nếu mỗi bước cần ghép phần tử lớn nhất với một complement xác định:

đưa mọi phần tử chưa dùng vào Multiset

trong khi Multiset chưa rỗng:
    lấy phần tử lớn nhất x

    tính phần tử cần ghép y

    nếu y không tồn tại:
        thất bại

    xóa đúng một lần xuất hiện của x
    xóa đúng một lần xuất hiện của y

    lưu cặp vào đáp án

Với duplicate, phải xóa đúng một occurrence.

Thử ít khả năng đầu

Có bài mà bước đầu chưa xác định duy nhất, nhưng sau khi cố định lựa chọn đầu tiên thì phần còn lại Greedy hoàn toàn.

Mã giả:

duyệt từng khả năng hợp lý cho bước đầu:
    sao chép trạng thái

    chạy Greedy cho phần còn lại

    nếu thành công:
        xuất đáp án
        kết thúc

Nếu có O(n)O(n) khả năng đầu và mỗi lần xử lý O(nlog⁡n)O(n\log n) thì tổng là:

O(n2log⁡n)O(n^2\log n)

Reconstruction

Khi sort làm mất vị trí ban đầu, lưu cả chỉ số:

mỗi phần tử lưu:
    giá trị
    vị trí gốc

Sau khi quyết định trên thứ tự đã sort, ghi kết quả trở lại vị trí gốc.

Giai đoạn 10 - GREEDY TRÊN CHUỖI/DÃY: LEXICOGRAPHIC, MONOTONIC DELETE, ORDERED SET

Xóa đơn điệu để tối ưu từ điển

Muốn xóa đúng KK chữ số để số còn lại nhỏ nhất, dùng cấu trúc giống Stack đơn điệu.

Khi gặp chữ số mới nhỏ hơn chữ số cuối đang giữ, nếu còn quyền xóa thì xóa chữ số lớn hơn ở trước.

Mã giả:

tạo dãy kết quả rỗng

duyệt từng chữ số d:
    trong khi còn quyền xóa
    và kết quả chưa rỗng
    và chữ số cuối lớn hơn d:

        xóa chữ số cuối
        giảm số lượt xóa

    thêm d vào cuối

nếu vẫn còn lượt xóa:
    xóa bớt từ cuối

Lý do: cải thiện ở vị trí sớm hơn luôn quan trọng hơn mọi cải thiện phía sau khi so sánh từ điển.

Muốn số lớn nhất thì đảo điều kiện so sánh.

Chọn subsequence theo thứ tự gốc

Nếu đề yêu cầu subsequence, không được sort toàn bộ dãy. Ta chỉ được chọn hoặc bỏ phần tử trong thứ tự ban đầu.

Ordered Set và Multiset

Khi mỗi bước cần phần tử lớn nhất không vượt ngưỡng xx, dùng predecessor.

Mã giả:

it = phần tử đầu tiên lớn hơn x

nếu it đang ở đầu tập:
    không có phần tử phù hợp
ngược lại:
    lùi it một bước
    chọn phần tử tại it
    xóa nó khỏi tập nếu mỗi phần tử chỉ dùng một lần

Độ phức tạp mỗi truy vấn:

O(log⁡n)O(\log n)

Patience-style Greedy

Với LIS, với mỗi độ dài kk, ta chỉ cần giữ giá trị kết thúc nhỏ nhất có thể.

Một tail nhỏ hơn có ưu thế hơn tail lớn hơn vì cho nhiều khả năng nối dài hơn.

Mã giả:

tails = rỗng

duyệt từng x:
    tìm vị trí đầu tiên trong tails có giá trị >= x

    nếu không có:
        thêm x vào cuối tails
    ngược lại:
        thay vị trí đó bằng x

Độ phức tạp:

O(nlog⁡n)O(n\log n)

tails không nhất thiết là một LIS thực tế. Nếu cần truy vết, phải lưu thêm chỉ số và parent.

Giai đoạn 11 - GREEDY TOÁN HỌC, BIT, REDISTRIBUTION, ENUMERATION NHỎ

Median và khoảng cách L1L_1

Muốn tối thiểu:

∑i∣xi−p∣\sum_i |x_i-p|

thì một median của dãy là lựa chọn tối ưu.

Nếu các phần tử cuối cùng phải đứng liên tiếp, thường biến đổi:

bi=posi−ib_i = pos_i-i

rồi lấy median của bib_i.

Mã giả:

thu thập vị trí pos của các phần tử cần gom

với mỗi vị trí thứ i:
    b[i] = pos[i] - i

lấy median của b

answer = tổng |b[i] - median|

Thử một số ít thứ tự cực trị

Nếu có hai thao tác cạnh tranh và không thể biết chắc thao tác nào nên làm trước, nhưng chỉ có vài thứ tự đáng xét, thử tất cả các thứ tự đó.

Ví dụ:

tính kết quả khi ưu tiên thao tác A trước B
tính kết quả khi ưu tiên thao tác B trước A

lấy kết quả tốt hơn

Đây là Greedy kết hợp enumeration nhỏ.

Quyết định bit từ cao xuống

Nếu cần tối ưu một số nhị phân dưới ràng buộc:

L≤X≤UL\le X\le U

bit cao quan trọng hơn toàn bộ các bit thấp.

Ta thử quyết định từ bit cao nhất xuống. Chỉ chốt một bit nếu sau khi chốt vẫn tồn tại ít nhất một completion nằm trong miền hợp lệ.

Mã giả:

answer = 0

duyệt bit từ cao xuống:
    thử đặt bit theo hướng tốt cho objective

    nếu vẫn còn ít nhất một số hợp lệ trong [L, U]:
        giữ quyết định đó
    ngược lại:
        chọn giá trị bit còn lại

Redistribution

Khi cần cân bằng số phần tử giữa các lớp modulo, có thể chuyển surplus từ lớp này sang lớp kế tiếp.

Nếu mỗi lần tăng giá trị thêm 11, residue thay đổi:

r→(r+1) mod kr \rightarrow (r+1)\bmod k

Greedy quét tuần tự các residue và chuyển phần dư tới nơi đang thiếu.

Công thức cực trị

Một số bài sau biến đổi cho ra trực tiếp một cận chặt.

Ví dụ nếu mỗi nhóm cần đúng 33 đơn vị lấy từ hai loại tài nguyên A,BA,B, số nhóm không thể vượt:

⌊A+B3⌋\left\lfloor\frac{A+B}{3}\right\rfloor

và cũng không thể vượt số lượng phía thiếu.

Nếu chứng minh được hai cận này đồng thời đạt được, có thể lấy min của các cận tương ứng.

Công thức đóng không tự động là Greedy; vẫn cần giải thích vì sao cách phân phối đạt được cận đó.

Giai đoạn 12 - HYBRID GREEDY: GRAPH, DSU/MST/DIJKSTRA, BINARY SEARCH FEASIBILITY, BOUNDARY

Giai đoạn cuối dùng Greedy như một thành phần trong lời giải lớn hơn.

Kruskal và tính chất lát cắt

Sắp xếp cạnh tăng theo trọng số.

Nếu hai đầu cạnh thuộc hai thành phần khác nhau, chọn cạnh đó.

Mã giả:

sắp xếp các cạnh theo trọng số tăng

khởi tạo DSU

duyệt từng cạnh (u, v, w):
    nếu u và v đang ở hai thành phần khác nhau:
        chọn cạnh
        gộp hai thành phần

Độ phức tạp:

O(mlog⁡m)O(m\log m)

Correctness đến từ tính chất lát cắt (cut property), không đến từ DSU.

DSU chỉ giúp kiểm tra nhanh cạnh có tạo chu trình hay không.

Dijkstra như một Greedy

Với trọng số cạnh không âm, khi đỉnh có khoảng cách tạm thời nhỏ nhất được lấy ra khỏi hàng đợi ưu tiên và trạng thái đó chưa lỗi thời, khoảng cách của nó đã được chốt.

Mã giả:

dist[source] = 0

đưa source vào Min-Heap

trong khi Heap chưa rỗng:
    lấy đỉnh u có dist nhỏ nhất

    nếu đây là bản ghi cũ:
        bỏ qua

    duyệt cạnh u -> v:
        nếu dist[u] + w < dist[v]:
            cập nhật dist[v]
            đưa trạng thái mới của v vào Heap

Độ phức tạp với adjacency list và Heap:

O((n+m)log⁡n)O((n+m)\log n)

Nếu có cạnh âm, invariant chốt khoảng cách có thể bị phá.

Binary Search Answer + Greedy Check

Có những bài không phải Greedy thuần. Greedy chỉ được dùng để kiểm tra một đáp án giả định.

Giả sử ta có hàm:

check(X)check(X)

và tính khả thi đơn điệu theo XX.

Ví dụ chia mảng thành không quá KK đoạn liên tiếp, mỗi đoạn có tổng không vượt XX.

Với XX cố định, chiến lược kiểm tra là nhồi mỗi đoạn dài nhất có thể.

Mã giả:

check(X):
    segments = 1
    current_sum = 0

    duyệt từng a:
        nếu a > X:
            trả false

        nếu current_sum + a <= X:
            current_sum += a
        ngược lại:
            segments += 1
            current_sum = a

    trả segments <= K

Giới hạn tìm kiếm tự nhiên:

lo=max⁡iailo=\max_i a_i hi=∑iaihi=\sum_i a_i

Mã giả:

trong khi lo < hi:
    mid = (lo + hi) / 2

    nếu check(mid) đúng:
        hi = mid
    ngược lại:
        lo = mid + 1

đáp án = lo

Ở dạng này cần hai chứng minh riêng:

  • Greedy trong check(X) là đúng;
  • tính khả thi theo XX là đơn điệu.

Ranh giới của Greedy

Khi lựa chọn cục bộ không chứng minh được an toàn, cần chuyển mô hình.

Các ranh giới thường gặp:

  • weighted interval scheduling chuyển sang DP;
  • 0/1 knapsack chuyển sang DP;
  • cạnh âm phá Dijkstra;
  • matching tổng quát không thể thay bằng ghép cặp cục bộ đơn giản;
  • bài tối ưu ngưỡng đơn điệu thường là Binary Search Answer chứ không phải Greedy thuần.

Năng lực quan trọng nhất ở giai đoạn này là nhận ra Greedy nằm ở đâu trong lời giải và khi nào phải dùng một thuật toán khác.

Phần 1. NỀN TẢNG GREEDY - LOCAL CHOICE, DOMINANCE

Mở

Bài toán Tried AC Độ khó
GD0000001   Làm bánh vòng tối đa (Bitter Alchemy) 0 0 1
GD0000002   Nước ép hỗn hợp (Mix Juice) 0 0 1
GD0000003   Rút tiền tối thiểu (Hit the Lottery) 0 0 1
GD0000004   Luộc trứng (Boiled Eggs) 0 0 1
GD0000005   Thu gom nước tăng lực (Energy Drink Collector) 0 0 1
GD0000006   Chuyến công tác (Business trip) 0 0 1
GD0000007   Cặp song sinh (Twins) 0 0 1
GD0000008   Tanya và đồ chơi (Tanya and Toys) 0 0 1
GD0000009   Dãy con tăng tham lam (Greedily Increasing Subsequence) 0 0 1
GD0000010   Mảng dày (Dense Array) 0 0 1
GD0000011   Rồng (Dragons) 0 0 1
GD0000012   Chú voi con và các bit (Little Elephant and Bits) 0 0 1
GD0000013   Đưa tích về một (Make Product Equal One) 0 0 1
GD0000014   Đợt giảm giá (Sale) 0 0 1
GD0000015   Ổ USB (USB Flash Drives) 0 0 1
GD0000016   Cộng và nhân (Addition and Multiplication) 0 0 1
GD0000017   Phản ví dụ đổi tiền tham lam (Coin Change Counterexample) 0 0 1
GD0000018   Tham lam và DP - Đổi tiền (Greedy vs DP - Coin Change) 0 0 1
QHD0000005   Tối thiểu số đồng xu (Minimizing Coins) 0 0 2
GD0000020   Bộ chứng minh tham lam (Greedy Proof Set) 0 0 1