0Pricing
DSA Interview Prep · Lekcja

Plecak bez ograniczeń i Coin Change II

Zezwalać na wielokrotne użycie elementów przez iterowanie pojemności w przód oraz rozwiązywać problemy coin-change-II (zliczanie sposobów) i cięcia pręta za pomocą tego wariantu

Plecak bez ograniczeń i Coin Change II to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 2 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 DSA Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Koncepcja plecaka nieograniczonego

W problemie Unbounded Knapsack każdy przedmiot można wybrać dowolną liczbę razy (w przeciwieństwie do problemu plecakowego 0/1, w którym każdy przedmiot jest używany najwyżej raz). Definicja stanu pozostaje taka sama — dp[c] = maksymalna wartość osiągalna przy pojemności c — zmienia się jednak kierunek iterowania. Ponieważ przedmioty można ponownie wykorzystywać, podczas aktualizowania dp[c] chcemy dopuścić ponowne użycie bieżącego przedmiotu, dlatego iterujemy pojemność od lewej do prawej (w przód).

Iterowanie w przód umożliwia ponowne użycie

Przypomnijmy, że w problemie plecakowym 0/1 iterowaliśmy od prawej do lewej, aby zapobiec ponownemu użyciu przedmiotów. W problemie plecakowym nieograniczonym robimy odwrotnie: iterujemy od lewej do prawej. Podczas obliczania dp[c] wartość dp[c-w] została już zaktualizowana w bieżącym przebiegu — oznacza to, że przedmiot i mógł już zostać uwzględniony. Tego właśnie potrzebujemy: przedmiot i można dodać ponownie do rozwiązania, które już go zawiera.

def unbounded_knapsack(weights, values, W):
    dp = [0] * (W + 1)
    
    for i in range(len(weights)):
        w, v = weights[i], values[i]
        for c in range(w, W + 1):  # iterate LEFT TO RIGHT
            dp[c] = max(dp[c], dp[c - w] + v)
    
    return dp[W]

weights = [1, 3, 4, 5]
values  = [1, 4, 5, 7]
print(unbounded_knapsack(weights, values, 7))  # 9

Coin Change II: zliczanie sposobów

Problem Coin Change II polega na tym, aby dla danych nominałów monet i kwoty policzyć liczbę różnych sposobów uzyskania tej kwoty (każdej monety można używać bez ograniczeń). Jest to wariant problemu plecakowego nieograniczonego, w którym zamiast maksymalizować wartość, zliczamy kombinacje. Zdefiniujmy dp[c] jako liczbę sposobów uzyskania kwoty c. Przypadek bazowy: dp[0] = 1 (istnieje jeden sposób uzyskania 0: nic nie wybrać).

Implementacja Coin Change II

Dla każdej monety iterujemy po kwotach od lewej do prawej i dodajemy: dp[c] += dp[c - coin]. Przypadek bazowy dp[0] = 1 inicjuje zliczanie. Zauważmy, że pętla zewnętrzna przebiega po monetach, a wewnętrzna po kwotach — dzięki temu naturalnie otrzymujemy liczbę kombinacji (a nie permutacji), ponieważ każdy nominał jest uwzględniany dokładnie raz w osobnym przebiegu pętli zewnętrznej.

def change(amount, coins):
    dp = [0] * (amount + 1)
    dp[0] = 1  # one way to make amount 0
    
    for coin in coins:
        for c in range(coin, amount + 1):
            dp[c] += dp[c - coin]
    
    return dp[amount]

print(change(5, [1, 2, 5]))   # 4
print(change(3, [2]))          # 0
print(change(10, [10]))        # 1

Kombinacje a permutacje

Kolejność pętli ma kluczowe znaczenie. Jeśli umieścimy amount w pętli zewnętrznej, a coin w pętli wewnętrznej, będziemy zliczać permutacje (kolejność ma znaczenie). Dla amount=5 i monet [1,2] ciągi 1+2+2 i 2+1+2 zostaną policzone osobno. Jeśli umieścimy coin w pętli zewnętrznej, będziemy zliczać kombinacje (kolejność nie ma znaczenia): 1+2+2 i 2+1+2 są tym samym sposobem. Problem Coin Change II wymaga zliczania kombinacji, dlatego coin znajduje się w pętli zewnętrznej.

# Count COMBINATIONS (order does not matter) — coin outer loop
def combinations(amount, coins):
    dp = [0] * (amount + 1)
    dp[0] = 1
    for coin in coins:          # coin outer
        for c in range(coin, amount + 1):
            dp[c] += dp[c - coin]
    return dp[amount]

# Count PERMUTATIONS (order matters) — amount outer loop
def permutations(amount, coins):
    dp = [0] * (amount + 1)
    dp[0] = 1
    for c in range(1, amount + 1):  # amount outer
        for coin in coins:
            if c >= coin:
                dp[c] += dp[c - coin]
    return dp[amount]

print(combinations(5, [1,2,5]))   # 4
print(permutations(5, [1,2,5]))   # 13

Problem cięcia pręta

Inny klasyczny problem plecakowy nieograniczony: mając pręt o długości n i ceny dla każdej długości pręta od 1 do n, należy znaleźć maksymalny przychód przez optymalne pocięcie pręta. Każdy kawałek o długości l można sprzedać za price[l], a kawałki mogą być używane wielokrotnie (pręt można podzielić na wiele kawałków o tej samej długości). Problem ten bezpośrednio odpowiada problemowi plecakowemu nieograniczonemu, w którym W = n, a przedmiotami są różne długości cięcia.

def rod_cutting(prices, n):
    # prices[i] = price of rod of length i+1
    dp = [0] * (n + 1)
    
    for length in range(1, n + 1):   # each cut length
        price = prices[length - 1]
        for c in range(length, n + 1):
            dp[c] = max(dp[c], dp[c - length] + price)
    
    return dp[n]

prices = [1, 5, 8, 9, 10, 17, 17, 20]
print(rod_cutting(prices, 8))  # 22

Coin Change I: minimalna liczba monet

Coin Change I (inny problem) polega na znalezieniu minimalnej liczby monet potrzebnych do uzyskania docelowej kwoty. W tym przypadku dp[c] = minimalna liczba monet potrzebnych do uzyskania kwoty c. Zależność rekurencyjna: dp[c] = min(dp[c], dp[c - coin] + 1). Wszystkie wpisy należy zainicjalizować wartością inf, z wyjątkiem dp[0] = 0. Jest to również problem nieograniczony (monet można używać wielokrotnie), dlatego iterujemy od lewej do prawej. Zwracamy dp[amount], jeśli wartość jest skończona, w przeciwnym razie -1.

def coinChange(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    
    for coin in coins:
        for c in range(coin, amount + 1):
            dp[c] = min(dp[c], dp[c - coin] + 1)
    
    return dp[amount] if dp[amount] != float('inf') else -1

print(coinChange([1,5,6,9], 11))  # 2 (5+6 or other combos)
print(coinChange([2], 3))          # -1

Kluczowa różnica: maksimum, minimum czy zliczanie

Trzy warianty problemu plecakowego nieograniczonego używają różnych operacji na dp[c-coin]: Maksymalizacja wartości: dp[c] = max(dp[c], dp[c-w] + v); inicjalizacja wartością 0. Minimalizacja kosztu: dp[c] = min(dp[c], dp[c-coin] + 1); inicjalizacja wartością inf, dp[0]=0. Zliczanie sposobów: dp[c] += dp[c-coin]; inicjalizacja wartością 0, dp[0]=1. Rozpoznanie właściwego wariantu to połowa sukcesu w zadaniach rekrutacyjnych.

Złożoność i wskazówki rekrutacyjne

Wszystkie warianty problemu plecakowego nieograniczonego działają w czasie O(n × W) i zajmują O(W) pamięci, gdzie n oznacza liczbę typów przedmiotów, a W — docelową kwotę. W problemach monetowych n oznacza liczbę nominałów. Podczas rozmowy rekrutacyjnej należy określić wariant (maksimum/minimum/zliczanie), zapisać DP 1D oraz jasno wskazać, czy pętla zewnętrzna przebiega po monetach, czy po kwocie — osoby oceniające wiedzą, że to rozróżnienie sprawdza dogłębne rozumienie DP.

Rozpoznawanie problemu nieograniczonego i 0/1

Skorzystaj z poniższych wskazówek, aby rozpoznać właściwy wariant: nieograniczone ponowne użycie → problem nieograniczony (iterowanie w przód); każdy przedmiot dokładnie raz → problem 0/1 (iterowanie wstecz); sformułowania „dowolną liczbę razy”, „nieograniczony zapas” lub „dozwolone ponowne użycie” → problem nieograniczony. Przykłady: coin change, rod cutting i integer break — wszystkie są problemami nieograniczonymi. Subset sum, partition i 0/1 knapsack — to problemy 0/1. Pomyłka na tym etapie prowadzi do błędnych odpowiedzi, które trudno zdebugować.

Integer Break i inne warianty

Integer Break (LeetCode 343): podzielić liczbę n na co najmniej 2 dodatnie liczby całkowite tak, aby zmaksymalizować ich iloczyn. Jest to problem plecakowy nieograniczony, w którym „przedmiotami” są liczby całkowite od 2 do n-1. Zdefiniujmy dp[i] jako maksymalny iloczyn liczb sumujących się do i. Dla każdego przedmiotu j od 2 do i dp[i] = max(dp[i], max(j, dp[j]) * max(i-j, dp[i-j])). Pokazuje to, jak wzorzec problemu nieograniczonego uogólnia się poza kontekstem monet.

def integerBreak(n):
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        for j in range(1, i):
            dp[i] = max(dp[i], max(j, dp[j]) * max(i-j, dp[i-j]))
    return dp[n]

print(integerBreak(10))  # 36 (3+3+4 = 3*3*4 = 36)

Szybki test

Proszę sprawdzić swoją wiedzę na temat zagadnień z kursu Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.

Podsumowanie lekcji

W tej lekcji poznali Państwo: że w problemie plecakowym nieograniczonym iteruje się pojemność od lewej do prawej, aby umożliwić ponowne użycie przedmiotu, że Coin Change II zlicza kombinacje, umieszczając coin w pętli zewnętrznej oraz że trzy warianty — maksymalizacja, minimalizacja i zliczanie — różnią się wyłącznie operacją DP i inicjalizacją. Następnie wykorzystamy problem plecakowy 0/1 do rozwiązania Partition Equal Subset Sum.

Często zadawane pytania

Czy lekcja „Plecak bez ograniczeń i Coin Change II” jest bezpłatna?

Tak — pełny tekst „Plecak bez ograniczeń i Coin Change II” 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 DSA Interview Prep, przejdź na CoddyKit PRO. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Plecak bez ograniczeń i Coin Change II”?

Zezwalać na wielokrotne użycie elementów przez iterowanie pojemności w przód oraz rozwiązywać problemy coin-change-II (zliczanie sposobów) i cięcia pręta za pomocą tego wariantu Ćwiczysz DSA 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ąć DSA Interview Prep?

Nie wymagamy żadnego doświadczenia. DSA 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 2 z 4.

Ile czasu zajmuje lekcja „Plecak bez ograniczeń i Coin Change II”?

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 DSA Interview Prep?

Tak. Każda lekcja DSA 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. Plecak 0/1 i optymalizacja pamięci
  2. Plecak bez ograniczeń i Coin Change II
  3. Podział na równe sumy podzbiorów
  4. Suma docelowa ze znakami dodatnimi i ujemnymi
← Powrót do DSA Interview Prep