0Pricing
DSA Interview Prep · Lezione

Greedy e DP: quando usare ciascuno

Individui le caratteristiche distintive dei problemi risolvibili con un approccio greedy rispetto a quelli che richiedono la DP, usando la proprietà della scelta greedy e l'argomento dello scambio.

Greedy e DP: quando usare ciascuno è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA Interview Prep include 4 lezioni in totale.

Panoramica di Greedy e DP

Sia Greedy sia la programmazione dinamica risolvono problemi di ottimizzazione, cioè problemi che richiedono di trovare un massimo, un minimo o una disposizione ottimale. Greedy compie la scelta localmente ottimale a ogni passaggio, senza riconsiderare le decisioni precedenti. La DP esplora tutte le possibilità, ma usa la memoizzazione per evitare di ripetere i calcoli. Sapere quale approccio applicare può farLe risparmiare ore di debugging di una soluzione greedy errata o di una tabella DP inutilmente complessa.

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

La proprietà della scelta greedy

Un problema ha la proprietà della scelta greedy quando una soluzione globalmente ottimale può sempre essere costruita effettuando scelte localmente ottimali, cioè greedy. Formalmente, esiste una soluzione ottimale che inizia con la scelta greedy, quindi non è mai necessario tornare indietro. Per dimostrarlo si usa generalmente un argomento di scambio: si suppone che una soluzione ottimale qualsiasi non includa la scelta greedy, quindi si dimostra che è possibile inserirla al suo posto senza peggiorare il risultato.

# 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], '...')

Struttura ottimale dei sottoproblemi

Sia Greedy sia la DP richiedono una struttura ottimale dei sottoproblemi: la soluzione ottimale del problema completo contiene soluzioni ottimali dei sottoproblemi. La differenza sta nel determinare se le soluzioni dei sottoproblemi possano essere individuate greedy, senza esplorare tutte le opzioni, oppure se sia necessario confrontare più scelte. Se effettua una scelta e il sottoproblema rimanente conserva la stessa struttura, Greedy funziona. Se deve confrontare diverse scelte, usi la 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')

Sottoproblemi sovrapposti: un segnale per la DP

Se lo stesso sottoproblema viene risolto più volte in una decomposizione ricorsiva, è necessaria la DP con memoizzazione. Disegni l'albero delle ricorsioni e cerchi i nodi ripetuti. Per Fibonacci, fib(3) viene calcolato due volte nell'albero di fib(5). Nel problema del resto con le monete [1,3,4] e obiettivo 6, i sottoproblemi per gli obiettivi 3, 2 e 1 compaiono più volte. Sottoproblemi sovrapposti più struttura ottimale dei sottoproblemi = 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)

Problemi greedy classici

Problemi in cui Greedy è dimostrabilmente corretto: (1) pianificazione di attività/intervalli — scelta greedy dell'intervallo con il termine più vicino. (2) albero di copertura minimo — algoritmi di Prim e Kruskal. (3) codifica di Huffman — unisce sempre i due nodi con la frequenza più bassa. (4) zaino frazionabile — seleziona gli elementi in base al rapporto valore/peso più alto. (5) Jump Game — tiene traccia dell'indice massimo raggiungibile. Tutti questi problemi possono essere giustificati con una dimostrazione basata sull'argomento di scambio.

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

Quando Greedy fallisce: controesempi

Trovare un controesempio è il modo più rapido per confutare un'ipotesi greedy. Nel problema del resto con le monete [1, 3, 4] e obiettivo 6, l'approccio greedy, che sceglie prima la moneta più grande, prende 4 e poi 1+1, per un totale di 3 monete. La DP trova invece 3+3, cioè 2 monete. Per lo zaino 0/1, l'approccio greedy basato sul rapporto sceglie l'elemento con il rapporto migliore, ma può non individuare combinazioni che riempiono meglio la capacità. Se riesce a costruire un controesempio in meno di un minuto, passi alla 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)

Tabella di confronto: Greedy e DP

Differenze principali a confronto: complessità temporale — Greedy è generalmente O(n log n), dominata dall'ordinamento; la DP è O(n × stati). complessità spaziale — Greedy usa O(1) spazio ausiliario; la DP usa O(stati). correttezza — Greedy richiede una dimostrazione; la DP è sempre corretta se gli stati e la ricorrenza sono corretti. applicabilità — Greedy si usa per la pianificazione, gli alberi di copertura e Huffman; la DP per lo zaino, l'allineamento di sequenze e i cammini minimi con pesi negativi.

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

Schema decisionale

Schema decisionale per i colloqui tecnici: (1) Può dimostrare la proprietà della scelta greedy con un argomento di scambio? Se sì → Greedy. (2) I sottoproblemi si sovrappongono, cioè lo stesso stato viene raggiunto in più modi? Se sì → DP. (3) Il problema chiede di contare o di enumerare tutte le soluzioni? → DP o backtracking. (4) Il problema chiede un singolo valore ottimale con un ordinamento naturale? Consideri Greedy come possibile approccio. (5) In caso di dubbio, implementi la DP: è sempre corretta se la ricorrenza è corretta, anche se più lenta.

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

Problemi di intervalli: Greedy e DP

I problemi di intervalli si dividono tra Greedy e DP. Per gli intervalli non sovrapposti, in cui occorre rimuoverne il minor numero possibile, si ordinano gli intervalli per orario di fine e si scelgono greedy: Greedy è dimostrabilmente ottimale. Nella pianificazione pesata di intervalli, in cui si massimizza il peso totale, è necessaria la DP, perché gli intervalli con peso elevato possono sovrapporsi a molti intervalli leggeri e richiedono il confronto di tutti i sottoinsiemi validi. Il fattore distintivo è che tutti gli intervalli abbiano lo stesso peso, caso in cui si usa Greedy, oppure pesi variabili, caso in cui si usa la 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

Riconoscere i segnali del problema

Segnali comuni nella descrizione di un problema: "numero minimo di operazioni", "profitto massimo", "selezione ottimale" → potrebbero indicare Greedy o DP: verifichi la sovrapposizione. "contare il numero di modi" → sempre DP. "trovare una pianificazione valida qualsiasi" → potrebbe indicare Greedy. "tutte le possibilità" → backtracking. "non è possibile prendere elementi adiacenti" → DP, come nel problema House Robber. "riunioni, intervalli, attività" → probabilmente Greedy. Collegare i segnali alle famiglie di algoritmi accelera la diagnosi dei problemi durante i colloqui tecnici.

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

Dimostrare la correttezza di Greedy

Per dimostrare la correttezza di un algoritmo greedy, usi l'argomento di scambio: (1) supponga che esista una soluzione ottimale OPT diversa dalla soluzione greedy G alla prima scelta; (2) dimostri che è possibile inserire la scelta greedy in OPT senza aumentare il valore della funzione obiettivo; (3) per induzione, la soluzione greedy è valida quanto qualsiasi soluzione ottimale. Nei colloqui tecnici non è necessaria una dimostrazione completa, ma spiegare l'intuizione dell'argomento di scambio dimostra una comprensione approfondita.

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

Verifica rapida

Metta alla prova la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep presentati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato che: Greedy è corretto quando vale la proprietà della scelta greedy, dimostrabile tramite un argomento di scambio; la DP è necessaria quando i sottoproblemi si sovrappongono, cioè quando lo stesso sottoproblema viene raggiunto in più modi, e non possono essere risolti con una singola regola greedy; inoltre, il modo più rapido per confutare un'ipotesi greedy consiste nel costruire un controesempio con input non convenzionali. Nella prossima lezione risolveremo i problemi di pianificazione e fusione degli intervalli usando l'approccio greedy basato sull'ordinamento per orario di fine.

Domande Frequenti

La lezione «Greedy e DP: quando usare ciascuno» è gratuita?

Sì — il testo completo di «Greedy e DP: quando usare ciascuno» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA Interview Prep include 4 lezioni in totale.

Cosa imparerò in «Greedy e DP: quando usare ciascuno»?

Individui le caratteristiche distintive dei problemi risolvibili con un approccio greedy rispetto a quelli che richiedono la DP, usando la proprietà della scelta greedy e l'argomento dello scambio. Eserciti DSA Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare DSA Interview Prep?

Non è richiesta alcuna esperienza precedente. DSA Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.

Quanto tempo richiede la lezione «Greedy e DP: quando usare ciascuno»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione DSA Interview Prep?

Sì. Ogni lezione DSA Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Greedy e DP: quando usare ciascuno
  2. Pianificazione e fusione degli intervalli
  3. Jump Game I e II
  4. Task Scheduler e Gas Station
← Torna a DSA Interview Prep