0Pricing
DSA Interview Prep · Lekcja

Algorytm zachłanny a programowanie dynamiczne: kiedy stosować które podejście

Rozpoznawać cechy problemów rozwiązywalnych algorytmem zachłannym oraz tych wymagających programowania dynamicznego, korzystając z własności wyboru zachłannego i argumentu wymiany

Algorytm zachłanny a programowanie dynamiczne: kiedy stosować które podejście to bezpłatna lekcja DSA 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 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.

Przegląd algorytmów zachłannych i DP

Zarówno algorytmy zachłanne, jak i programowanie dynamiczne służą do rozwiązywania problemów optymalizacyjnych — znajdowania maksimum, minimum lub optymalnego układu. Algorytm zachłanny dokonuje na każdym kroku lokalnie optymalnego wyboru bez ponownego rozważania wcześniejszych decyzji. DP bada wszystkie możliwości, ale używa zapamiętywania wyników, aby uniknąć ponownych obliczeń. Wiedza o tym, które podejście zastosować, może oszczędzić wiele godzin debugowania niepoprawnego algorytmu zachłannego lub niepotrzebnie złożonej tabeli DP.

# Greedy: always take the locally best option
# Example: coin change with coins [1, 5, 10, 25]
# Greedy: take as many 25s as possible, then 10s, etc.
# This works for standard denominations but NOT all coin sets!

# DP: explore all possibilities via memoisation
# Example: coin change with coins [1, 3, 4] and target 6
# Greedy would pick 4, then 1, 1 → 3 coins
# DP finds: 3 + 3 → 2 coins (optimal!)
print('Greedy can fail when local optimum != global optimum')

Własność wyboru zachłannego

Problem ma własność wyboru zachłannego, gdy globalnie optymalne rozwiązanie można zawsze skonstruować, dokonując lokalnie optymalnych (zachłannych) wyborów. Formalnie: istnieje optymalne rozwiązanie, które zaczyna się od zachłannego wyboru, więc nigdy nie trzeba wykonywać backtrackingu. Dowodzenie tej własności zwykle wykorzystuje argument wymiany: zakładamy, że dowolne optymalne rozwiązanie nie zawiera zachłannego wyboru, a następnie pokazujemy, że można go do niego wprowadzić bez pogorszenia wyniku.

# Exchange argument example: Activity Selection
# Greedy: always pick the activity that ends earliest
# Proof: suppose optimal solution starts with activity A (not earliest-ending)
# Let G be the earliest-ending activity.
# Replace A with G in the solution:
# - G ends no later than A, so G does not conflict with any activity A allowed
# - The solution remains valid with at least as many activities
# Therefore greedy choice (earliest end) is always safe.

activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14)]
activities.sort(key=lambda x: x[1])  # sort by end time
print('Sorted by end:', activities[:4], '...')

Optymalna podstruktura

Zarówno algorytmy zachłanne, jak i DP wymagają optymalnej podstruktury: optymalne rozwiązanie całego problemu zawiera optymalne rozwiązania podproblemów. Różnica polega na tym, czy optymalne rozwiązania podproblemów można wyznaczać zachłannie (bez sprawdzania wszystkich możliwości), czy też trzeba porównywać wiele wyborów. Jeśli po dokonaniu wyboru pozostały podproblem ma taką samą strukturę, algorytm zachłanny będzie działać. Jeśli trzeba porównać kilka wyborów, należy użyć DP.

# Greedy works: activity selection
# Making the greedy choice (earliest-ending) leaves a sub-problem
# that is structurally identical (activity selection on remaining activities)
# and the greedy choice for the sub-problem is still valid.

# DP needed: 0/1 knapsack
# After choosing to include/exclude item i, the remaining sub-problem
# depends on WHICH item we chose — different choices yield different sub-problems.
# No single greedy rule works for all inputs.

print('Greedy: sub-problem is unique after each choice')
print('DP: sub-problem depends on which choice was made')

Nakładające się podproblemy jako sygnał do użycia DP

Jeśli ten sam podproblem jest rozwiązywany wielokrotnie w rekurencyjnym rozkładzie problemu, potrzebne jest DP z zapamiętywaniem wyników. Należy narysować drzewo rekurencji i poszukać powtarzających się węzłów. Dla ciągu Fibonacciego fib(3) jest obliczane dwukrotnie w drzewie dla fib(5). Dla problemu wydawania reszty z monetami [1,3,4] i celem 6 podproblemy dla celów 3, 2 i 1 pojawiają się wielokrotnie. Nakładające się podproblemy wraz z optymalną podstrukturą oznaczają, że należy użyć DP.

# Recursion tree for coin change [1,3,4], target=6
# bt(6) → bt(5) → bt(4) → bt(3) (repeated!)
#              → bt(2) → bt(1) (repeated!)
#         → bt(3) (repeated!)
#       → bt(2) (repeated!)

# Without memoisation: exponential time
# With DP table: O(target * len(coins)) time

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

print(coin_change_dp([1, 3, 4], 6))  # 2 (3+3)
print(coin_change_dp([2], 3))        # -1 (impossible)

Klasyczne problemy rozwiązywane zachłannie

Problemy, dla których poprawność podejścia zachłannego można udowodnić: (1) harmonogramowanie aktywności/przedziałów — wybór przedziału kończącego się najwcześniej. (2) Minimalne drzewo rozpinające — algorytmy Prima i Kruskala. (3) Kodowanie Huffmana — zawsze łączymy dwa węzły o najmniejszych częstotliwościach. (4) Problem plecaka ułamkowego — wybieramy elementy według najwyższego stosunku wartości do wagi. (5) Gra w skoki — śledzimy maksymalny osiągalny indeks. Wszystkie te problemy mają uzasadnienie w postaci dowodu za pomocą argumentu wymiany.

# Fractional Knapsack: greedy works
def fractional_knapsack(items, capacity):
    # Sort by value/weight ratio descending
    items.sort(key=lambda x: x[1]/x[0], reverse=True)
    total = 0
    for weight, value in items:
        if capacity <= 0: break
        take = min(weight, capacity)
        total += take * (value / weight)
        capacity -= take
    return total

items = [(10, 60), (20, 100), (30, 120)]  # (weight, value)
print(fractional_knapsack(items, 50))  # 240.0

# 0/1 Knapsack: greedy FAILS
# Must use DP (can't take fractions)

Kiedy algorytm zachłanny zawodzi: kontrprzykłady

Znalezienie kontrprzykładu to najszybszy sposób na obalenie hipotezy dotyczącej algorytmu zachłannego. Dla problemu wydawania reszty z monetami [1, 3, 4] i celem 6 algorytm zachłanny (wybierający najpierw największą monetę) wybiera 4, a następnie 1+1, czyli 3 monety. DP znajduje rozwiązanie 3+3, czyli 2 monety. W przypadku plecaka 0/1 algorytm zachłanny oparty na stosunku wartości do wagi wybiera element o najlepszym stosunku, ale może pominąć kombinacje, które lepiej wykorzystują pojemność. Jeśli potrafią Państwo skonstruować kontrprzykład w mniej niż minutę, należy przełączyć się na DP.

# Counterexample: coin change with non-standard coins
def greedy_coins(coins, amount):
    coins.sort(reverse=True)
    count = 0
    for c in coins:
        while amount >= c:
            amount -= c
            count += 1
    return count if amount == 0 else -1

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

coins, target = [1, 3, 4], 6
print('Greedy:', greedy_coins(coins[:], target))  # 3 (4+1+1)
print('DP:    ', dp_coins(coins, target))          # 2 (3+3)

Tabela porównawcza: algorytm zachłanny a DP

Najważniejsze różnice przedstawione obok siebie: złożoność czasowa — algorytm zachłanny ma zwykle O(n log n) (złożoność wynika głównie z sortowania), a DP ma O(n × states). Złożoność pamięciowa — algorytm zachłanny wymaga O(1) dodatkowej pamięci, a DP O(states). Poprawność — algorytm zachłanny wymaga dowodu, natomiast DP jest zawsze poprawne, jeśli stany i rekurencja są prawidłowe. Zastosowanie — algorytmy zachłanne stosuje się do harmonogramowania, drzew rozpinających i kodowania Huffmana, a DP do plecaka, dopasowywania sekwencji oraz najkrótszej ścieżki z ujemnymi wagami.

# Performance comparison
import time

def time_it(func, *args):
    start = time.time()
    result = func(*args)
    return result, time.time() - start

# Large coin change test
coins = [1, 5, 10, 25, 100]
amount = 10000

def dp_coins(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for a in range(1, amount + 1):
        for c in coins:
            if c <= a: dp[a] = min(dp[a], dp[a-c]+1)
    return dp[amount]

result, elapsed = time_it(dp_coins, coins, amount)
print(f'DP coin change(amount={amount}): {result} coins in {elapsed:.4f}s')

Schemat podejmowania decyzji

Schemat decyzyjny podczas rozmowy rekrutacyjnej: (1) Czy można udowodnić własność wyboru zachłannego za pomocą argumentu wymiany? Jeśli tak → algorytm zachłanny. (2) Czy podproblemy nakładają się na siebie (ten sam stan jest osiągany na wiele sposobów)? Jeśli tak → DP. (3) Czy problem wymaga zliczenia lub wyliczenia wszystkich rozwiązań? → DP albo backtracking. (4) Czy problem wymaga znalezienia jednej optymalnej wartości przy naturalnym uporządkowaniu? Należy podejrzewać rozwiązanie zachłanne. (5) W razie wątpliwości należy zaimplementować DP — jest zawsze poprawne, jeśli rekurencja jest prawidłowa, nawet jeśli działa wolniej.

# Decision questions to ask:
questions = [
    '1. Is there a natural ordering (by time, ratio, size)?',
    '2. Does making the greedy choice leave a smaller same-type problem?',
    '3. Can I construct a counterexample quickly?',
    '4. Are sub-problems reused across different choice sequences?',
    '5. Does the problem involve counting or listing (not just optimising)?',
]
for q in questions:
    print(q)

print()
print('Greedy signals: scheduling, spanning tree, Huffman, jump game')
print('DP signals: knapsack, edit distance, LCS, coin change (general)')

Problemy przedziałowe: algorytm zachłanny a DP

Problemy przedziałowe dzielą się na rozwiązywane zachłannie i za pomocą DP. Przedziały rozłączne (usunięcie najmniejszej liczby przedziałów): sortujemy według czasu zakończenia i zachłannie wybieramy przedziały — algorytm zachłanny jest dowodliwie optymalny. Harmonogramowanie ważonych przedziałów (maksymalizacja łącznej wagi): potrzebne jest DP, ponieważ ciężkie przedziały mogą nakładać się na wiele lekkich, co wymaga porównania wszystkich poprawnych podzbiorów. Czynnikiem rozstrzygającym jest to, czy wszystkie przedziały mają jednakową wagę (algorytm zachłanny), czy różne wagi (DP).

# Non-overlapping intervals: greedy works
def erase_overlap_intervals(intervals):
    if not intervals: return 0
    intervals.sort(key=lambda x: x[1])
    count = 0
    last_end = float('-inf')
    for start, end in intervals:
        if start >= last_end:
            last_end = end  # keep this interval
        else:
            count += 1  # remove this interval
    return count

print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]]))  # 1
print(erase_overlap_intervals([[1,2],[1,2],[1,2]]))        # 2

Rozpoznawanie sygnałów w treści problemu

Typowe sygnały w treści problemu: „minimalna liczba operacji”, „maksymalny zysk”, „optymalny wybór” → rozwiązaniem może być algorytm zachłanny lub DP, należy sprawdzić, czy występują nakładające się podproblemy. „zlicz liczbę sposobów” → zawsze DP. „znajdź dowolny prawidłowy harmonogram” → może być algorytm zachłanny. „wszystkie możliwe” → backtracking. „nie można wybrać sąsiadujących” → DP (problem rabusia okradającego domy). „spotkania, przedziały, zadania” → prawdopodobnie algorytm zachłanny. Powiązanie sygnałów z rodzinami algorytmów przyspiesza rozpoznawanie rodzaju problemu podczas rozmowy rekrutacyjnej.

# Signal-to-algorithm mapping
signals = {
    'minimum steps/coins/operations': 'DP (unless trivially greedy)',
    'maximum profit/value with constraint': 'DP (knapsack family)',
    'count ways to reach/achieve': 'DP (always)',
    'all combinations/permutations': 'Backtracking',
    'schedule tasks within time': 'Greedy (sort by deadline/end)',
    'cannot pick adjacent': 'DP (house robber pattern)',
    'free to pick any subset': 'DP or Greedy (check overlap)',
    'interval merging/selecting': 'Greedy (sort by end time)',
}
for signal, algo in signals.items():
    print(f'{signal!r}: → {algo}')

Dowodzenie poprawności algorytmu zachłannego

Aby udowodnić poprawność algorytmu zachłannego, należy użyć argumentu wymiany: (1) Zakładamy, że istnieje optymalne rozwiązanie OPT, które różni się od rozwiązania zachłannego G przy pierwszym wyborze. (2) Pokazujemy, że można zastąpić wybór w OPT wyborem zachłannym bez zwiększania wartości funkcji celu. (3) Na podstawie indukcji rozwiązanie zachłanne jest co najmniej tak dobre jak każde rozwiązanie optymalne. Podczas rozmowy rekrutacyjnej nie trzeba przedstawiać pełnego dowodu, ale wyjaśnienie intuicji argumentu wymiany świadczy o głębokim zrozumieniu tematu.

# Exchange argument demo: earliest-finish-time activity selection
# Suppose OPT starts with activity A (not earliest-ending)
# Let G = earliest-ending activity available
# A.end >= G.end (G ends earlier or same time)

# Swap A for G in OPT:
# - G.end <= A.end, so G does not conflict with anything A allowed after it
# - OPT remains valid with the same number of activities
# - Repeat: after swap, OPT begins with G, matching greedy first choice
# By induction, OPT can be transformed to match G activity by activity
# without losing activities → greedy is optimal

print('Exchange argument: any OPT can be modified to match Greedy without loss')
print('This proves Greedy >= OPT in objective value')

Szybki test

Sprawdź swoją wiedzę na temat koncepcji Data Structures & Algorithms — Coding Interview Prep z tej lekcji.

Podsumowanie lekcji

W tej lekcji nauczyli się Państwo, że: algorytm zachłanny jest poprawny, gdy zachodzi własność wyboru zachłannego — można ją udowodnić za pomocą argumentu wymiany, DP jest potrzebne, gdy podproblemy nakładają się na siebie (ten sam podproblem jest osiągany na wiele sposobów) i nie można ich rozwiązać za pomocą jednej reguły zachłannej, a najszybszym sposobem obalenia hipotezy dotyczącej algorytmu zachłannego jest skonstruowanie kontrprzykładu z niestandardowymi danymi wejściowymi. Następnie rozwiążemy problem harmonogramowania i scalania przedziałów za pomocą zachłannego podejścia polegającego na sortowaniu według czasu zakończenia.

Często zadawane pytania

Czy lekcja „Algorytm zachłanny a programowanie dynamiczne: kiedy stosować które podejście” jest bezpłatna?

Tak — pełny tekst „Algorytm zachłanny a programowanie dynamiczne: kiedy stosować które podejście” 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 „Algorytm zachłanny a programowanie dynamiczne: kiedy stosować które podejście”?

Rozpoznawać cechy problemów rozwiązywalnych algorytmem zachłannym oraz tych wymagających programowania dynamicznego, korzystając z własności wyboru zachłannego i argumentu wymiany Ć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 1 z 4.

Ile czasu zajmuje lekcja „Algorytm zachłanny a programowanie dynamiczne: kiedy stosować które podejście”?

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. Algorytm zachłanny a programowanie dynamiczne: kiedy stosować które podejście
  2. Harmonogramowanie i scalanie przedziałów
  3. Jump Game I i II
  4. Task Scheduler i Gas Station
← Powrót do DSA Interview Prep