House Robber: ricorrenza prendi o salta
Modelli la decisione rob/skip come ricorrenza DP, riduca lo spazio a due variabili ed estenda la soluzione alle case disposte in cerchio
House Robber: ricorrenza prendi o salta è una lezione Coding 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 Coding Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Coding Interview Prep include 4 lezioni in totale.
Il problema del ladro di case
Il problema del ladro di case chiede: dato un array di interi non negativi che rappresentano la somma di denaro presente in ogni casa, qual è la somma massima che è possibile rubare senza derubare due case adiacenti? Ad esempio, [2, 7, 9, 3, 1] restituisce 12, derubando le case 0, 2 e 4. È un classico problema di DP 1D in cui a ogni passaggio si prende una decisione binaria.
nums = [2, 7, 9, 3, 1]
# Can't rob adjacent houses
# Options: rob index 0 and 2 and 4 → 2+9+1=12
# or rob index 1 and 3 → 7+3=10
print('Max profit:', 12) # answer is 12Definire la ricorrenza
Sia dp[i] la somma massima rubata dalle prime i+1 case. Per ogni casa i si hanno due possibilità: saltarla, prendendo dp[i-1], oppure derubarla, prendendo nums[i] + dp[i-2]. La ricorrenza è dp[i] = max(dp[i-1], nums[i] + dp[i-2]). Questo è il modello fondamentale prendi o salta, che ricorre in molti problemi di DP.
# Recurrence: dp[i] = max(dp[i-1], nums[i] + dp[i-2])
# Base cases:
# dp[0] = nums[0] (only one house, rob it)
# dp[1] = max(nums[0], nums[1]) (take the richer of the two)
def rob(nums):
n = len(nums)
if n == 1: return nums[0]
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i-1], nums[i] + dp[i-2])
return dp[-1]
print(rob([2, 7, 9, 3, 1])) # 12Ripercorrere la tabella DP
Per [2, 7, 9, 3, 1], ripercorriamo la tabella: dp[0] = 2, dp[1] = max(2, 7) = 7, dp[2] = max(7, 9+2) = 11, dp[3] = max(11, 3+7) = 11, dp[4] = max(11, 1+11) = 12. La risposta finale è dp[4] = 12. Ripercorrere manualmente la tabella conferma che la ricorrenza gestisce correttamente sia la scelta di prendere sia quella di saltare a ogni posizione.
nums = [2, 7, 9, 3, 1]
dp = [0] * len(nums)
dp[0] = 2
dp[1] = max(2, 7) # 7
for i in range(2, len(nums)):
skip = dp[i-1]
take = nums[i] + dp[i-2]
dp[i] = max(skip, take)
print(f'dp[{i}] = max({skip}, {nums[i]}+{dp[i-2]}) = {dp[i]}')
print('Answer:', dp[-1])Ridurre lo spazio a O(1)
La tabella DP guarda solo due posizioni indietro, quindi è possibile sostituire l'intero array con due variabili: prev2, che rappresenta il valore di due passaggi precedente, e prev1, quello di un passaggio precedente. Dopo ogni iterazione si esegue lo scorrimento: prev2 = prev1 e prev1 = current. In questo modo la memoria si riduce da O(n) a O(1), mantenendo la complessità temporale O(n).
def rob_optimised(nums):
if not nums: return 0
if len(nums) == 1: return nums[0]
prev2 = nums[0]
prev1 = max(nums[0], nums[1])
for i in range(2, len(nums)):
curr = max(prev1, nums[i] + prev2)
prev2 = prev1
prev1 = curr
return prev1
print(rob_optimised([2, 7, 9, 3, 1])) # 12
print(rob_optimised([1, 2, 3, 1])) # 4Casi limite da gestire
Si esegua sempre il test della soluzione sui casi limite: un array vuoto, che restituisce 0; un array con un solo elemento, che restituisce quell'elemento; e un array con due elementi, che restituisce il maggiore dei due. Durante un colloquio, menzionare e gestire questi casi dimostra accuratezza. Il controllo if n == 1 impedisce l'accesso fuori dai limiti dell'array quando si accede a nums[1] per calcolare dp[1].
def rob(nums):
if not nums: return 0
if len(nums) == 1: return nums[0]
prev2 = nums[0]
prev1 = max(nums[0], nums[1])
for i in range(2, len(nums)):
curr = max(prev1, nums[i] + prev2)
prev2, prev1 = prev1, curr
return prev1
print(rob([])) # 0
print(rob([5])) # 5
print(rob([3, 10])) # 10
print(rob([10, 3])) # 10Ladro di case II: case circolari
La variante circolare (LeetCode 213) dispone le case in cerchio, rendendo adiacenti la prima e l'ultima. Non è possibile applicare direttamente la ricorrenza lineare. L'intuizione chiave è la seguente: o si deruba la prima casa escludendo l'ultima, oppure si esclude la prima includendo l'ultima. Si esegue l'algoritmo lineare del ladro di case su entrambi i sottoarray e si prende il massimo.
def rob_linear(nums):
prev2, prev1 = 0, 0
for n in nums:
prev2, prev1 = prev1, max(prev1, n + prev2)
return prev1
def rob_circular(nums):
if len(nums) == 1: return nums[0]
# Either include first (exclude last) or include last (exclude first)
return max(rob_linear(nums[:-1]), rob_linear(nums[1:]))
print(rob_circular([2, 3, 2])) # 3
print(rob_circular([1, 2, 3, 1])) # 4Perché l'approccio greedy fallisce in questo caso
Un approccio greedy ingenuo potrebbe tentare di derubare sempre la casa disponibile con il valore maggiore. Tuttavia, fallisce su input come [2, 1, 1, 2]: l'approccio greedy sceglie la casa 0, di valore 2, e poi la casa 3, anch'essa di valore 2, per un totale di 4, mentre derubare le case 0 e 2 dà anch'esso 3. Aspetti — in questo caso l'approccio greedy funziona! Si provi però [1, 3, 1, 3, 100]: l'approccio greedy sceglie i valori 3 e 3, agli indici 1 e 3, ottenendo 6, e non considera la soluzione ottimale 1+1+100=102. La DP è necessaria perché scelte ottimali localmente non garantiscono un ottimo globale.
# Greedy failure example
nums = [1, 3, 1, 3, 100]
# Greedy: pick max each step
# picks 3 (index 1), then 3 (index 3) → total 6
# DP optimal: pick 1 (index 0) + 1 (index 2) + 100 (index 4) → 102
def rob(nums):
prev2, prev1 = 0, 0
for n in nums:
prev2, prev1 = prev1, max(prev1, n + prev2)
return prev1
print(rob(nums)) # 102Riconoscere il modello prendi o salta
Il modello prendi o salta si generalizza oltre il problema del ladro di case. Ogni volta che si scorre un array e a ogni posizione si sceglie tra includere l'elemento corrente, saltando quello precedente, oppure escluderlo, mantenendo il risultato precedente, si è di fronte a una DP prendi o salta. Vincoli come nessun elemento adiacente o nessun intervallo sovrapposto indicano che è opportuno applicare questo modello.
# General take-or-skip template
def take_or_skip(values, gap=1):
'''Max sum where selected elements must be at least gap+1 apart.'''
n = len(values)
if n == 0: return 0
# dp[i] = best up to index i
dp = [0] * (n + gap)
for i in range(n):
take = values[i] + (dp[i - 1] if i >= 1 else 0)
skip = dp[i + gap - 1] if i + gap - 1 < len(dp) else 0
dp[i + gap] = max(skip, take)
return dp[-1]
print(take_or_skip([2, 7, 9, 3, 1])) # house robber-likeVariante Delete and Earn
Delete and Earn (LeetCode 740) chiede: per ogni numero scelto si guadagna num × count(num), ma è necessario eliminare tutte le occorrenze di num-1 e num+1. Il problema si riduce direttamente a quello del ladro di case: si costruisce un array earn[v] = v × count(v) per tutti i valori, quindi si esegue l'algoritmo del ladro di case su questo array. Riconoscere queste riduzioni è una competenza fondamentale nei colloqui.
from collections import Counter
def delete_and_earn(nums):
if not nums: return 0
count = Counter(nums)
max_val = max(nums)
# earn[v] = total points from taking all v's
earn = [v * count[v] for v in range(max_val + 1)]
# Now run house robber on earn
prev2, prev1 = 0, 0
for e in earn:
prev2, prev1 = prev1, max(prev1, e + prev2)
return prev1
print(delete_and_earn([3, 4, 2])) # 6 (take 3+3=no, take 4+2=6)
print(delete_and_earn([2, 2, 3, 3, 3, 4])) # 9 (take all 3s)Ladro di case III: albero binario
In Ladro di case III, le case sono disposte in un albero binario. Non è possibile derubare contemporaneamente un nodo e il suo genitore diretto. Si definisca una funzione helper che restituisce due valori: rob(node) → (rob_root, skip_root). Se si deruba la radice, si sommano i valori skip dei due figli. Se si salta la radice, si somma il valore migliore di ciascun figlio. Si tratta di una DFS post-order con una decisione prendi o salta a ogni nodo.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def rob_tree(root):
def dfs(node):
if not node: return (0, 0) # (rob, skip)
l_rob, l_skip = dfs(node.left)
r_rob, r_skip = dfs(node.right)
rob = node.val + l_skip + r_skip
skip = max(l_rob, l_skip) + max(r_rob, r_skip)
return (rob, skip)
return max(dfs(root))
# Tree: 3 -> 2,3 -> None,3,None,1
root = TreeNode(3, TreeNode(2, None, TreeNode(3)), TreeNode(3, None, TreeNode(1)))
print(rob_tree(root)) # 7Complessità e discussione durante il colloquio
La versione lineare del problema del ladro di case richiede O(n) tempo e O(1) spazio grazie all'ottimizzazione con due variabili. Anche la variante circolare richiede O(n) tempo, perché richiama due volte la versione lineare. La variante su albero richiede O(n) tempo e O(h) spazio, dove h è l'altezza dell'albero. Durante un colloquio, dichiari sempre la complessità dopo aver scritto il codice e menzioni l'ottimizzazione dello spazio: dimostra che sa andare oltre una prima soluzione funzionante.
# Summary of complexities
# Linear House Robber:
# Time: O(n), Space: O(1) with two-variable trick
# Circular House Robber:
# Time: O(n), Space: O(1) (two passes)
# Tree House Robber:
# Time: O(n), Space: O(h) call stack
# Quick benchmark
import time
import random
nums = [random.randint(0, 100) for _ in range(10**6)]
start = time.time()
prev2 = prev1 = 0
for n in nums:
prev2, prev1 = prev1, max(prev1, n + prev2)
print(f'1M elements in {time.time()-start:.3f}s, result={prev1}')Verifica rapida
Metta alla prova la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato: la ricorrenza di scelta o salto dp[i] = max(dp[i-1], nums[i] + dp[i-2]), la riduzione dello spazio O(n) a O(1) con due variabili a scorrimento ed estendere questo schema ad array circolari e alberi binari. Nella prossima lezione esamineremo i problemi Maximum Subarray e Maximum Product Subarray usando l'algoritmo di Kadane.
Domande Frequenti
La lezione «House Robber: ricorrenza prendi o salta» è gratuita?
Sì — il testo completo di «House Robber: ricorrenza prendi o salta» è 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 Coding Interview Prep, passa a CoddyKit PRO. Il corso Coding Interview Prep include 4 lezioni in totale.
Cosa imparerò in «House Robber: ricorrenza prendi o salta»?
Modelli la decisione rob/skip come ricorrenza DP, riduca lo spazio a due variabili ed estenda la soluzione alle case disposte in cerchio Eserciti Coding 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 Coding Interview Prep?
Non è richiesta alcuna esperienza precedente. Coding 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 «House Robber: ricorrenza prendi o salta»?
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 Coding Interview Prep?
Sì. Ogni lezione Coding 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
- House Robber: ricorrenza prendi o salta
- Subarray a somma massima e subarray a prodotto massimo
- Word Break e segmentazione delle stringhe
- Decode Ways e conteggio dei percorsi