La suite de Fibonacci est définie par :
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
return fibonacci(n-1) + fibonacci(n-2)fibonacci(4)
fibonacci(3)
fibonacci(2)
fibonacci(1)
fibonacci(0)
fibonacci(1)
fibonacci(2)
fibonacci(1)
fibonacci(0)
def fibm(n, memo=None):
if memo is None:
memo = {0: 0, 1: 1}
if n not in memo:
memo[n] = fibm(n-1, memo) + fibm(n-2, memo)
return memo[n]La programmation dynamique s’applique aux problèmes qui vérifient deux propriétés :
La méthode consiste à résoudre chaque sous-problème une seule fois et à mémoriser son résultat pour ne pas le recalculer.
Les deux approches ont la même complexité temporelle. La tabulation évite les risques liés à la récursion et permet parfois d’optimiser la mémoire.
Le terme programmation ne signifie pas écrire un programme mais planifier une solution. C’est une méthode algorithmique, introduite par Richard Bellman dans les années 1950.
top-downRappel : un algorithme glouton est un algorithme qui résout un problème en faisant des choix locaux optimaux. Il ne revient pas sur ses choix.
def rendu_monnaie_glouton(sar, pieces):
pieces = sorted(pieces, reverse=True)
rendu = []
for p in pieces:
while sar >= p:
rendu.append(p)
sar -= p
return renduIl existe des cas où l’algorithme glouton ne donne pas la solution optimale, ou ne donne pas de solution du tout, alors qu’une solution existe (ici : 1 pièce de 5 cts, et 3 pièces de 2 cts).
On peut donc écrire un algorithme récursif pour résoudre ce problème.
Attention : il faut prévoir le cas où on ne peut pas rendre la somme. CF code après.
def rendu_dyn(sar, pieces): # renvoie bool, liste = trouvé ? et liste pces
# Cas de base, on a trouvé youpi
if sar == 0:
return True, []
# On recherche la meilleure (= la plus courte) liste de pieces
best_liste, best_size = None, None
# On parcours toutes les pieces disponibles
for p in pieces:
# Si la piece est > a la somme a rendre = piece inutile, next
if p > sar:
continue
# On demande a rendu_dyn de renvoyer la solution optimale
# pour sar-p
# Il renvoie une liste `liste` de pieces si il a trouvé `trouve`
trouve, liste = rendu_dyn(sar-p, pieces)
# Si rendu_dyn n'a pas trouvé, on passe a la piéce suivante
if not trouve:
continue
# Sinon on check si la solution avec cette piece est
# la meilleure (plus courte)
if (best_size is None # c le premier, on prend de ttes façons
or len(liste) < best_size): # c une meilleure solution
best_size = len(liste) + 1 # on enregistre la taille (= la liste + la piece)
best_liste = [p] + liste # on enregistre la liste (+ la piece)
# si on a pas trouvé, on renvoie False
if best_size is None:
return False, []
# On a trouvé, on renvoie la liste
return True, best_liste
rendu_dyn(11, [10,2,5])def rendu_dyn_mem(sar, pieces, memo=None): # renvoie bool, liste = trouvé ? et liste pces
if memo is None:
memo = {0: (True,[])}
if sar in memo:
return memo[sar]
# On recherche la meilleure (= la plus courte) liste de pieces
best_liste, best_size = None, None
# On parcours toutes les pieces disponibles
for p in pieces:
# Si la piece est > a la somme a rendre = piece inutile, next
if p > sar:
continue
# On demande a rendu_dyn_mem de renvoyer la solution optimale
# pour sar-p
# Il renvoie une liste `liste` de pieces si il a trouvé `trouve`
trouve, liste = rendu_dyn_mem(sar-p, pieces, memo)
# Si rendu_dyn_mem n'a pas trouvé, on passe a la piéce suivante
if not trouve:
continue
# Sinon on check si la solution avec cette piece est
# la meilleure (plus courte)
if (best_size is None # c le premier, on prend de ttes façons
or len(liste) < best_size): # c une meilleure solution
best_size = len(liste) + 1 # on enregistre la taille (= la liste + la piece)
best_liste = [p] + liste # on enregistre la liste (+ la piece)
# si on a pas trouvé, on renvoie False
if best_size is None:
# On enregistre la solution dans le memo
memo[sar] = False, []
return False, []
# On enregistre la solution dans le memo
memo[sar] = True, best_liste
# On a trouvé, on renvoie la liste
return True, best_liste
rendu_dyn_mem(11, [10,2,5])bottom-upLa mémoïsation est une approche descendante (top-down) :
import sys
sys.setrecursionlimit(10000) # contournement, pas une vraie solutionL’approche ascendante (bottom-up) résout d’abord les petits sous-problèmes, puis construit la solution au problème de taille à partir de ceux-ci. Elle n’utilise pas de récursion.
Au lieu de partir de et de descendre, on part des cas de base et on remonte :
dp de taille
dp[0] = 0 et dp[1] = 1dp[i] = dp[i-1] + dp[i-2]dp[n]def fibt(n):
if n == 0:
return 0
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dp | 0 | 1 | 1 | 2 | 3 | 5 | 8 |
On remplit le tableau de gauche à droite. Chaque valeur ne dépend que des deux cases précédentes.
Pour calculer dp[i], on n’a besoin que de
dp[i-1] et dp[i-2]. Inutile de conserver tout
le tableau.
def fibt_opt(n):
if n == 0:
return 0
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return bCette optimisation est caractéristique de l’approche ascendante : elle n’est pas possible avec la mémoïsation, qui doit conserver tous les appels en attente sur la pile.
On construit la solution pour toutes les sommes de à , dans l’ordre croissant.
dp de taille
dp[0] = (True, []) : rendre 0 est toujours possible,
avec une liste videdp[m - p] a été trouvé
(trouve)len(liste) + 1 < best_size, on met à
jour la meilleure solutiondp[m] = (False, [])def rendu_topdown(sar, pieces):
# dp[m] = (trouvé, liste) : solution optimale pour rendre la somme m
# Cas de base : rendre 0 est toujours possible, avec une liste vide
dp = {0: (True, [])}
# On remplit dp pour toutes les sommes de 1 à sar (ordre croissant)
for m in range(1, sar + 1):
best_liste, best_size = None, None
# On parcourt toutes les pièces disponibles
for p in pieces:
# Si la pièce est > à la somme courante = pièce inutile, next
if p > m:
continue
# dp[m-p] est forcément déjà calculé (m-p < m)
# On récupère la solution optimale pour m-p
trouve, liste = dp[m - p]
# Si on n'a pas pu rendre m-p, cette pièce ne mène à rien
if not trouve:
continue
# Sinon on vérifie si la solution avec cette pièce est
# la meilleure (= la plus courte) trouvée jusqu'ici
if (best_size is None # c'est la première, on prend de toutes façons
or len(liste) + 1 < best_size): # c'est une strictement meilleure solution
best_size = len(liste) + 1 # on enregistre la taille (liste + la pièce)
best_liste = [p] + liste # on enregistre la liste (+ la pièce)
# Si aucune pièce n'a fonctionné, m est impossible à rendre
if best_size is None:
dp[m] = False, []
else:
dp[m] = True, best_liste
return dp[sar]
rendu_topdown(11, [10, 2, 5])| trouvé | liste optimale | |
|---|---|---|
| 0 | Oui | [] |
| 1 | Non | — |
| 2 | Oui | [2] |
| 3 | Non | — |
| 4 | Oui | [2, 2] |
| 5 | Oui | [5] |
| 6 | Oui | [2, 2, 2] |
| 7 | Oui | [2, 5] |
| 8 | Oui | [2, 2, 2, 2] |
| 9 | Oui | [2, 2, 5] |
| 10 | Oui | [10] |
| 11 | Oui | [2, 2, 2, 5] |
Les cases à Non correspondent à des sommes impossibles à rendre avec les pièces disponibles (ici : 1 et 3).
Si dp[s] vaut (False, []) à la fin, c’est
qu’il n’existe aucune façon de rendre la somme
avec les pièces disponibles.
trouve, lst = rendu_bas(3, [2, 5, 10])
if not trouve:
print("Impossible de rendre cette somme.")
else:
print(f"{len(lst)} pièce(s) : {lst}")dp| Approche | Direction | Récursion | Temps | Mémoire |
|---|---|---|---|---|
| Naïve | Top-down | Oui | (pile) | |
| Mémoïsation | Top-down | Oui | (pile + dict) | |
| Tabulaire | Bottom-up | Non | (tableau) | |
| Tabulaire opt. | Bottom-up | Non |
| Approche | Direction | Récursion | Temps | Mémoire |
|---|---|---|---|---|
| Glouton | — | Non | ||
| Dynamique naïf | Top-down | Oui | exponentiel | (pile) |
| Mémoïsation | Top-down | Oui | (pile + dict) | |
| Tabulaire | Bottom-up | Non |