คู่มือโอลิมปิกคอมพิวเตอร์

Computer Olympiad Guide for Thai High School Students

Greedy

หลักการ

  • วิธีคิดสำหรับปัญหา optimization โดยทั่วไปแล้วจะมีขั้นตอน ซึ่งแต่ละขั้นจะมีตัวเลือกที่แตกต่างกันไป

  • สำหรับหลายๆ ปัญหา optimization อาจจะไม่จำเป็นต้องใช้ dynamic programming เพื่อตัดสินตัวเลือกที่ดีที่สุด

  • วิธีคิดแบบ greedy จะเลือกตัวเลือกที่ดีที่สุดในขณะนั้นๆ เสมอ โดยหวังว่าการเลือกตัวเลือกที่ดีที่สุดใน local จะนำไปสู่วิธีการแก้ปัญหาที่ดีที่สุดใน global

  • วิธีคิดแบบ greedy จะไม่นำไปสู่วิธีการแก้ปัญหาที่ดีที่สุดเสมอไป มีเพียงบางปัญหาเท่านั้นที่สามารถนำไปใช้ได้

  • การพิสูจน์ว่าสามารถใช้วิธีคิดแบบ greedy นั้นทำยาก บางครั้งการลองเขียนโค้ดไปเลยจะสะดวกกว่าการพิสูจน์

โจทย์ greedy ที่ดีมักมีคุณสมบัติว่าเมื่อตัดสินใจเลือกบางอย่างแล้ว เราไม่จำเป็นต้องย้อนกลับมาเปลี่ยนการเลือกนั้นอีก

ปัญหาการทอนเหรียญ

พิจารณาปัญหาที่ต้องการทอนเงินมูลค่า n โดยใช้เหรียญที่มีมูลค่า {c1, c2, c…, ck} จำนวนเหรียญที่น้อยที่สุดที่ต้องใช้เป็นเท่าไร?

ตัวอย่าง

มูลค่าของเหรียญคือ

{1, 2, 5, 10, 20, 50, 100, 200}

และ n = 520

Greedy approach

ตัวอย่างวิธีคิดแบบ greedy คือเลือกเหรียญที่มีมูลค่ามากสุดก่อนเสมอ จนกว่าเราจะสามารถรวมมูลค่าได้ถูกต้อง

คำเตือน วิธีคิดแบบ greedy ไม่จำเป็นจะเป็นวิธีที่ดีที่สุดเสมอไป กรณีนี้ขึ้นอยู่กับประเภทของเหรียญว่าสามารถใช้วิธี greedy ได้หรือไม่

ตัวอย่าง counterexample คือเหรียญ {1, 3, 4} และต้องทอน 6 ถ้าเลือกเหรียญใหญ่สุดก่อนจะได้ 4 + 1 + 1 ใช้ 3 เหรียญ แต่คำตอบที่ดีที่สุดคือ 3 + 3 ใช้ 2 เหรียญ

วิธีพิสูจน์ที่พบบ่อย

Exchange argument

สมมติว่ามีคำตอบที่ดีที่สุดอยู่แล้ว แล้วแสดงว่าสามารถสลับบางส่วนของคำตอบนั้นให้เหมือน greedy choice ได้โดยคำตอบไม่แย่ลง ถ้าทำซ้ำไปเรื่อย ๆ จะได้ว่ามี optimal solution ที่ตรงกับ greedy

Stay-ahead argument

เปรียบเทียบคำตอบของ greedy กับ optimal หลังจากตัดสินใจไปทีละขั้น แล้วพิสูจน์ว่า greedy ไม่เคยตามหลัง optimal เช่น เลือกงานได้ไม่น้อยกว่า หรือจบเร็วกว่าเสมอ

ปัญหาการจัดตารางเวลา

พิจารณาปัญหาที่ต้องการเรียง n งานซึ่งมีเวลาเริ่มและเวลาจบแตกต่างกันไป โดยต้องการให้สามารถจัดเรียงงานให้ได้จำนวนมากที่สุด

ตัวอย่าง

งาน

เวลาเริ่ม

เวลาจบ

A

1

3

B

2

5

C

3

9

D

6

8

คำอธิบาย

แนวคิดที่เลือกงานถัดไปที่เลิกเร็วที่สุดเสมอ สามารถอ้างได้ว่าหากเราเลือกงานที่เลิกช้ากว่างานที่เราเลือก เราจะได้จำนวนงานอย่างมากที่สุดเท่ากับจำนวนงานที่เราเลือกด้วยวิธีเดิม เพราะฉะนั้นการเลือกงานที่เลิกช้ากว่าจะไม่สามารถให้คำตอบที่ดีกว่าได้

ตัวอย่าง implementation

vector<pair<int, int>> job; // {end, start}
sort(job.begin(), job.end());

int last_end = -1;
int ans = 0;

for (auto [end, start] : job) {
  if (start >= last_end) {
    ans++;
    last_end = end;
  }
}

โจทย์ที่มักใช้ greedy

  • interval scheduling: เลือกงานที่จบเร็วสุด

  • minimum number of rooms: ใช้ sweep line หรือ priority queue

  • Huffman coding: รวมค่าน้อยสุดสองตัวเสมอ

  • minimum spanning tree: Kruskal และ Prim เป็น greedy

  • Dijkstra: เลือก node ที่ distance น้อยสุดที่ยังไม่ finalize

Checklist

  • ระบุ objective ว่าต้อง maximize หรือ minimize อะไร

  • เขียน greedy choice ให้ชัดเจน เช่น “เลือกงานที่จบเร็วสุด”

  • หา counterexample ถ้ามี เพื่อไม่ใช้ greedy ผิดโจทย์

  • ถ้าต้องพิสูจน์ ให้ลอง exchange argument ก่อน