Forberedelse til kodeinterviews · Lektion

Coin change og trappe med minimale omkostninger

Formulér rekurrenserne for coin-change og min-cost-climbing-stairs, vælg den rigtige DP-retning, og gennemgå tabellen manuelt.

Lektion 4 af 413 trin

Coin change og trappe med minimale omkostninger er en gratis Forberedelse til kodeinterviews-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Forberedelse til kodeinterviews, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.

Møntveksling: Problemet

Møntveksling (LeetCode #322) giver dig møntværdier og et målbeløb. Find det mindste antal mønter, der skal bruges til at danne det præcise beløb. Du har et ubegrænset antal mønter af hver værdi. Dette er en klassisk variant af det ubundne rygsækproblem — hver genstand, altså hver mønt, kan bruges et vilkårligt antal gange. Det er et af de vigtigste DP-problemer, fordi det afprøver din evne til at formulere en rekurrens fra bunden.

# 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')

Møntveksling: Udledning af rekurrensen

Definér dp[i] som det mindste antal mønter, der skal til for at danne beløbet i. For hvert beløb i prøver du at bruge hver mønt c: Hvis i >= c, så er dp[i] = min(dp[i], 1 + dp[i-c]). Tallet '1' står for den mønt, vi netop brugte; dp[i-c] er den optimale løsning for det resterende beløb. Dette forudsætter uendeligt mange mønter. Basistilfælde: dp[0] = 0. Initialiser alle andre poster til uendelighed for at angive, at de endnu ikke kan opnås.

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

Møntveksling: Hvorfor en grådig tilgang fejler

En grådig tilgang, hvor du altid vælger den største mønt, der passer, fejler ved møntveksling. Eksempel: coins=[1, 3, 4], amount=6. Den grådige tilgang vælger 4 og derefter 1+1, altså 3 mønter. Den optimale løsning er 3+3, altså 2 mønter. Den grådige tilgang virker for standardmøntværdierne 1, 5, 10 og 25 cent, fordi de tilfældigvis opfylder den grådige egenskab. Men for vilkårlige møntsæt er DP nødvendig. Det er en klassisk pointe i programmeringsinterviews — hvis du fastslår, at en grådig tilgang fejler, og forklarer hvorfor, viser det stærk analytisk tænkning.

# 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)

Møntveksling II: Optælling af mulighederne

Møntveksling II (LeetCode #518) spørger efter antallet af måder at danne beløbet på, ikke det mindste antal mønter. Rekurrensen ændres: I stedet for min bruger du sum. dp[i] += dp[i-coin] for hver mønt. Udfyldningsrækkefølgen er vigtig: For at tælle hver kombination én gang gennemløber du mønterne i den ydre løkke og beløbene i den indre løkke. Hvis du bytter om på løkkerne, tæller du permutationer i stedet for kombinationer, hvilket er et andet problem.

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)

Trappen med minimale omkostninger: Problemet

Trappen med minimale omkostninger (LeetCode #746) består af en trappe, hvor hvert trin har en omkostning. Du kan gå 1 eller 2 trin ad gangen. Find den mindste omkostning ved at nå toppen, altså ét trin efter det sidste trappetrin. Du kan starte gratis på trin 0 eller trin 1. Problemet kombinerer på elegant vis rekurrensen for trappeklatring med møntvekslingens mønster for omkostningsminimering og danner derfor en naturlig bro mellem de to.

# 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)

Trappen med minimale omkostninger: Rekurrensen

Definér dp[i] som den mindste omkostning ved at nå trin i. Du når trin i ved at betale cost[i-1] fra trin i-1 eller cost[i-2] fra trin i-2. Derfor er dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]). Basistilfælde: dp[0] = 0, fordi du starter før trappen uden omkostning, og dp[1] = 0, fordi du også kan starte gratis på trin 1. Svaret er dp[n], hvor 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

Trappen med minimale omkostninger: Pladsoptimering

Da dp[i] kun afhænger af dp[i-1] og dp[i-2], kan du reducere pladsforbruget til O(1) med to variable, ligesom ved Fibonacci. Erstat arrayet med prev2 og prev1. Opdater dem ved hvert trin. Dette er en standardoptimering, der kan klares på én linje, og som interviewere forventer, at du kan nævne, efter at du har præsenteret tabel-løsningen med O(n)-plads. Nævn det altid på eget initiativ: 'Vi kan reducere dette til O(1)-plads, fordi vi kun har brug for de to seneste værdier.'

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

Alternativ DP-formulering

Nogle problemer har flere gyldige DP-formuleringer. For trappen med minimale omkostninger kan du definere dp[i] som den mindste omkostning ved at FORLADE trin i, hvor du betaler cost[i] og vælger at gå til i+1 eller i+2. Derefter er dp[i] = cost[i] + min(dp[i+1], dp[i+2]), udfyldt fra højre mod venstre, og svaret er min(dp[0], dp[1]). Begge formuleringer er korrekte. Øv dig i at forklare, hvilken formulering du valgte, og hvorfor — det viser, at du er fortrolig med 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

Sammenhængen mellem møntveksling og trappen

Både møntveksling og trappen med minimale omkostninger er eksempler på det samme DP-mønster: Ved hvert trin træffer du et valg fra en endelig mængde muligheder og optimerer et mål over valgenes rækkefølge. Forskellene er overfladiske: Møntveksling holder styr på antal, så der lægges 1 til for hver mønt, mens trappen holder styr på omkostning, så cost[i] lægges til for hvert trin. Når du genkender denne fælles struktur, kan du løse nye DP-problemer ved at koble dem til velkendte skabeloner.

# 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')

Mindste antal perfekte kvadrater

Perfekte kvadrater (LeetCode #279) spørger efter det mindste antal perfekte kvadrater (1, 4, 9, 16, ...) hvis sum er n. Det er præcis møntveksling, hvor 'mønterne' er perfekte kvadrattal. Generér alle perfekte kvadrater op til n, og kør derefter møntveksling. DP har en tidskompleksitet på O(n * sqrt(n)). Lagranges firekvadratsætning fortæller os, at svaret højst er 4, hvilket også muliggør en matematisk tilgang med O(sqrt(n))-tid — men DP er den forventede løsning.

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

Fejlfinding i DP: Almindelige fejl

Almindelige DP-fejl: forkert basistilfælde (dp[0] er sat forkert), forkert udfyldningsrækkefølge (du tilgår en værdi, der endnu ikke er beregnet), én-fejl i tilstandsdefinitionen (dp[i] er omkostningen TIL at nå i kontra omkostningen ved at FORLADE i) og at der ikke returneres -1, når der stadig står uendelighed (umulige tilfælde). Afprøv altid de enkleste tilfælde, såsom tomt input, ét element og target=0, før du afprøver større datamængder.

# 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()

Hurtig kontrol

Afprøv din forståelse af begreberne i Data Structures & Algorithms — Coding Interview Prep fra denne lektion.

Opsummering af lektionen

I denne lektion lærte du om: DP til møntveksling med et minimum af mønter (ubundet rygsækproblem) og hvorfor en grådig tilgang fejler, møntveksling II til optælling af kombinationer med mønter i den ydre løkke og beløb i den indre løkke samt trappen med minimale omkostninger med både venstre-mod-højre- og højre-mod-venstre-formuleringer. Derefter udforsker vi 1D-DP-mønstre med husrøveren, Kadane's algoritme og orddeling.

Gratis at komme i gang

Lær Forberedelse til kodeinterviews med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
90
Lektioner
360

Ofte stillede spørgsmål

Er lektionen “Coin change og trappe med minimale omkostninger” gratis?

Ja — hele teksten til “Coin change og trappe med minimale omkostninger” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Forberedelse til kodeinterviews-kurset, skal du opgradere til CoddyKit PRO. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Coin change og trappe med minimale omkostninger”?

Formulér rekurrenserne for coin-change og min-cost-climbing-stairs, vælg den rigtige DP-retning, og gennemgå tabellen manuelt. Du øver dig i Forberedelse til kodeinterviews med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Forberedelse til kodeinterviews?

Der kræves ingen tidligere erfaring. Forberedelse til kodeinterviews på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.

Hvor lang tid tager lektionen “Coin change og trappe med minimale omkostninger”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Forberedelse til kodeinterviews-lektion?

Ja. Alle Forberedelse til kodeinterviews-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Genkendelse af DP: overlappende delproblemer
  2. Top-down-DP med memoization
  3. Bottom-up-DP med tabulering
  4. Coin change og trappe med minimale omkostninger
← Tilbage til Forberedelse til kodeinterviews