0Pricing
Coding Interview Prep · Lekcja

Wydawanie reszty i schody o minimalnym koszcie

Sformułują Państwo rekurencje dla coin-change i min-cost-climbing-stairs, wybiorą właściwy kierunek DP i ręcznie prześledzą tablicę.

Wydawanie reszty i schody o minimalnym koszcie to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Wydawanie reszty: problem

Problem Wydawania reszty (LeetCode #322) polega na znalezieniu minimalnej liczby monet potrzebnych do uzyskania dokładnej kwoty docelowej na podstawie podanych nominałów. Dostępna jest nieograniczona liczba monet każdego nominału. Jest to klasyczny wariant problemu plecakowego bez ograniczeń — każdy element (monetę) można wykorzystać dowolną liczbę razy. To jeden z najważniejszych problemów DP, ponieważ sprawdza umiejętność samodzielnego sformułowania rekurencji.

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

Wydawanie reszty: wyprowadzenie rekurencji

Zdefiniujmy dp[i] jako minimalną liczbę monet potrzebnych do uzyskania kwoty i. Dla każdej kwoty i należy spróbować użyć każdej monety c: jeśli i >= c, wówczas dp[i] = min(dp[i], 1 + dp[i-c]). „1” oznacza monetę, której właśnie użyliśmy, a dp[i-c] to optymalne rozwiązanie dla pozostałej kwoty. Zakładamy przy tym nieograniczoną liczbę monet. Przypadek bazowy: dp[0] = 0. Wszystkie pozostałe elementy należy zainicjalizować wartością nieskończoności, aby oznaczyć kwoty „jeszcze nieosiągalne”.

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

Wydawanie reszty: dlaczego strategia zachłanna zawodzi

Strategia zachłanna (zawsze wybierająca największą pasującą monetę) zawodzi w problemie wydawania reszty. Przykład: coins=[1, 3, 4], amount=6. Strategia zachłanna wybiera 4, a następnie 1+1, czyli 3 monety. Optymalne rozwiązanie to 3+3, czyli 2 monety. Strategia zachłanna działa dla standardowych nominałów (1, 5, 10, 25 centów), ponieważ akurat spełniają one własność zachłanną. Jednak dla dowolnych zestawów monet wymagane jest DP. To klasyczny temat na rozmowach kwalifikacyjnych — stwierdzenie, że strategia zachłanna zawodzi, i wyjaśnienie dlaczego pokazuje dobre myślenie analityczne.

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

Wydawanie reszty II: liczenie sposobów

Wydawanie reszty II (LeetCode #518) polega na policzeniu liczby sposobów uzyskania danej kwoty (a nie minimalnej liczby monet). Rekurencja ulega zmianie: zamiast funkcji min używamy sumowania. Dla każdej monety stosujemy dp[i] += dp[i-coin]. Kolejność wypełniania ma znaczenie: aby każdą kombinację policzyć dokładnie raz, należy umieścić monety w pętli zewnętrznej, a kwoty w pętli wewnętrznej. Odwrócenie tych pętli powoduje liczenie permutacji zamiast kombinacji (czyli rozwiązuje inny 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)

Schody z minimalnym kosztem: problem

Schody z minimalnym kosztem (LeetCode #746) to problem, w którym każdy stopień ma przypisany koszt. Można pokonywać za każdym razem 1 lub 2 stopnie. Należy znaleźć minimalny koszt dotarcia na szczyt (jeden stopień za ostatnim stopniem). Można bezpłatnie rozpocząć na stopniu 0 lub 1. Ten problem w elegancki sposób łączy rekurencję problemu wspinania się po schodach ze wzorcem minimalizacji kosztu znanym z problemu wydawania reszty, tworząc naturalne przejście między tymi dwoma zagadnieniami.

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

Schody z minimalnym kosztem: rekurencja

Zdefiniujmy dp[i] jako minimalny koszt dotarcia do stopnia i. Do stopnia i można dotrzeć, płacąc cost[i-1] (z poprzedniego stopnia) albo cost[i-2] (z tego oddalonego o dwa stopnie). Zatem dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]). Przypadki bazowe: dp[0] = 0 (start przed schodami jest bezpłatny), dp[1] = 0 (można również bezpłatnie rozpocząć na stopniu 1). Odpowiedzią jest dp[n], gdzie 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

Schody z minimalnym kosztem: optymalizacja pamięci

Ponieważ dp[i] zależy tylko od dp[i-1] i dp[i-2], pamięć można zmniejszyć do O(1) za pomocą dwóch zmiennych, podobnie jak w przypadku Fibonacciego. Tablicę zastępujemy zmiennymi prev2 i prev1. Należy aktualizować je na każdym kroku. Jest to standardowa optymalizacja, której rekruterzy oczekują po przedstawieniu rozwiązania z tablicą O(n). Warto wspomnieć o niej z wyprzedzeniem: „Można zmniejszyć zapotrzebowanie na pamięć do O(1), ponieważ potrzebujemy tylko dwóch ostatnich wartości”.

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

Alternatywne sformułowanie DP

Niektóre problemy mają wiele poprawnych sformułowań DP. W przypadku schodów z minimalnym kosztem można zdefiniować dp[i] jako minimalny koszt opuszczenia stopnia i (opłacenia cost[i] i wybrania przejścia do i+1 lub i+2). Wówczas dp[i] = cost[i] + min(dp[i+1], dp[i+2]), a tablicę wypełnia się od prawej do lewej; odpowiedzią jest min(dp[0], dp[1]). Oba sformułowania są poprawne. Warto ćwiczyć wyjaśnianie, które sformułowanie zostało wybrane i dlaczego — pokazuje to biegłość w 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

Powiązanie problemów wydawania reszty i schodów

Zarówno problem wydawania reszty, jak i schodów z minimalnym kosztem są przykładami tego samego wzorca DP: na każdym kroku należy dokonać wyboru spośród skończonego zbioru możliwości i zoptymalizować funkcję celu dla całej sekwencji wyborów. Różnice dotyczą szczegółów: w problemie wydawania reszty śledzimy liczbę (dodajemy 1 dla każdej monety), a w problemie schodów koszt (dodajemy cost[i] za każdy stopień). Rozpoznanie tej wspólnej struktury pozwala rozwiązywać nowe problemy DP przez przyporządkowanie ich do znanych szablonów.

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

Minimalna liczba kwadratów liczb całkowity

Problem Kwadratów liczb całkowity (LeetCode #279) polega na znalezieniu minimalnej liczby kwadratów liczb całkowity (1, 4, 9, 16, ...), których suma wynosi n. Jest to dokładnie problem wydawania reszty, w którym „monetami” są kwadraty liczb całkowity. Należy wygenerować wszystkie kwadraty liczb całkowity nie większe niż n, a następnie zastosować algorytm wydawania reszty. DP działa w czasie O(n * sqrt(n)). Twierdzenie Lagrange’a o czterech kwadratach mówi, że odpowiedź wynosi co najwyżej 4, co pozwala również zastosować matematyczne rozwiązanie w czasie O(sqrt(n)) — jednak oczekiwanym rozwiązaniem jest 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

Debugowanie DP: typowe błędy

Najczęstsze błędy w DP to: niepoprawny przypadek bazowy (nieprawidłowo ustawione dp[0]), niepoprawna kolejność wypełniania (odwoływanie się do wartości, która nie została jeszcze obliczona), błąd o jeden w definicji stanu (dp[i] oznacza koszt DOTARCIA do i w porównaniu z kosztem OPUSZCZENIA i) oraz brak zwracania -1, gdy pozostaje nieskończoność (przypadki niemożliwe). Zawsze należy najpierw testować najprostsze przypadki (puste dane wejściowe, jeden element, target=0), a dopiero potem większe dane.

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

Szybki test

Proszę sprawdzić swoje rozumienie pojęć Data Structures & Algorithms — Coding Interview Prep z tej lekcji.

Podsumowanie lekcji

W tej lekcji omówiono: DP problemu wydawania reszty z minimalną liczbą monet (plecak bez ograniczeń) oraz przyczyny porażki strategii zachłannej, problem wydawania reszty II do liczenia kombinacji z monetami w pętli zewnętrznej i kwotami w pętli wewnętrznej, a także schody z minimalnym kosztem z sformułowaniami od lewej do prawej i od prawej do lewej. Następnie omówimy wzorce DP 1D na przykładzie rabowania domów, algorytmu Kadane’a i problemu dzielenia słowa.

Często zadawane pytania

Czy lekcja „Wydawanie reszty i schody o minimalnym koszcie” jest bezpłatna?

Tak — pełny tekst „Wydawanie reszty i schody o minimalnym koszcie” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Wydawanie reszty i schody o minimalnym koszcie”?

Sformułują Państwo rekurencje dla coin-change i min-cost-climbing-stairs, wybiorą właściwy kierunek DP i ręcznie prześledzą tablicę. Ćwiczysz Coding Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Coding Interview Prep?

Nie wymagamy żadnego doświadczenia. Coding Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Wydawanie reszty i schody o minimalnym koszcie”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Coding Interview Prep?

Tak. Każda lekcja Coding Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Rozpoznawanie DP: nakładające się podproblemy
  2. DP z góry na dół z memoizacją
  3. DP z dołu do góry z tabulacją
  4. Wydawanie reszty i schody o minimalnym koszcie
← Powrót do Coding Interview Prep