ละโมบกับ DP: ควรใช้แบบใด
ระบุลักษณะสำคัญของปัญหาที่แก้ได้ด้วยวิธีละโมบ เทียบกับปัญหาที่ต้องใช้ DP โดยอาศัยคุณสมบัติการเลือกแบบละโมบและข้อโต้แย้งแบบสับเปลี่ยน
ละโมบกับ DP: ควรใช้แบบใด เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
ภาพรวมอัลกอริทึมแบบละโมบและ DP
ทั้ง แบบละโมบ และ การเขียนโปรแกรมพลวัตใช้แก้ปัญหาการหาค่าที่เหมาะที่สุด — เช่น การหาค่าสูงสุด ค่าต่ำสุด หรือการจัดวางที่ดีที่สุด แบบละโมบจะเลือก ทางเลือกที่ดีที่สุดเฉพาะหน้า ในแต่ละขั้น โดยไม่ทบทวนการตัดสินใจก่อนหน้า ส่วน DP จะสำรวจความเป็นไปได้ทั้งหมด แต่ใช้การบันทึกผลลัพธ์เพื่อหลีกเลี่ยงการคำนวณซ้ำ การรู้ว่าควรใช้แนวทางใดช่วยประหยัดเวลาในการแก้ข้อผิดพลาดจากอัลกอริทึมแบบละโมบที่ไม่ถูกต้อง หรือจากตาราง DP ที่ซับซ้อนเกินความจำเป็น
# Greedy: always take the locally best option
# Example: coin change with coins [1, 5, 10, 25]
# Greedy: take as many 25s as possible, then 10s, etc.
# This works for standard denominations but NOT all coin sets!
# DP: explore all possibilities via memoisation
# Example: coin change with coins [1, 3, 4] and target 6
# Greedy would pick 4, then 1, 1 → 3 coins
# DP finds: 3 + 3 → 2 coins (optimal!)
print('Greedy can fail when local optimum != global optimum')คุณสมบัติของการเลือกแบบละโมบ
ปัญหาจะมี คุณสมบัติของการเลือกแบบละโมบ เมื่อสามารถสร้างคำตอบที่ดีที่สุดโดยรวมได้เสมอด้วยการเลือกทางเลือกที่ดีที่สุดเฉพาะหน้า (แบบละโมบ) ตามหลักการอย่างเป็นทางการ: มีคำตอบที่ดีที่สุดอย่างน้อยหนึ่งคำตอบที่เริ่มต้นด้วยการเลือกแบบละโมบ ดังนั้นเราจึงไม่จำเป็นต้อง backtrack โดยทั่วไป การพิสูจน์คุณสมบัตินี้ใช้ อาร์กิวเมนต์การสับเปลี่ยน: สมมติว่าคำตอบที่ดีที่สุดใด ๆ ไม่มีการเลือกแบบละโมบ แล้วแสดงให้เห็นว่าสามารถสลับการเลือกนั้นเข้าไปได้โดยไม่ทำให้ผลลัพธ์แย่ลง
# Exchange argument example: Activity Selection
# Greedy: always pick the activity that ends earliest
# Proof: suppose optimal solution starts with activity A (not earliest-ending)
# Let G be the earliest-ending activity.
# Replace A with G in the solution:
# - G ends no later than A, so G does not conflict with any activity A allowed
# - The solution remains valid with at least as many activities
# Therefore greedy choice (earliest end) is always safe.
activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14)]
activities.sort(key=lambda x: x[1]) # sort by end time
print('Sorted by end:', activities[:4], '...')โครงสร้างย่อยที่เหมาะที่สุด
ทั้งอัลกอริทึมแบบละโมบและ DP ต้องอาศัย โครงสร้างย่อยที่เหมาะที่สุด: คำตอบที่ดีที่สุดของปัญหาทั้งหมดต้องประกอบด้วยคำตอบที่ดีที่สุดของปัญหาย่อย ความแตกต่างอยู่ที่ว่าเราสามารถหาคำตอบที่ดีที่สุดของปัญหาย่อยได้ด้วยวิธีละโมบ (โดยไม่ต้องสำรวจทุกตัวเลือก) หรือจำเป็นต้องเปรียบเทียบตัวเลือกหลายแบบ หากคุณเลือกทางเลือกหนึ่งแล้วปัญหาย่อยที่เหลือมีโครงสร้างเหมือนเดิม แบบละโมบก็ใช้ได้ แต่หากต้องเปรียบเทียบหลายทางเลือก ให้ใช้ DP
# Greedy works: activity selection
# Making the greedy choice (earliest-ending) leaves a sub-problem
# that is structurally identical (activity selection on remaining activities)
# and the greedy choice for the sub-problem is still valid.
# DP needed: 0/1 knapsack
# After choosing to include/exclude item i, the remaining sub-problem
# depends on WHICH item we chose — different choices yield different sub-problems.
# No single greedy rule works for all inputs.
print('Greedy: sub-problem is unique after each choice')
print('DP: sub-problem depends on which choice was made')สัญญาณของปัญหาย่อยที่ทับซ้อนกันซึ่งบ่งชี้ว่าใช้ DP
หากมีการแก้ปัญหาย่อยเดียวกันหลายครั้งในการแยกปัญหาแบบเรียกซ้ำ จำเป็นต้องใช้ DP พร้อมการบันทึกผลลัพธ์ ให้เขียนต้นไม้การเรียกซ้ำและมองหาโหนดที่ปรากฏซ้ำ สำหรับฟีโบนักชี fib(3) จะถูกคำนวณสองครั้งในต้นไม้ของ fib(5) สำหรับการทอนเหรียญด้วยเหรียญ [1,3,4] และเป้าหมาย 6 ปัญหาย่อยสำหรับเป้าหมาย 3, 2 และ 1 จะปรากฏหลายครั้ง ปัญหาย่อยที่ทับซ้อนกันและโครงสร้างย่อยที่เหมาะที่สุดรวมกันเป็น DP
# Recursion tree for coin change [1,3,4], target=6
# bt(6) → bt(5) → bt(4) → bt(3) (repeated!)
# → bt(2) → bt(1) (repeated!)
# → bt(3) (repeated!)
# → bt(2) (repeated!)
# Without memoisation: exponential time
# With DP table: O(target * len(coins)) time
def coin_change_dp(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change_dp([1, 3, 4], 6)) # 2 (3+3)
print(coin_change_dp([2], 3)) # -1 (impossible)ปัญหาแบบละโมบคลาสสิก
ปัญหาที่พิสูจน์ได้ว่าใช้วิธีละโมบได้ถูกต้องมีดังนี้: (1) การจัดตารางกิจกรรม/ช่วงเวลา — ใช้วิธีละโมบโดยเลือกเวลาสิ้นสุดที่เร็วที่สุด (2) ต้นไม้ทอดขยายต่ำสุด — ใช้อัลกอริทึมของพริมและครูสคัล (3) การเข้ารหัสฮัฟฟ์แมน — รวมโหนดสองโหนดที่มีความถี่ต่ำที่สุดเสมอ (4) ปัญหากระเป๋าเป้แบบแบ่งส่วนได้ — เลือกรายการตามอัตราส่วนค่าต่อน้ำหนักที่สูงที่สุด (5) เกมกระโดด — ติดตามดัชนีสูงสุดที่เข้าถึงได้ ปัญหาทั้งหมดนี้มีเหตุผลรองรับด้วยการพิสูจน์แบบอาร์กิวเมนต์การสับเปลี่ยน
# Fractional Knapsack: greedy works
def fractional_knapsack(items, capacity):
# Sort by value/weight ratio descending
items.sort(key=lambda x: x[1]/x[0], reverse=True)
total = 0
for weight, value in items:
if capacity <= 0: break
take = min(weight, capacity)
total += take * (value / weight)
capacity -= take
return total
items = [(10, 60), (20, 100), (30, 120)] # (weight, value)
print(fractional_knapsack(items, 50)) # 240.0
# 0/1 Knapsack: greedy FAILS
# Must use DP (can't take fractions)เมื่อวิธีละโมบล้มเหลว: ตัวอย่างโต้แย้ง
การหาตัวอย่างโต้แย้งเป็นวิธีที่เร็วที่สุดในการหักล้างสมมติฐานแบบละโมบ สำหรับการทอนเหรียญด้วยเหรียญ [1, 3, 4] และเป้าหมาย 6 วิธีละโมบ (เลือกค่ามากที่สุดก่อน) จะเลือก 4 แล้วตามด้วย 1+1 รวมเป็นเหรียญ 3 เหรียญ ส่วน DP พบว่า 3+3 ใช้เพียง 2 เหรียญ สำหรับปัญหากระเป๋าเป้า 0/1 วิธีละโมบตามอัตราส่วนจะเลือกสิ่งของที่มีอัตราส่วนดีที่สุด แต่อาจพลาดการผสมผสานที่ใช้ความจุได้ดีกว่า หากคุณสร้างตัวอย่างโต้แย้งได้ภายในเวลาไม่ถึงหนึ่งนาที ให้เปลี่ยนไปใช้ DP
# Counterexample: coin change with non-standard coins
def greedy_coins(coins, amount):
coins.sort(reverse=True)
count = 0
for c in coins:
while amount >= c:
amount -= c
count += 1
return count if amount == 0 else -1
def dp_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a-c] + 1)
return dp[amount] if dp[amount] < float('inf') else -1
coins, target = [1, 3, 4], 6
print('Greedy:', greedy_coins(coins[:], target)) # 3 (4+1+1)
print('DP: ', dp_coins(coins, target)) # 2 (3+3)ตารางเปรียบเทียบ: แบบละโมบกับ DP
ความแตกต่างสำคัญเมื่อวางเทียบกัน: ความซับซ้อนด้านเวลา — โดยทั่วไปแบบละโมบมีความซับซ้อน O(n log n) (โดยมีการเรียงลำดับเป็นปัจจัยหลัก); DP มีความซับซ้อน O(n × สถานะ) ความซับซ้อนด้านพื้นที่ — แบบละโมบใช้พื้นที่เสริม O(1); DP ใช้พื้นที่ O(สถานะ) ความถูกต้อง — แบบละโมบต้องมีการพิสูจน์ ส่วน DP ถูกต้องเสมอหากกำหนดสถานะและความสัมพันธ์เวียนเกิดได้ถูกต้อง การประยุกต์ใช้ — แบบละโมบเหมาะกับการจัดตาราง ต้นไม้ทอดขยาย และฮัฟฟ์แมน; DP เหมาะกับปัญหากระเป๋าเป้ การจัดแนวลำดับ และเส้นทางสั้นที่สุดที่มีน้ำหนักติดลบ
# Performance comparison
import time
def time_it(func, *args):
start = time.time()
result = func(*args)
return result, time.time() - start
# Large coin change test
coins = [1, 5, 10, 25, 100]
amount = 10000
def dp_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a-c]+1)
return dp[amount]
result, elapsed = time_it(dp_coins, coins, amount)
print(f'DP coin change(amount={amount}): {result} coins in {elapsed:.4f}s')กรอบการตัดสินใจ
ผังงานสำหรับการตัดสินใจในการสัมภาษณ์: (1) คุณพิสูจน์คุณสมบัติของการเลือกแบบละโมบด้วยอาร์กิวเมนต์การสับเปลี่ยนได้หรือไม่ หากได้ → ใช้วิธีละโมบ (2) ปัญหาย่อยมีการทับซ้อนกันหรือไม่ (มีการเข้าถึงสถานะเดียวกันได้หลายทาง) หากใช่ → ใช้ DP (3) ปัญหาต้องการให้ นับ หรือ แจกแจงคำตอบทั้งหมดหรือไม่ → ใช้ DP หรือการย้อนกลับ (4) ปัญหาต้องการค่าที่ดีที่สุดเพียงค่าเดียวและมีลำดับตามธรรมชาติหรือไม่ ให้สงสัยว่าอาจใช้วิธีละโมบ (5) เมื่อไม่แน่ใจ ให้เขียนโค้ด DP — วิธีนี้ถูกต้องเสมอหากความสัมพันธ์เวียนเกิดถูกต้อง แม้จะช้ากว่าก็ตาม
# Decision questions to ask:
questions = [
'1. Is there a natural ordering (by time, ratio, size)?',
'2. Does making the greedy choice leave a smaller same-type problem?',
'3. Can I construct a counterexample quickly?',
'4. Are sub-problems reused across different choice sequences?',
'5. Does the problem involve counting or listing (not just optimising)?',
]
for q in questions:
print(q)
print()
print('Greedy signals: scheduling, spanning tree, Huffman, jump game')
print('DP signals: knapsack, edit distance, LCS, coin change (general)')ปัญหาช่วงเวลา: แบบละโมบกับ DP
ปัญหาช่วงเวลาแบ่งได้เป็นกลุ่มที่ใช้วิธีละโมบและกลุ่มที่ใช้ DP ช่วงเวลาที่ไม่ทับซ้อนกัน (remove ให้น้อยที่สุด): ใช้ sort ตามเวลา end แล้วเลือกช่วงเวลาแบบละโมบ — วิธีละโมบพิสูจน์ได้ว่าให้คำตอบดีที่สุด การจัดตารางช่วงเวลาแบบมีน้ำหนัก (ทำให้น้ำหนักรวมสูงสุด): จำเป็นต้องใช้ DP เพราะช่วงเวลาที่มีน้ำหนักมากอาจทับซ้อนกับช่วงเวลาน้ำหนักน้อยหลายช่วง จึงต้องเปรียบเทียบเซตย่อยที่ถูกต้องทั้งหมด ปัจจัยที่ใช้แยกความแตกต่างคือช่วงเวลาทั้งหมดมี น้ำหนักเท่ากัน (แบบละโมบ) หรือมี น้ำหนักแตกต่างกัน (DP)
# Non-overlapping intervals: greedy works
def erase_overlap_intervals(intervals):
if not intervals: return 0
intervals.sort(key=lambda x: x[1])
count = 0
last_end = float('-inf')
for start, end in intervals:
if start >= last_end:
last_end = end # keep this interval
else:
count += 1 # remove this interval
return count
print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]])) # 1
print(erase_overlap_intervals([[1,2],[1,2],[1,2]])) # 2การสังเกตสัญญาณของปัญหา
สัญญาณที่พบบ่อยในโจทย์ปัญหา: 'จำนวนการดำเนินการน้อยที่สุด', 'กำไรสูงสุด', 'การเลือกที่เหมาะที่สุด' → อาจใช้วิธีละโมบหรือ DP ให้ตรวจสอบการทับซ้อน 'นับจำนวนวิธี' → ใช้ DP เสมอ 'หาตารางเวลาที่ถูกต้องใด ๆ' → อาจใช้วิธีละโมบ 'ทุกความเป็นไปได้' → ใช้การย้อนกลับ 'ไม่สามารถเลือกสิ่งที่อยู่ติดกันได้' → ใช้ DP (ปัญหาขโมยขึ้นบ้าน) 'การประชุม ช่วงเวลา งาน' → มักใช้วิธีละโมบ การเชื่อมโยงสัญญาณเข้ากับกลุ่มอัลกอริทึมช่วยให้วินิจฉัยโจทย์สัมภาษณ์ได้เร็วขึ้น
# Signal-to-algorithm mapping
signals = {
'minimum steps/coins/operations': 'DP (unless trivially greedy)',
'maximum profit/value with constraint': 'DP (knapsack family)',
'count ways to reach/achieve': 'DP (always)',
'all combinations/permutations': 'Backtracking',
'schedule tasks within time': 'Greedy (sort by deadline/end)',
'cannot pick adjacent': 'DP (house robber pattern)',
'free to pick any subset': 'DP or Greedy (check overlap)',
'interval merging/selecting': 'Greedy (sort by end time)',
}
for signal, algo in signals.items():
print(f'{signal!r}: → {algo}')การพิสูจน์ความถูกต้องของวิธีละโมบ
ในการพิสูจน์ว่าอัลกอริทึมแบบละโมบถูกต้อง ให้ใช้อาร์กิวเมนต์การสับเปลี่ยน: (1) สมมติว่ามีคำตอบที่ดีที่สุด OPT ซึ่งแตกต่างจากคำตอบแบบละโมบ G ที่การเลือกครั้งแรก (2) แสดงให้เห็นว่าสามารถสลับการเลือกแบบละโมบเข้าไปใน OPT ได้โดยไม่เพิ่มค่าของเป้าหมาย (3) ด้วยการอุปนัย คำตอบแบบละโมบจึงดีเทียบเท่ากับคำตอบที่ดีที่สุดใด ๆ ในการสัมภาษณ์ คุณไม่จำเป็นต้องอธิบายการพิสูจน์เต็มรูปแบบ แต่การอธิบายแนวคิดของอาร์กิวเมนต์การสับเปลี่ยนจะแสดงให้เห็นถึงความเข้าใจอย่างลึกซึ้ง
# Exchange argument demo: earliest-finish-time activity selection
# Suppose OPT starts with activity A (not earliest-ending)
# Let G = earliest-ending activity available
# A.end >= G.end (G ends earlier or same time)
# Swap A for G in OPT:
# - G.end <= A.end, so G does not conflict with anything A allowed after it
# - OPT remains valid with the same number of activities
# - Repeat: after swap, OPT begins with G, matching greedy first choice
# By induction, OPT can be transformed to match G activity by activity
# without losing activities → greedy is optimal
print('Exchange argument: any OPT can be modified to match Greedy without loss')
print('This proves Greedy >= OPT in objective value')ตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโค้ดจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า: วิธีละโมบถูกต้องเมื่อมีคุณสมบัติของการเลือกแบบละโมบ — ซึ่งพิสูจน์ได้ด้วยอาร์กิวเมนต์การสับเปลี่ยน, จำเป็นต้องใช้ DP เมื่อปัญหาย่อยทับซ้อนกัน (เข้าถึงปัญหาย่อยเดียวกันได้หลายทาง) และไม่สามารถแก้ได้ด้วยกฎแบบละโมบเพียงกฎเดียว และ วิธีที่เร็วที่สุดในการหักล้างสมมติฐานแบบละโมบคือการสร้างตัวอย่างโต้แย้งด้วยข้อมูลนำเข้าที่ไม่เป็นมาตรฐาน บทถัดไป เราจะแก้ปัญหาการจัดตารางและการรวมช่วงเวลาโดยใช้แนวทางแบบละโมบที่เรียงลำดับตามเวลา end
คำถามที่พบบ่อย
บทเรียน “ละโมบกับ DP: ควรใช้แบบใด” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ละโมบกับ DP: ควรใช้แบบใด” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ละโมบกับ DP: ควรใช้แบบใด”
ระบุลักษณะสำคัญของปัญหาที่แก้ได้ด้วยวิธีละโมบ เทียบกับปัญหาที่ต้องใช้ DP โดยอาศัยคุณสมบัติการเลือกแบบละโมบและข้อโต้แย้งแบบสับเปลี่ยน คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “ละโมบกับ DP: ควรใช้แบบใด” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- ละโมบกับ DP: ควรใช้แบบใด
- การจัดตารางช่วงเวลาและการรวมช่วง
- เกมกระโดด I และ II
- ตัวจัดตารางงานและสถานีเติมน้ำมัน