0Pricing
DSA Interview Prep · บทเรียน

ละโมบกับ 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. ละโมบกับ DP: ควรใช้แบบใด
  2. การจัดตารางช่วงเวลาและการรวมช่วง
  3. เกมกระโดด I และ II
  4. ตัวจัดตารางงานและสถานีเติมน้ำมัน
← กลับไปที่ DSA Interview Prep