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

การทอนเหรียญและบันไดต้นทุนต่ำสุด

กำหนดความสัมพันธ์เวียนเกิดของการทอนเหรียญและการปีนบันไดด้วยต้นทุนต่ำสุด เลือกทิศทาง DP ที่เหมาะสม และติดตามตารางด้วยตนเอง

การทอนเหรียญและบันไดต้นทุนต่ำสุด เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

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

การทอนเหรียญ (LeetCode #322) ให้ชุดมูลค่าเหรียญและจำนวนเงินเป้าหมายมา จงหาจำนวนเหรียญที่น้อยที่สุดที่ต้องใช้เพื่อให้ได้จำนวนเงินดังกล่าวพอดี คุณมีเหรียญแต่ละมูลค่าได้ไม่จำกัดจำนวน นี่เป็น รูปแบบหนึ่งของปัญหาเป้แบบไม่จำกัดจำนวน ซึ่งสิ่งของแต่ละชิ้น (เหรียญ) สามารถใช้ซ้ำได้กี่ครั้งก็ได้ ปัญหานี้เป็นหนึ่งในโจทย์ DP ที่สำคัญที่สุด เพราะทดสอบความสามารถในการสร้างสูตรเวียนเกิดขึ้นมาเองตั้งแต่ต้น

# Problem examples:
# coins=[1,5,6,9], amount=11 -> 2 (5+6 or 2+9? no: 5+6=11 YES)
# coins=[2],       amount=3  -> -1 (impossible)
# coins=[1,2,5],   amount=11 -> 3 (5+5+1)
# coins=[186,419,83,408], amount=6249 -> 20

# Key choices:
# - Try each coin denomination at each step
# - Minimum coins = 1 + minimum(coins to make amount - coin)
# - If amount < 0: impossible
# - If amount = 0: done (0 coins)

print('Coin change: unbounded knapsack, find minimum count')

การทอนเหรียญ: การอนุมานสูตรเวียนเกิด

กำหนดให้ dp[i] = จำนวนเหรียญขั้นต่ำที่ใช้ทำจำนวนเงิน i สำหรับจำนวนเงินแต่ละค่า i ให้ลองใช้เหรียญแต่ละเหรียญ c หาก i >= c แล้ว dp[i] = min(dp[i], 1 + dp[i-c]) ค่า 1 หมายถึงเหรียญที่เพิ่งใช้ไป ส่วน dp[i-c] คือคำตอบที่ดีที่สุดสำหรับจำนวนเงินที่เหลือ แนวคิดนี้ตั้งอยู่บนสมมติฐานว่ามีเหรียญไม่จำกัดจำนวน กรณีฐานคือ dp[0] = 0 ส่วนรายการอื่น ๆ ให้เริ่มต้นด้วยค่าอนันต์เพื่อแทนค่าที่ยังไม่สามารถทำให้ได้

def coin_change(coins, amount):
    # dp[i] = min coins to make amount i
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0  # base: 0 coins for amount 0

    for i in range(1, amount + 1):
        for coin in coins:
            if i >= coin and dp[i - coin] != float('inf'):
                dp[i] = min(dp[i], 1 + dp[i - coin])

    return dp[amount] if dp[amount] != float('inf') else -1

print(coin_change([1, 5, 6, 9], 11))  # 2
print(coin_change([2], 3))             # -1
print(coin_change([1, 2, 5], 11))      # 3

# Trace dp for coins=[1,5] amount=6:
# dp[0]=0, dp[1]=1, dp[2]=2, dp[3]=3, dp[4]=4, dp[5]=1, dp[6]=2

การทอนเหรียญ: เหตุใดวิธีเลือกแบบโลภจึงใช้ไม่ได้

วิธีเลือกแบบโลภ ซึ่งเลือกเหรียญมูลค่าสูงสุดที่ยังใช้ได้เสมอ ใช้แก้ปัญหาการทอนเหรียญไม่ได้ ตัวอย่างเช่น coins=[1, 3, 4], amount=6 วิธีเลือกแบบโลภจะเลือก 4 แล้วเลือก 1+1 รวมเป็น 3 เหรียญ แต่คำตอบที่ดีที่สุดคือ 3+3 ซึ่งใช้เพียง 2 เหรียญ วิธีเลือกแบบโลภใช้ได้กับมูลค่าเหรียญมาตรฐาน (1, 5, 10, 25 เซนต์) เพราะมูลค่าเหล่านี้บังเอิญมีคุณสมบัติที่ทำให้วิธีเลือกแบบโลภใช้ได้ แต่สำหรับชุดเหรียญทั่วไป จำเป็นต้องใช้ DP นี่เป็นประเด็นสำคัญในการสัมภาษณ์ การระบุว่าวิธีเลือกแบบโลภใช้ไม่ได้และอธิบายเหตุผลได้ แสดงถึงทักษะการคิดวิเคราะห์ที่ดี

# Greedy failure example:
# coins=[1,3,4], amount=6
# Greedy: 4 (rem=2), 1 (rem=1), 1 (rem=0) -> 3 coins
# Optimal: 3 (rem=3), 3 (rem=0) -> 2 coins

def coin_change_greedy_wrong(coins, amount):
    coins_sorted = sorted(coins, reverse=True)
    count = 0
    for coin in coins_sorted:
        while amount >= coin:
            amount -= coin
            count += 1
    return count if amount == 0 else -1

print('Greedy:', coin_change_greedy_wrong([1,3,4], 6))  # 3 (WRONG)
print('DP:    ', coin_change([1,3,4], 6))               # 2 (CORRECT)

การทอนเหรียญ II: นับจำนวนวิธี

การทอนเหรียญ II (LeetCode #518) ถามถึงจำนวนวิธีในการทำจำนวนเงินให้ได้ตามต้องการ ไม่ใช่จำนวนเหรียญขั้นต่ำ สูตรเวียนเกิดจึงเปลี่ยนไป โดยแทนที่จะใช้ค่าต่ำสุด ให้ใช้ผลรวม ใช้ dp[i] += dp[i-coin] สำหรับเหรียญแต่ละเหรียญ ลำดับการเติมค่ามีความสำคัญ หากต้องการนับแต่ละชุดเหรียญ เพียงครั้งเดียว ให้วนรอบเหรียญในวงรอบชั้นนอก และวนรอบจำนวนเงินในวงรอบชั้นใน หากสลับลำดับวงรอบ จะเป็นการนับการเรียงลำดับแทนชุดเหรียญ ซึ่งเป็นคนละปัญหากัน

def coin_change_ii(coins, amount):
    # dp[i] = number of ways to make amount i
    dp = [0] * (amount + 1)
    dp[0] = 1  # one way to make amount 0: use no coins

    # Outer loop: coins -- ensures each coin type processed once
    for coin in coins:
        # Inner loop: amounts
        for i in range(coin, amount + 1):
            dp[i] += dp[i - coin]

    return dp[amount]

print(coin_change_ii([1, 2, 5], 5))   # 4: [1,1,1,1,1],[1,1,1,2],[1,2,2],[5]
print(coin_change_ii([2], 3))          # 0: impossible
print(coin_change_ii([10], 10))        # 1

# Key: coin outer, amount inner = COMBINATIONS (unordered)
# Reverse (amount outer, coin inner) = PERMUTATIONS (ordered)

บันไดต้นทุนต่ำสุด: ปัญหา

บันไดที่มีต้นทุนต่ำสุด (LeetCode #746) ให้บันไดที่แต่ละขั้นมีค่าใช้จ่าย คุณสามารถก้าวครั้งละ 1 หรือ 2 ขั้นได้ จงหาค่าใช้จ่ายต่ำสุดในการขึ้นไปถึงยอดบันได ซึ่งอยู่ถัดจากขั้นสุดท้ายไปหนึ่งขั้น คุณสามารถเริ่มที่ขั้น 0 หรือขั้น 1 ได้โดยไม่เสียค่าใช้จ่าย ปัญหานี้ผสานสูตรเวียนเกิดของการขึ้นบันไดเข้ากับรูปแบบการหาค่าใช้จ่ายต่ำสุดของการทอนเหรียญได้อย่างลงตัว จึงเป็นสะพานเชื่อมตามธรรมชาติระหว่างปัญหาทั้งสองแบบ

# cost = [10, 15, 20]
# Pay cost[i] to leave step i
# You can step to i+1 or i+2
# Goal: reach top (index 3) with minimum cost

# Path options:
# Start at 0: cost 10, go to 2: cost 20, done -> 30
# Start at 1: cost 15, go to 3: done -> 15  <- OPTIMAL
# Start at 0: cost 10, go to 1: cost 15 -> 25

cost = [10, 15, 20]
# Optimal: start at step 1, pay 15, jump to top -> cost = 15
print('Expected:', 15)

บันไดต้นทุนต่ำสุด: สูตรเวียนเกิด

กำหนดให้ dp[i] = ค่าใช้จ่ายต่ำสุดในการไปถึงขั้น i คุณจะมาถึงขั้น i ได้โดยจ่าย cost[i-1] จากขั้น i-1 หรือ cost[i-2] จากขั้น i-2 ดังนั้น dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]) กรณีฐานคือ dp[0] = 0 ซึ่งหมายถึงการเริ่มต้นก่อนบันไดโดยไม่เสียค่าใช้จ่าย และ dp[1] = 0 ซึ่งหมายถึงสามารถเริ่มที่ขั้น 1 ได้โดยไม่เสียค่าใช้จ่าย คำตอบคือ dp[n] โดย n = len(cost)

def min_cost_climbing_stairs(cost):
    n = len(cost)
    # dp[i] = minimum cost to reach step i
    # Steps 0 to n; step n is the top (goal)
    dp = [0] * (n + 1)
    # dp[0] = 0 (free to start here)
    # dp[1] = 0 (free to start here)
    for i in range(2, n + 1):
        dp[i] = min(dp[i-1] + cost[i-1],   # step from i-1
                    dp[i-2] + cost[i-2])    # jump from i-2
    return dp[n]

print(min_cost_climbing_stairs([10, 15, 20]))      # 15
print(min_cost_climbing_stairs([1,100,1,1,1,100,1,1,100,1]))  # 6

บันไดต้นทุนต่ำสุด: การปรับให้ใช้พื้นที่น้อยลง

เนื่องจาก dp[i] ขึ้นอยู่กับเพียง dp[i-1] และ dp[i-2] เราจึงลดพื้นที่ให้เหลือ O(1) ได้ด้วยตัวแปรสองตัว เช่นเดียวกับฟีโบนักชี ให้แทนที่อาร์เรย์ด้วย prev2 และ prev1 แล้วอัปเดตค่าทั้งสองตัวในแต่ละขั้น นี่เป็นการปรับให้เหมาะสมแบบบรรทัดเดียวที่เป็นมาตรฐาน ซึ่งผู้สัมภาษณ์คาดหวังให้กล่าวถึงหลังจากนำเสนอวิธีแก้ด้วยตารางขนาด O(n) แล้ว ควรกล่าวถึงเรื่องนี้ก่อนเสมอว่า “เราลดพื้นที่ให้เหลือ O(1) ได้ เพราะต้องใช้เพียงค่าสองค่าล่าสุดเท่านั้น”

def min_cost_optimised(cost):
    n = len(cost)
    prev2, prev1 = 0, 0  # dp[0] and dp[1]
    for i in range(2, n + 1):
        curr = min(prev1 + cost[i-1], prev2 + cost[i-2])
        prev2, prev1 = prev1, curr
    return prev1

print(min_cost_optimised([10, 15, 20]))  # 15
print(min_cost_optimised([1,100,1,1,1,100,1,1,100,1]))  # 6

# Alternative: directly use cost array as rolling storage
def min_cost_v2(cost):
    n = len(cost)
    for i in range(2, n):
        cost[i] += min(cost[i-1], cost[i-2])
    return min(cost[-1], cost[-2])

from copy import deepcopy
cost_test = [10,15,20]
print(min_cost_v2(deepcopy(cost_test)))  # 15

การกำหนดรูปแบบ DP ทางเลือก

ปัญหาบางอย่างมีรูปแบบ DP ที่ถูกต้องได้หลายรูปแบบ สำหรับบันไดต้นทุนต่ำสุด คุณอาจกำหนดให้ dp[i] = ค่าใช้จ่ายต่ำสุดในการ LEAVE ขั้น i โดยจ่าย cost[i] แล้วเลือกไปยังขั้น i+1 หรือ i+2 จากนั้น dp[i] = cost[i] + min(dp[i+1], dp[i+2]) โดยเติมค่าจากขวาไปซ้าย และคำตอบคือ min(dp[0], dp[1]) ทั้งสองรูปแบบถูกต้อง ควรฝึกอธิบายว่าคุณเลือกรูปแบบใดและเพราะเหตุใด เพราะจะแสดงให้เห็นถึงความคล่องแคล่วด้าน DP

def min_cost_alternative(cost):
    n = len(cost)
    # dp[i] = min cost when starting FROM step i
    # Fill right to left
    dp = cost[:] + [0]  # dp[n] = 0 (already at top)
    for i in range(n - 1, -1, -1):
        # Pay cost[i], then choose i+1 or i+2
        if i + 2 <= n:
            dp[i] = cost[i] + min(dp[i+1], dp[i+2])
        else:
            dp[i] = cost[i] + dp[i+1]
    # Can start at step 0 or step 1
    return min(dp[0], dp[1])

print(min_cost_alternative([10, 15, 20]))  # 15
print(min_cost_alternative([1,100,1,1,1,100,1,1,100,1]))  # 6

การเชื่อมโยงการทอนเหรียญกับบันได

ทั้งการทอนเหรียญและบันไดต้นทุนต่ำสุดเป็นตัวอย่างของรูปแบบ DP เดียวกัน นั่นคือ ในแต่ละขั้น ให้เลือกจากตัวเลือกจำนวนจำกัด แล้วปรับวัตถุประสงค์ให้ดีที่สุดตลอดลำดับการเลือก ความแตกต่างเป็นเพียงรายละเอียดของสิ่งที่ติดตาม: การทอนเหรียญติดตามจำนวน โดยบวก 1 ต่อเหรียญ ส่วนบันไดติดตามค่าใช้จ่าย โดยบวก cost[i] ต่อขั้น การมองเห็นโครงสร้างร่วมนี้ช่วยให้คุณแก้ปัญหา DP ใหม่ ๆ ได้ด้วยการจับคู่เข้ากับรูปแบบที่คุ้นเคย

# Shared pattern:
# dp[state] = optimise(dp[prev_state_1] + cost_1,
#                      dp[prev_state_2] + cost_2, ...)

# Coin change:  dp[amount] = min(1 + dp[amount - coin] for coin in coins)
# Min stair:    dp[step]   = min(cost[step-1]+dp[step-1], cost[step-2]+dp[step-2])
# Max path sum: dp[cell]   = max(dp[top], dp[left]) + grid[cell]
# House robber: dp[house]  = max(dp[house-1], dp[house-2] + value[house])

# All four are the SAME pattern with different:
# - State representation
# - Number of choices per state
# - Objective (min/max)
# - Transition cost
print('DP pattern: state + choices + objective + cost = template')

จำนวนกำลังสองสมบูรณ์น้อยที่สุด

กำลังสองสมบูรณ์ (LeetCode #279) ถามถึงจำนวนกำลังสองสมบูรณ์ที่น้อยที่สุด (1, 4, 9, 16, ...) ซึ่งรวมกันได้ n ปัญหานี้เหมือนกับการทอนเหรียญทุกประการ โดยให้ “เหรียญ” เป็นจำนวนกำลังสองสมบูรณ์ สร้างจำนวนกำลังสองสมบูรณ์ทั้งหมดที่ไม่เกิน n แล้วใช้วิธีการทอนเหรียญต่อ DP ใช้เวลา O(n * sqrt(n)) ทฤษฎีสี่กำลังสองของลากร็องฌ์บอกว่าคำตอบมีค่าไม่เกิน 4 ซึ่งเปิดทางให้ใช้แนวทางทางคณิตศาสตร์ที่ใช้เวลา O(sqrt(n)) ได้ด้วย แต่คำตอบที่คาดหวังคือการใช้ DP

import math

def num_squares(n):
    # Generate all perfect squares up to n
    squares = [i*i for i in range(1, int(math.sqrt(n)) + 1)]
    # Coin change with squares as 'coins'
    dp = [float('inf')] * (n + 1)
    dp[0] = 0
    for i in range(1, n + 1):
        for sq in squares:
            if i >= sq:
                dp[i] = min(dp[i], 1 + dp[i - sq])
    return dp[n]

print(num_squares(12))  # 3: 4+4+4
print(num_squares(13))  # 2: 4+9
print(num_squares(1))   # 1: 1

การแก้จุดบกพร่องของ DP: ข้อผิดพลาดที่พบบ่อย

ข้อผิดพลาดของ DP ที่พบบ่อย ได้แก่ กำหนดกรณีฐานผิด (กำหนดค่า dp[0] ไม่ถูกต้อง) ลำดับการเติมค่าผิด (เข้าถึงค่าที่ยังไม่ได้คำนวณ) กำหนดสถานะคลาดเคลื่อนหนึ่งตำแหน่ง (dp[i] คือค่าใช้จ่ายในการไปถึง i หรือค่าใช้จ่ายในการ TO i) และ ไม่คืนค่า -1 เมื่อยังมีค่าอนันต์เหลืออยู่ (กรณีที่เป็นไปไม่ได้) ควรทดสอบด้วยกรณีที่ง่ายที่สุดเสมอ เช่น ข้อมูลเข้าว่าง องค์ประกอบเดียว และ target=0 ก่อนทดสอบด้วยข้อมูลเข้าขนาดใหญ่ขึ้น

# Common DP debugging checklist:
# 1. Base case: what is dp[0]? dp[1]? Are they correct?
# 2. State definition: write it in English before coding
# 3. Recurrence: trace manually on a 3-element example
# 4. Fill order: dependency arrows point left/up? Fill left/up first
# 5. Infinity check: return -1 or 0 when dp[target] == inf?
# 6. Array bounds: dp has size n+1 for 0..n, or n for 0..n-1?

# Quick test template:
def test_coin_change():
    assert coin_change([1], 0) == 0     # base case
    assert coin_change([1], 1) == 1     # single coin
    assert coin_change([2], 3) == -1    # impossible
    assert coin_change([1,5,6,9], 11) == 2
    print('All tests passed!')

test_coin_change()

ตรวจสอบความเข้าใจ

ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้

สรุปบทเรียน

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

คำถามที่พบบ่อย

บทเรียน “การทอนเหรียญและบันไดต้นทุนต่ำสุด” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การทอนเหรียญและบันไดต้นทุนต่ำสุด” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การทอนเหรียญและบันไดต้นทุนต่ำสุด”

กำหนดความสัมพันธ์เวียนเกิดของการทอนเหรียญและการปีนบันไดด้วยต้นทุนต่ำสุด เลือกทิศทาง DP ที่เหมาะสม และติดตามตารางด้วยตนเอง คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “การทอนเหรียญและบันไดต้นทุนต่ำสุด” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม

ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

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

  1. รู้จัก DP: ปัญหาย่อยที่ซ้ำซ้อน
  2. DP จากบนลงล่างด้วยการจดจำผลลัพธ์
  3. DP จากล่างขึ้นบนด้วยตาราง
  4. การทอนเหรียญและบันไดต้นทุนต่ำสุด
← กลับไปที่ Coding Interview Prep