Reconnaître la DP : sous-problèmes qui se recouvrent
Identifiez les situations où la récursivité par force brute résout plusieurs fois le même sous-problème, dessinez l’arbre de récursion de Fibonacci et observez l’explosion exponentielle.
Reconnaître la DP : sous-problèmes qui se recouvrent est une leçon Coding 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 Coding Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Coding Interview Prep comprend 4 leçons au total.
Qu'est-ce que la programmation dynamique ?
La programmation dynamique (DP) résout les problèmes complexes en les décomposant en sous-problèmes plus simples qui se chevauchent, en résolvant chaque sous-problème une seule fois et en stockant le résultat pour éviter les calculs redondants. La DP s'applique lorsqu'un problème possède deux éléments : des sous-problèmes qui se chevauchent (le même sous-problème est résolu plusieurs fois dans une récursion naïve) et une sous-structure optimale (la solution optimale peut être construite à partir des solutions optimales des sous-problèmes). Sans ces deux éléments, la DP n'est d'aucune utilité.
# Two ingredients of DP:
# 1. Overlapping sub-problems:
# fib(5) -> fib(4) + fib(3)
# fib(4) -> fib(3) + fib(2) <- fib(3) computed twice!
# Without caching: O(2^n) calls for Fibonacci
# 2. Optimal substructure:
# Shortest path from A to C through B:
# shortest(A,C) = shortest(A,B) + shortest(B,C)
# The sub-path A->B must itself be the shortest
# Contrast with greedy: greedy makes one locally optimal
# choice; DP tries all choices and picks the best.
print('DP = overlapping sub-problems + optimal substructure')Fibonacci : le point d'entrée classique de la DP
La suite de Fibonacci (fib(n) = fib(n-1) + fib(n-2)) est l'exemple classique des sous-problèmes qui se chevauchent. La récursion naïve a une complexité temporelle exponentielle O(2^n), car elle recalcule plusieurs fois les mêmes valeurs. L'arbre de récursion pour fib(6) montre que fib(3) est calculé 3 fois, que fib(2) l'est 5 fois, et ainsi de suite. Cette explosion exponentielle est exactement ce que la DP élimine en stockant les résultats déjà calculés.
import time
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n-1) + fib_naive(n-2)
# Count the calls:
call_count = [0]
def fib_count(n):
call_count[0] += 1
if n <= 1: return n
return fib_count(n-1) + fib_count(n-2)
fib_count(10)
print(f'Calls for fib(10): {call_count[0]}') # 177 calls for n=10!
call_count[0] = 0
fib_count(20)
print(f'Calls for fib(20): {call_count[0]}') # 21891 calls
# n=30 -> ~2.7 million calls: exponential growthVisualiser l'arbre de récursion
Représenter l'arbre de récursion pour fib(5) révèle le gaspillage : chaque nœud engendre deux enfants, et des sous-arbres identiques apparaissent plusieurs fois. Le nombre total de nœuds de l'arbre est O(2^n). Lorsque vous observez ce schéma — des appels de fonction identiques avec les mêmes arguments qui se répètent dans l'arbre — cela indique que la DP peut être utile en mettant les résultats en cache. Cette capacité de visualisation est essentielle : si vous pouvez repérer les sous-arbres répétés, vous savez que la DP est applicable.
# fib(5) recursion tree (simplified):
# fib(5)
# / \
# fib(4) fib(3)
# / \ / \
# fib(3) fib(2) fib(2) fib(1)
# / \ \
# fib(2) fib(1) fib(1)
# / \
# fib(1) fib(0)
# fib(3) appears TWICE
# fib(2) appears THREE TIMES
# Each redundant call wastes exponential time
# Key insight: fib(n) only has O(n) DISTINCT sub-problems
# (fib(0), fib(1), ..., fib(n))
# DP computes each ONCE -> O(n) total
print('Distinct sub-problems: O(n) but naive calls: O(2^n)')Identifier les sous-problèmes qui se chevauchent
Pour reconnaître les sous-problèmes qui se chevauchent, écrivez la récursion par force brute, puis demandez-vous : « Y a-t-il plusieurs appels récursifs avec les mêmes (SAME) arguments ? » Si oui, la DP peut être utile. Parmi les formulations courantes dans les énoncés, on trouve : « nombre minimal/maximal de X », « combien de façons de faire Y », « pouvons-nous atteindre Z ? ». Ces formulations indiquent presque toujours un problème de sous-structure optimale, où la réponse à la position i dépend des réponses aux positions précédentes.
# DP signal phrases in problem statements:
# 'minimum number of coins to make amount X'
# 'maximum profit from stock trades'
# 'number of ways to climb n stairs'
# 'can you reach the last index?'
# 'longest common subsequence'
# 'edit distance between two strings'
# All have this shape:
# solve(input) = f(solve(smaller_input_1), solve(smaller_input_2), ...)
# And multiple branches end up calling solve with the same argument.
# If the recursion tree has repeated nodes: DP
# If subproblems are all independent: divide-and-conquer (no DP needed)
print('Repeated arguments in recursion tree -> DP')Explication de la sous-structure optimale
La sous-structure optimale signifie que la solution optimale d'un problème peut être construite à partir des solutions optimales de ses sous-problèmes. Par exemple, le plus court chemin de A à C passant par B est optimal si et seulement si les sous-chemins A→B et B→C sont chacun individuellement optimaux. Si cette propriété est vérifiée, vous pouvez construire l'optimum global de bas en haut à partir d'optima locaux. Les problèmes dépourvus de sous-structure optimale (par exemple, le plus long chemin dans un graphe général avec des cycles) ne peuvent pas être résolus avec la DP.
# Optimal substructure examples:
# SHORTEST PATH: shortest(A,C) = min over all B: shortest(A,B) + w(B,C)
# -> Sub-paths must be optimal: YES, has optimal substructure
# LONGEST PATH (no cycles, DAG): can also use DP
# -> Longer path through node B means sub-path A->B must be longest
# LONGEST PATH (with cycles): NO optimal substructure
# -> Best path from A to C might reuse nodes: sub-problems not independent
# COIN CHANGE: min coins for amount n = 1 + min(min coins for n-coin_i)
# -> YES: optimal for n-coin_i is needed for optimal n
print('Optimal substructure: build global optimum from local optima')Monter des marches : votre première DP
Monter des marches (LeetCode n° 70) : combien de façons distinctes pouvez-vous monter n marches en avançant d'une ou de deux marches à la fois ? Soit dp[i] le nombre de façons d'atteindre la marche i. Vous pouvez atteindre la marche i depuis la marche i-1 (un pas) ou depuis la marche i-2 (deux pas), donc dp[i] = dp[i-1] + dp[i-2]. C'est la suite de Fibonacci ! Cas de base : dp[1] = 1 et dp[2] = 2. Reconnaître que « monter des marches » se réduit à Fibonacci est une idée classique des entretiens.
def climb_stairs(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1 # 1 way to reach step 1
dp[2] = 2 # 2 ways to reach step 2: (1+1) or (2)
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2] # come from i-1 or i-2
return dp[n]
for n in range(1, 8):
print(f'climb_stairs({n}) = {climb_stairs(n)}')
# 1, 2, 3, 5, 8, 13, 21 -- Fibonacci sequence!Le cadre DP : définition, récurrence, ordre
Un cadre DP fiable en 3 étapes : 1. Définissez l'état — que représente dp[i] (ou dp[i][j]) ? Écrivez-le en anglais. 2. Écrivez la récurrence — exprimez dp[i] en fonction de sous-problèmes plus petits. Incluez tous les cas. 3. Déterminez l'ordre de remplissage — assurez-vous que dp[i-1] (et les autres dépendances) sont calculés avant dp[i]. Les cas de base initialisent la frontière. Ce cadre transforme une intuition DP floue en un plan d'implémentation concret.
# Framework applied to climbing stairs:
# Step 1 - Define state:
# dp[i] = number of distinct ways to reach step i
# Step 2 - Recurrence:
# dp[i] = dp[i-1] + dp[i-2] (come from step i-1 or i-2)
# Step 3 - Fill order:
# Compute dp[1], dp[2], dp[3], ..., dp[n] in order
# Because dp[i] depends on dp[i-1] and dp[i-2] (smaller)
# Base cases: dp[1]=1, dp[2]=2
# Framework applied to coin change:
# Step 1: dp[amount] = minimum coins to make that amount
# Step 2: dp[i] = 1 + min(dp[i-coin] for coin in coins if i >= coin)
# Step 3: Fill i from 1 to amount
# Base: dp[0] = 0 (zero coins for zero amount)
print('DP framework: define state -> recurrence -> fill order')Quand NOT utiliser DP
La DP n'est pas toujours la solution. Utilisez une approche gloutonne lorsqu'un choix localement optimal mène toujours à la solution globalement optimale (sélection d'activités, jeu de sauts I). Utilisez diviser pour régner lorsque les sous-problèmes ne se chevauchent pas (tri fusion, recherche binaire). Utilisez BFS lorsque le problème consiste à trouver le plus court chemin dans un graphe non pondéré. La DP est correcte, mais souvent disproportionnée lorsqu'une approche gloutonne ou plus simple existe. Lors des entretiens, expliquez pourquoi vous avez choisi la DP plutôt que les autres approches.
# DP vs alternatives:
# Problem: can you jump to the end of the array?
# Greedy: track max reachable index -> O(n) O(1) BETTER than DP
# Problem: shortest path unweighted graph?
# BFS: O(V+E) BETTER than DP on general graph
# Problem: sort an array?
# Comparison sort: O(n log n), no DP needed
# DP IS the right choice when:
# - Greedy fails (choices interact)
# - Need to count/enumerate all possibilities
# - Problem has 'how many ways' or 'minimum/maximum' flavor
# - Recursion tree clearly shows overlapping sub-problems
print('Ask: does greedy fail? If yes, consider DP.')Compter les sous-problèmes distincts
Le nombre de sous-problèmes distincts détermine la complexité en temps et en espace mémoire de la DP. Pour une DP 1D sur une entrée de taille n, il y a O(n) sous-problèmes. Pour une DP 2D sur deux entrées de tailles m et n, il y a O(mn) sous-problèmes. Chaque sous-problème est résolu en O(k) temps (pour k choix à chaque étape), ce qui donne un temps total de O(n*k) ou O(mn*k). Comptez toujours d'abord les sous-problèmes distincts : cela vous donne la complexité temporelle de la DP avant même de l'implémenter.
# Sub-problem count examples:
# Problem | Sub-problems | Each costs | Total
# Fibonacci | O(n) | O(1) | O(n)
# Coin change | O(amount) | O(coins) | O(amount * coins)
# LCS (m,n chars) | O(m*n) | O(1) | O(m*n)
# Edit distance | O(m*n) | O(1) | O(m*n)
# 0/1 Knapsack | O(n*W) | O(1) | O(n*W)
# Matrix chain | O(n^2) | O(n) | O(n^3)
# Rule: DP time = (# distinct sub-problems) * (time per sub-problem)
print('Time = subproblems * work-per-subproblem')Cambriolage de maisons : choix qui se chevauchent
Cambriolage de maisons (LeetCode #198) consiste à trouver le montant maximal que vous pouvez voler dans une rangée de maisons sans voler deux maisons adjacentes. Pour chaque maison, vous avez le choix : la voler (ajouter sa valeur et ignorer la précédente) ou l'ignorer (prendre le meilleur résultat précédent). dp[i] = max(dp[i-1], dp[i-2] + nums[i]). Ce schéma de choix à chaque étape est la relation de récurrence de DP 1D la plus simple et apparaît dans des dizaines de problèmes d'entretien.
def rob(nums):
if not nums: return 0
if len(nums) == 1: return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-1], # skip house i
dp[i-2] + nums[i]) # rob house i
return dp[-1]
print(rob([1, 2, 3, 1])) # 4: rob house 0 and 2 (1+3)
print(rob([2, 7, 9, 3, 1]))# 12: rob house 0, 2, 4 (2+9+1)
print(rob([2, 1, 1, 2])) # 4: rob house 0 and 3Vérification de cohérence : force brute ou DP
Vérifiez toujours votre DP par rapport à une solution par force brute sur de petites entrées. La force brute constitue votre référence de vérité. Une fois que la DP correspond à la force brute pour tous les cas d'essai, vous savez que la relation de récurrence est correcte. Ce n'est qu'alors que vous optimiserez l'espace mémoire. Cette approche pilotée par les essais — force brute → DP descendante → DP ascendante → DP optimisée en espace mémoire — est la méthode professionnelle pour développer et vérifier des solutions DP pendant un entretien.
# Brute-force for house robber (exponential)
def rob_brute(nums, i=0):
if i >= len(nums):
return 0
# Option 1: rob house i
rob_it = nums[i] + rob_brute(nums, i + 2)
# Option 2: skip house i
skip_it = rob_brute(nums, i + 1)
return max(rob_it, skip_it)
# Verify on small inputs:
test_cases = [[1,2,3,1], [2,7,9,3,1], [2,1,1,2]]
for tc in test_cases:
bf = rob_brute(tc)
dp = rob(tc)
print(f'{tc}: brute={bf}, dp={dp}, match={bf==dp}')Vérification rapide
Vérifiez 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 : les deux ingrédients de la DP (les sous-problèmes qui se chevauchent et la sous-structure optimale), comment visualiser l'arbre de récursion pour repérer les appels répétés, le cadre DP en trois étapes (définition de l'état, récurrence, ordre de remplissage), ainsi que les premiers exemples, notamment Fibonacci, les escaliers et le cambriolage de maisons. Ensuite, nous implémenterons la DP descendante avec mémoïsation.
Questions Fréquemment Posées
La leçon « Reconnaître la DP : sous-problèmes qui se recouvrent » est-elle gratuite ?
Oui — le texte complet de « Reconnaître la DP : sous-problèmes qui se recouvrent » 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 Coding Interview Prep, passe à CoddyKit PRO. Le cours Coding Interview Prep comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Reconnaître la DP : sous-problèmes qui se recouvrent » ?
Identifiez les situations où la récursivité par force brute résout plusieurs fois le même sous-problème, dessinez l’arbre de récursion de Fibonacci et observez l’explosion exponentielle. Tu pratiques Coding 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 Coding Interview Prep ?
Aucune expérience préalable n'est requise. Coding 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 « Reconnaître la DP : sous-problèmes qui se recouvrent » ?
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 Coding Interview Prep ?
Oui. Chaque leçon Coding 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
- Reconnaître la DP : sous-problèmes qui se recouvrent
- DP descendante avec mémoïsation
- DP ascendante avec tabulation
- Rendu de monnaie et escalier au coût minimal