Files
TermNSI/Programmation_Dynamique/README.md

156 lines
5.8 KiB
Markdown

# Programmation Dynamique
### Le programme
![bo.png](assets/bo.png)
> La programmation dynamique est une méthode algorithmique permettant de résoudre efficacement des problèmes complexes en mémorisant les résultats des sous-problèmes déjà résolus.
---
### Motivation : un problème de performance
Dans le chapitre Récursivité, vous avez écrit la fonction `fibonacci` suivante :
```python
def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)
```
Et vous avez observé que l'arbre des appels contient de nombreux **doublons** :
```
fibonacci(5)
/ \
fibonacci(4) fibonacci(3) ← déjà calculé !
/ \ / \
fibonacci(3) fibonacci(2) fibonacci(2) fibonacci(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)
```
`fibonacci(3)` est calculé **2 fois**, `fibonacci(2)` est calculé **3 fois**, `fibonacci(1)` est calculé **5 fois**.
La complexité de cet algorithme est **exponentielle** : O(2ⁿ). Pour `fibonacci(50)`, cela représente plus de 1 000 milliards d'appels !
**L'idée clé** : si on avait mémorisé le résultat de `fibonacci(3)` lors du premier calcul, on n'aurait pas eu besoin de le recalculer.
---
### L'idée clé : la programmation dynamique
> **Définition** : La **programmation dynamique** est une méthode algorithmique qui consiste à :
>
> 1. Décomposer un problème en **sous-problèmes** plus simples
> 2. **Mémoriser** le résultat de chaque sous-problème pour éviter de le recalculer
> 3. Combiner les résultats pour résoudre le problème initial
Il existe deux façons de mettre en œuvre cette idée :
| Approche | Principe | Autre nom |
|----------|----------|-----------|
| **Top-down** | On part du problème initial et on descend vers les sous-problèmes, en mémorisant au passage | Mémoïsation |
| **Bottom-up** | On commence par les sous-problèmes les plus simples et on remonte vers le problème initial | Tabulation |
---
### Approche Top-Down : la mémoïsation
On garde la structure récursive, mais on ajoute un **dictionnaire** pour mémoriser les résultats déjà calculés. Avant chaque calcul, on vérifie si le résultat n'est pas déjà connu.
```python
def fibonacci_memo(n):
memo = {}
memo[0] = 0
memo[1] = 1
def fib(k):
if k in memo: # si déjà calculé, on retourne directement
return memo[k]
memo[k] = fib(k-1) + fib(k-2) # sinon, on calcule et on mémorise
return memo[k]
return fib(n)
```
Avec `fibonacci_memo(5)`, chaque valeur n'est calculée qu'**une seule fois** :
```
fib(5) → calcule fib(4) et fib(3)
fib(4) → calcule fib(3) et fib(2)
fib(3) → calcule fib(2) et fib(1) = 1 ✓ (mémorisé)
fib(2) → calcule fib(1) = 1 ✓ et fib(0) = 0 ✓ → mémorise fib(2) = 1
fib(3) → fib(2) = 1 ✓ (déjà en memo) + fib(1) = 1 ✓ → mémorise fib(3) = 2
fib(4) → fib(3) = 2 ✓ (déjà en memo) + fib(2) = 1 ✓ → mémorise fib(4) = 3
fib(5) → fib(4) = 3 ✓ + fib(3) = 2 ✓ → mémorise fib(5) = 5
```
La complexité passe de O(2ⁿ) à **O(n)** en temps, et O(n) en mémoire (pour stocker le dictionnaire).
---
### Approche Bottom-Up : le tableau
On abandonne la récursion. On part des cas de base et on remplit un **tableau** dans l'ordre croissant jusqu'à atteindre la valeur souhaitée.
```python
def fibonacci_bottom_up(n):
if n == 0:
return 0
tableau = [0] * (n + 1) # tableau[i] contiendra fibonacci(i)
tableau[0] = 0
tableau[1] = 1
for i in range(2, n + 1):
tableau[i] = tableau[i-1] + tableau[i-2]
return tableau[n]
```
Déroulement pour `fibonacci_bottom_up(6)` :
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| tableau[i] | 0 | 1 | 1 | 2 | 3 | 5 | **8** |
Chaque case est calculée exactement une fois, dans l'ordre. Complexité : **O(n)** en temps, O(n) en mémoire.
> **Remarque** : On peut même optimiser l'espace mémoire à O(1) en ne gardant que les deux dernières valeurs, mais cette optimisation sort du programme.
---
### Comparaison des trois approches
| Critère | Naïf (récursif) | Top-Down (mémoïsation) | Bottom-Up (tableau) |
|---------|-----------------|------------------------|----------------------|
| **Style** | Récursif | Récursif + dictionnaire | Itératif |
| **Complexité temps** | O(2ⁿ) | O(n) | O(n) |
| **Complexité espace** | O(n) pile d'appels | O(n) dictionnaire | O(n) tableau |
| **Lisibilité** | Très lisible | Lisible | Moins intuitif |
| **Risque** | RecursionError pour n grand | RecursionError pour n grand | Aucun |
> En pratique, le **bottom-up** est souvent préféré en compétition et en entreprise car il évite les risques liés à la récursion. Le **top-down** est plus naturel quand on part d'une formule récursive.
---
### À retenir
- La **programmation dynamique** = décomposer + mémoriser + combiner
- Elle s'applique quand un problème a des **sous-problèmes qui se répètent**
- Deux approches : **top-down** (mémoïsation, récursif) et **bottom-up** (tableau, itératif)
- Les deux ont une complexité **polynomiale** là où la version naïve est souvent **exponentielle**
---
Auteur : Florian Mathieu
Licence CC BY NC
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Licence Creative Commons" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a> <br />Ce cours est mis à disposition selon les termes de la <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International</a>.