House Robber : récurrence prendre ou ignorer
Modélisez la décision de cambrioler ou d’ignorer une maison comme une récurrence de DP, réduisez l’espace à deux variables et étendez la solution aux maisons disposées en cercle.
House Robber : récurrence prendre ou ignorer est une leçon DSA Interview Prep gratuite sur CoddyKit. Ceci est la leçon 1 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage DSA Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours DSA Interview Prep comprend 4 leçons au total.
Le problème du voleur de maisons
Le problème du voleur de maisons demande, étant donné un tableau d'entiers non négatifs représentant la somme d'argent de chaque maison, de trouver le montant maximal que vous pouvez voler sans cambrioler deux maisons adjacentes. Par exemple, [2, 7, 9, 3, 1] donne 12, en cambriolant les maisons 0, 2 et 4. Il s'agit d'un problème classique de DP 1D, dans lequel vous prenez une décision binaire à chaque étape.
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 12Définir la récurrence
Soit dp[i] le montant maximal volé dans les i+1 premières maisons. Pour chaque maison i, vous avez deux choix : la passer, en prenant dp[i-1], ou la cambrioler, en prenant nums[i] + dp[i-2]. La récurrence est dp[i] = max(dp[i-1], nums[i] + dp[i-2]). Il s'agit du schéma fondamental prendre-ou-passer, qui apparaît dans de nombreux problèmes de 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])) # 12Parcourir la table de DP
Pour [2, 7, 9, 3, 1], parcourons la table : 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 réponse finale est dp[4] = 12. Parcourir manuellement la table confirme que la récurrence gère correctement le fait de prendre ou de passer à chaque position.
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])Réduire l'espace à O(1)
La table de DP ne consulte jamais plus de deux positions en arrière, nous pouvons donc remplacer le tableau entier par deux variables : prev2, à deux étapes en arrière, et prev1, à une étape en arrière. Après chaque itération, nous les décalons : prev2 = prev1 et prev1 = current. Cela réduit la mémoire de O(n) à O(1), tout en conservant une complexité en time de 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])) # 4Gérer les cas limites
Testez toujours votre solution avec des cas limites : un tableau vide, qui doit renvoyer 0 ; un tableau à un seul élément, qui doit renvoyer cet élément ; et un tableau à deux éléments, qui doit renvoyer le maximum des deux. Lors d'un entretien, mentionner et gérer ces cas démontre votre rigueur. La condition if n == 1 évite un accès à un indice inexistant lors de l'accès à nums[1] pour 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])) # 10Voleur de maisons II : maisons disposées en cercle
La variante circulaire (LeetCode 213) dispose les maisons en cercle, de sorte que la première et la dernière sont adjacentes. Vous ne pouvez pas appliquer directement la récurrence linéaire. L'idée clé est la suivante : soit vous cambriolez la première maison et excluez la dernière, soit vous excluez la première et incluez la dernière. Exécutez le voleur de maisons linéaire sur les deux sous-tableaux et prenez le maximum.
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])) # 4Pourquoi l'approche gloutonne échoue ici
Une approche gloutonne naïve pourrait essayer de toujours cambrioler la maison disponible qui a la plus grande valeur. Cependant, elle échoue avec des entrées comme [2, 1, 1, 2] : elle choisit la maison 0, d'une valeur de 2, puis la maison 3, également d'une valeur de 2, pour un total de 4, tandis que cambrioler les maisons 0 et 2 ne donne que 3. Attendez — dans ce cas, l'approche gloutonne fonctionne ! Mais essayez [1, 3, 1, 3, 100] : elle choisit les maisons de valeur 3 et 3, aux indices 1 et 3, pour un total de 6, et manque l'optimum 1+1+100 = 102. La DP est nécessaire, car des choix localement optimaux ne garantissent pas un optimum global.
# 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)) # 102Reconnaître le schéma prendre-ou-passer
Le schéma prendre-ou-passer se généralise au-delà du problème du voleur de maisons. Chaque fois que vous parcourez un tableau et qu'à chaque position vous choisissez entre inclure l'élément courant, en passant le précédent, ou l'exclure, en conservant le résultat précédent, vous avez affaire à une DP prendre-ou-passer. Repérez des contraintes comme aucun élément adjacent ou aucun intervalle qui se chevauche : elles indiquent souvent qu'il faut appliquer ce schéma.
# 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 Supprimer et gagner
Supprimer et gagner (LeetCode 740) demande ceci : pour chaque nombre choisi, vous gagnez num × count(num), mais devez supprimer toutes les occurrences de num-1 et num+1. Cela se réduit directement au problème du voleur de maisons : construisez un tableau earn[v] = v × count(v) pour toutes les valeurs, puis appliquez le voleur de maisons à ce tableau. Reconnaître les réductions est une compétence essentielle en entretien.
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)Voleur de maisons III : arbre binaire
Dans Voleur de maisons III, les maisons sont disposées en arbre binaire. Vous ne pouvez pas cambrioler simultanément un nœud et son parent direct. Définissez une fonction auxiliaire qui renvoie deux valeurs : rob(node) → (rob_root, skip_root). Si vous cambriolez la racine, additionnez les valeurs correspondant au fait de ne pas cambrioler chacun de ses deux enfants. Si vous passez la racine, additionnez la meilleure valeur obtenue pour chaque enfant. Il s'agit d'un DFS en post-ordre, avec une décision prendre-ou-passer à chaque nœud.
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)) # 7Complexité et discussion en entretien
Le voleur de maisons linéaire s'exécute en O(n) time et utilise un espace O(1) grâce à l'optimisation à deux variables. La variante circulaire s'exécute également en O(n) time, puisqu'elle appelle deux fois la version linéaire. La variante avec arbre s'exécute en O(n) time et utilise un espace O(h), où h est la hauteur de l'arbre. Lors d'un entretien, indiquez toujours la complexité après avoir écrit le code et mentionnez l'optimisation de l'espace : cela montre que vous allez au-delà d'une première solution fonctionnelle.
# 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}')Vérification rapide
Évaluez votre compréhension des concepts de structures de données et d'algorithmes — préparation aux entretiens de programmation présentés dans cette leçon.
Récapitulatif de la leçon
Dans cette leçon, vous avez appris : la récurrence prendre-ou-ignorer dp[i] = max(dp[i-1], nums[i] + dp[i-2]), la réduction de l'espace O(n) à O(1) à l'aide de deux variables mises à jour au fil du parcours, et l'extension de ce schéma aux tableaux circulaires et aux arbres binaires. Ensuite, nous étudierons les problèmes du sous-tableau de somme maximale et du sous-tableau de produit maximal à l'aide de l'algorithme de Kadane.
Questions Fréquemment Posées
La leçon « House Robber : récurrence prendre ou ignorer » est-elle gratuite ?
Oui — le texte complet de « House Robber : récurrence prendre ou ignorer » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours DSA Interview Prep, passe à CoddyKit PRO. Le cours DSA Interview Prep comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « House Robber : récurrence prendre ou ignorer » ?
Modélisez la décision de cambrioler ou d’ignorer une maison comme une récurrence de DP, réduisez l’espace à deux variables et étendez la solution aux maisons disposées en cercle. Tu pratiques DSA Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer DSA Interview Prep ?
Aucune expérience préalable n'est requise. DSA Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 1 sur 4.
Combien de temps prend la leçon « House Robber : récurrence prendre ou ignorer » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon DSA Interview Prep ?
Oui. Chaque leçon DSA Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- House Robber : récurrence prendre ou ignorer
- Sous-tableau de somme maximale et sous-tableau de produit maximal
- Word Break et segmentation de chaînes
- Décoder des façons et compter des chemins