0Pricing
Coding Interview Prep · Lekcja

Schemat przedziałowego DP i kolejność wypełniania

Definiować stan przedziałowego DP dp[i][j], wyjaśniać, dlaczego przedziały należy wypełniać według rosnącej długości, oraz prześledzić ten schemat na przykładzie mnożenia łańcucha macierzy

Schemat przedziałowego DP i kolejność wypełniania to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 1 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.

Czym jest DP przedziałowe?

DP przedziałowe to wzorzec programowania dynamicznego, w którym stan dp[i][j] reprezentuje optymalną odpowiedź dla podproblemu obejmującego indeksy od i do j. Kluczowa obserwacja polega na tym, że najpierw rozwiązujemy mniejsze przedziały, a następnie stopniowo rozszerzamy rozwiązanie na cały zakres. Ten wzorzec naturalnie odwzorowuje problemy takie jak mnożenie łańcucha macierzy, partycjonowanie palindromu i rozbijanie balonów, w których granicami podproblemu są lewy i prawy koniec zakresu.

Definicja stanu i przypadki bazowe

W DP przedziałowym stanem jest dp[i][j], gdzie i <= j. Przypadkami bazowymi są przedziały zawierające jeden element: dp[i][i]. Można je rozwiązać bezpośrednio — na przykład pojedyncza macierz ma koszt mnożenia równy zero. Przedziały zawierające dwa elementy, dp[i][i+1], również często mają proste rozwiązania. Wypełniamy tabelę dla rosnących długości przedziałów, zaczynając od długości 1 i kończąc na n.

n = 4
dp = [[0] * n for _ in range(n)]
# Base cases: single elements
for i in range(n):
    dp[i][i] = 0  # length-1 intervals

Kolejność wypełniania: rosnąca długość

Kluczowym szczegółem w DP przedziałowym jest kolejność wypełniania. Musimy obliczyć wszystkie przedziały o długości L, zanim obliczymy przedziały o długości L+1, ponieważ dłuższy przedział zależy od krótszych podprzedziałów. Pętla zewnętrzna przechodzi po długościach przedziałów od 2 do n, pętla środkowa ustawia lewą granicę i, a prawą granicę wyznaczamy jako j = i + L - 1.

n = 5
dp = [[float('inf')] * n for _ in range(n)]
for i in range(n):
    dp[i][i] = 0

for length in range(2, n + 1):      # interval length
    for i in range(n - length + 1): # left boundary
        j = i + length - 1          # right boundary
        for k in range(i, j):       # split point
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j])

Konfiguracja mnożenia łańcucha macierzy

Klasycznym problemem DP przedziałowego jest mnożenie łańcucha macierzy: mając macierze o wymiarach dims[0..n], należy znaleźć minimalną liczbę mnożeń skalarnych potrzebnych do obliczenia iloczynu. Mnożenie macierzy A(p×q) przez B(q×r) kosztuje p*q*r operacji. dp[i][j] oznacza minimalny koszt pomnożenia macierzy od i do j. Punkt podziału k określa, gdzie sekwencja zostaje podzielona na dwa podłańcuchy.

def matrix_chain_order(dims):
    n = len(dims) - 1  # number of matrices
    dp = [[0] * n for _ in range(n)]
    
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                cost = dp[i][k] + dp[k+1][j] + dims[i]*dims[k+1]*dims[j+1]
                dp[i][j] = min(dp[i][j], cost)
    return dp[0][n-1]

print(matrix_chain_order([10, 30, 5, 60]))  # 4500

Śledzenie tabeli DP

Prześledźmy przykład mnożenia łańcucha macierzy o wymiarach [10, 30, 5, 60], reprezentujący trzy macierze: A(10×30), B(30×5) i C(5×60). Dla dp[0][2] próbujemy podziału w k=0: dp[0][0] + dp[1][2] + 10×30×60 = 0 + 9000 + 18000 = 27000, a także w k=1: dp[0][1] + dp[2][2] + 10×5×60 = 1500 + 0 + 3000 = 4500. Zatem dp[0][2] = 4500, co osiągamy, mnożąc najpierw AB.

Dlaczego ta kolejność wypełniania działa

Podczas obliczania dp[i][j] odwołujemy się do dp[i][k] oraz dp[k+1][j] dla każdego k w zakresie [i, j-1]. Oba podprzedziały mają ściśle mniejszą długość niż [i, j]. Iterowanie po długościach od najmniejszych do największych gwarantuje, że wszystkie wymagane podprzedziały zostaną obliczone, zanim będą potrzebne. Jest to podstawowy argument poprawności kolejności wypełniania w DP przedziałowym — krótsze przedziały zawsze są zależnościami dłuższych.

DP przedziałowe top-down z memoizacją

DP przedziałowe można również zaimplementować odgórnie z memoizacją. Piszemy funkcję rekurencyjną solve(i, j), która zwraca optymalny koszt dla przedziału [i, j], a wyniki zapisujemy w słowniku. Rekurencja automatycznie obsługuje kolejność wypełniania. Podejście top-down często ułatwia rozumowanie, ale może wiązać się z narzutem wywołań funkcji; podejście bottom-up jest w praktyce szybsze dla dużych danych wejściowych.

from functools import lru_cache

def matrix_chain_memo(dims):
    n = len(dims) - 1
    
    @lru_cache(maxsize=None)
    def solve(i, j):
        if i == j:
            return 0
        return min(
            solve(i, k) + solve(k+1, j) + dims[i]*dims[k+1]*dims[j+1]
            for k in range(i, j)
        )
    
    return solve(0, n-1)

print(matrix_chain_memo([10, 30, 5, 60]))  # 4500

Złożoność czasowa i pamięciowa

DP przedziałowe ma O(n²) stanów (wszystkie pary (i, j)), a każdy stan iteruje po O(n) punktach podziału, co daje łącznie O(n³) czasu. Pamięć zajmowana przez tabelę DP to O(n²). W przypadku mnożenia łańcucha 100 macierzy oznacza to 1 000 000 operacji — jest to bardzo łatwe do wykonania. Ten wzorzec pojawia się w wielu trudnych zadaniach LeetCode i jest często wybierany podczas rozmów kwalifikacyjnych FAANG ze względu na swoją nieoczywistą strukturę.

Odtwarzanie optymalnego rozwiązania

Aby odtworzyć rzeczywiste nawiasowanie (a nie tylko koszt), należy przechowywać osobną tabelę split[i][j], rejestrującą wartość k, dla której w danym stanie osiągnięto minimum. Następnie odczytujemy podziały rekurencyjnie: reconstruct(i, j) wypisuje optymalne grupowanie, wywołując się rekurencyjnie dla [i, split[i][j]] oraz [split[i][j]+1, j]. Technika ta ma zastosowanie we wszystkich problemach DP przedziałowego.

def matrix_chain_with_split(dims):
    n = len(dims) - 1
    dp = [[0]*n for _ in range(n)]
    split = [[0]*n for _ in range(n)]
    
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                cost = dp[i][k] + dp[k+1][j] + dims[i]*dims[k+1]*dims[j+1]
                if cost < dp[i][j]:
                    dp[i][j] = cost
                    split[i][j] = k
    return dp[0][n-1], split

Szablon dowolnego problemu DP przedziałowego

Uniwersalny szablon DP przedziałowego składa się z trzech części: (1) zainicjalizowania przypadków bazowych dla pojedynczych elementów, (2) iterowania po rosnących długościach i, dla każdej długości, iterowania po prawidłowych lewych granicach oraz wyznaczenia prawej granicy, a także (3) iterowania dla każdego przedziału po wszystkich punktach podziału i zastosowania zależności rekurencyjnej właściwej dla danego problemu. Jedyną zmienianą między problemami częścią jest wzór rekurencyjny wewnątrz najbardziej zagnieżdżonej pętli.

def interval_dp_template(n, base_cost, split_cost):
    dp = [[float('inf')] * n for _ in range(n)]
    for i in range(n):
        dp[i][i] = base_cost(i)  # problem-specific base case
    
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            for k in range(i, j):
                # problem-specific recurrence
                candidate = dp[i][k] + dp[k+1][j] + split_cost(i, k, j)
                dp[i][j] = min(dp[i][j], candidate)
    
    return dp[0][n-1]

Typowe problemy DP przedziałowego

Problemy wykorzystujące DP przedziałowe obejmują: mnożenie łańcucha macierzy (minimalizacja liczby operacji), rozbijanie balonów (maksymalizacja liczby monet), Dziwnego drukarza (minimalizacja liczby operacji drukowania), triangulację wielokąta o minimalnym wyniku oraz partycjonowanie palindromu II. Każdy z nich korzysta z tego samego szkieletu kolejności wypełniania, ale z innej zależności rekurencyjnej. Należy rozpoznać ten wzorzec, gdy problem wymaga znalezienia optymalnej wartości dla zakresu lub sekwencji, którą można podzielić w dowolnym punkcie wewnętrznym.

Szybkie sprawdzenie

Sprawdź swoje zrozumienie zagadnień Data Structures & Algorithms — Coding Interview Prep z tej lekcji.

Podsumowanie lekcji

W tej lekcji nauczyłeś się, że: DP przedziałowe wykorzystuje dp[i][j] do reprezentowania optymalnej odpowiedzi dla zakresu, kolejność wypełniania musi uwzględniać rosnące długości przedziałów, aby najpierw obliczyć podprzedziały, a uniwersalny szablon ma złożoność czasową O(n³) i pamięciową O(n²). Następnie poznamy najdłuższy palindromiczny podciąg i podłańcuch, korzystając właśnie z tego wzorca.

Często zadawane pytania

Czy lekcja „Schemat przedziałowego DP i kolejność wypełniania” jest bezpłatna?

Tak — pełny tekst „Schemat przedziałowego DP i kolejność wypełniania” 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 „Schemat przedziałowego DP i kolejność wypełniania”?

Definiować stan przedziałowego DP dp[i][j], wyjaśniać, dlaczego przedziały należy wypełniać według rosnącej długości, oraz prześledzić ten schemat na przykładzie mnożenia łańcucha macierzy Ć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 1 z 4.

Ile czasu zajmuje lekcja „Schemat przedziałowego DP i kolejność wypełniania”?

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. Schemat przedziałowego DP i kolejność wypełniania
  2. Najdłuższy palindromiczny podciąg i podłańcuch
  3. Dzielenie palindromu II
  4. Burst Balloons: odwrócone przedziałowe DP
← Powrót do Coding Interview Prep