ajout chapitre programmation dynamique, enfin

This commit is contained in:
2026-03-30 20:55:32 +02:00
parent c6ee9b49f4
commit 944845b5bf
9 changed files with 1104 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
# Corrigé — Exercices Programmation Dynamique
## Exercice 1 — L'escalier
**1.** Pour `n = 5` : escalier(5) = escalier(4) + escalier(3) = 5 + 3 = **8 façons**
**2.** La suite `escalier(n)` suit la **suite de Fibonacci** (décalée d'un rang) : 1, 2, 3, 5, 8, 13...
**3.** Relation de récurrence :
```
escalier(1) = 1
escalier(2) = 2
escalier(n) = escalier(n-1) + escalier(n-2) pour n > 2
```
**4.** Version mémoïsation :
```python
def escalier_memo(n):
memo = {1: 1, 2: 2}
def escalier(k):
if k in memo:
return memo[k]
memo[k] = escalier(k - 1) + escalier(k - 2)
return memo[k]
return escalier(n)
```
**5.** Version bottom-up :
```python
def escalier_bottom_up(n):
if n == 1:
return 1
tableau = [0] * (n + 1)
tableau[1] = 1
tableau[2] = 2
for i in range(3, n + 1):
tableau[i] = tableau[i - 1] + tableau[i - 2]
return tableau[n]
```
**6.** Vérification :
```python
for i in range(1, 11):
assert escalier_memo(i) == escalier_bottom_up(i), f"Erreur pour n={i}"
print("Toutes les valeurs correspondent !")
# Résultats : 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
```
## Exercice 2 — Rendu de monnaie revisité
**1.** Avec les pièces [2, 5, 10] et somme = 6 : le glouton prend 5 (la plus grande pièce ≤ 6), il reste 1 à rendre, mais aucune pièce de [2, 5, 10] ne permet de rendre 1. **Le glouton échoue.**
**2.** Oui. La programmation dynamique explore toutes les combinaisons et trouve : 2+2+2 = **3 pièces**. La somme 6 est bien rendable, le glouton était simplement mal parti en choisissant 5 en premier.
**3.** `rendu_bottom_up` :
```python
def rendu_bottom_up(pieces, somme):
tableau = [float('inf')] * (somme + 1)
tableau[0] = 0
for s in range(1, somme + 1):
for piece in pieces:
if piece <= s and tableau[s - piece] + 1 < tableau[s]:
tableau[s] = tableau[s - piece] + 1
return tableau[somme]
# Tests
print(rendu_bottom_up([2, 5, 10], 6)) # 3 (2+2+2)
print(rendu_bottom_up([1, 3, 4], 6)) # 2 (3+3)
print(rendu_bottom_up([1, 5, 6, 9], 11)) # 2 (5+6)
```
**4.** Version avec reconstruction :
```python
def rendu_avec_pieces(pieces, somme):
tableau = [float('inf')] * (somme + 1)
tableau[0] = 0
derniere_piece = [-1] * (somme + 1) # mémorise quelle pièce a été utilisée
for s in range(1, somme + 1):
for piece in pieces:
if piece <= s and tableau[s - piece] + 1 < tableau[s]:
tableau[s] = tableau[s - piece] + 1
derniere_piece[s] = piece
# Reconstruction
pieces_utilisees = []
s = somme
while s > 0:
p = derniere_piece[s]
pieces_utilisees.append(p)
s -= p
return tableau[somme], pieces_utilisees
print(rendu_avec_pieces([1, 3, 4], 6))
# (2, [3, 3])
```
## Exercice 3 — Sac à dos : à la main et en code
**1.** Tableau `tableau[i][w]` :
objets = [(1,2), (2,5), (3,8), (4,9)], capacité = 6
| | w=0 | w=1 | w=2 | w=3 | w=4 | w=5 | w=6 |
|---|-----|-----|-----|-----|-----|-----|-----|
| i=0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| i=1 (A:1kg,2) | 0 | 2 | 2 | 2 | 2 | 2 | 2 |
| i=2 (+B:2kg,5) | 0 | 2 | 5 | 7 | 7 | 7 | 7 |
| i=3 (+C:3kg,8) | 0 | 2 | 5 | 8 | 10 | 13 | 15 |
| i=4 (+D:4kg,9) | 0 | 2 | 5 | 8 | 10 | 13 | 15 |
**2.** Valeur maximale : **15**
**3.** Reconstruction depuis `tableau[4][6] = 15` :
- `tableau[4][6] = tableau[3][6]` → D non pris, w reste 6
- `tableau[3][6] = 15 ≠ tableau[2][6] = 7` → C pris (3kg), w = 6-3 = 3
- `tableau[2][3] = 7 ≠ tableau[1][3] = 2` → B pris (2kg), w = 3-2 = 1
- `tableau[1][1] = 2 ≠ tableau[0][1] = 0` → A pris (1kg), w = 0
Solution : **A + B + C** = 2+5+8 = 15 ✓
**4.** Vérification :
```python
objets = [(1, 2), (2, 5), (3, 8), (4, 9)]
valeur, choix = sac_reconstruction(objets, 6)
print(valeur, choix) # 15, [(3, 8), (2, 5), (1, 2)]
```
## Exercice 4 — Triangle de Pascal
**1.** Relation de récurrence :
```
pascal(0, 0) = 1
pascal(n, 0) = 1 ← bord gauche
pascal(n, n) = 1 ← bord droit
pascal(n, k) = pascal(n-1, k-1) + pascal(n-1, k) pour 0 < k < n
```
**2.** Implémentation bottom-up :
```python
def triangle_pascal(n):
triangle = []
for ligne in range(n):
t = [1] * (ligne + 1) # initialise avec des 1
for k in range(1, ligne): # cases intérieures
t[k] = triangle[ligne-1][k-1] + triangle[ligne-1][k]
triangle.append(t)
return triangle
```
**3.** Vérification :
```python
resultat = triangle_pascal(5)
assert resultat == [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
print("Correct !")
```
**4.** Bonus : La somme des éléments de la ligne `n` est `2ⁿ`. Par exemple, ligne 4 : 1+4+6+4+1 = 16 = 2⁴. Lien avec Fibonacci : la somme des diagonales "montantes" du triangle de Pascal donne les nombres de Fibonacci.
---
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>.

View File

@@ -0,0 +1,194 @@
# Corrigé — TP Le Donjon du Dragon
## Partie 1 — Comprendre le problème
**Question 1.** Chemins de `(0,0)` à `(2,3)` (grille 3×4) en allant uniquement droite/bas :
Il faut faire exactement 3 pas vers la droite et 2 pas vers le bas, soit C(5,2) = **10 chemins**.
**Question 2.** Le chemin optimal est `(0,0)→(1,0)→(1,1)→(2,1)→(2,2)→(2,3)` : 3+5+9+2+8-5 = **22** d'or.
| Chemin | Or total |
|--------|----------|
| Tout droite puis tout bas | 3-1+4+1+6-5 = **8** |
| Bas, droite×3, bas | 3+5+9-2+6-5 = **16** |
| Bas×2, droite×3 | 3+5-3+2+8-5 = **10** |
| Bas, droite, bas, droite×2 | 3+5+9+2+8-5 = **22** ← optimal |
**Question 3.** Pour une grille `n×m`, le nombre de chemins est C(n+m-2, n-1) (combinaison : choisir quand descendre parmi tous les pas).
## Partie 2 — Formulation récursive
**Question 4.** Cas de base :
- `donjon(i, 0)` : on ne peut aller que vers le bas → somme de la colonne 0 jusqu'à `i`
- `donjon(0, j)` : on ne peut aller que vers la droite → somme de la ligne 0 jusqu'à `j`
**Question 5.** Relation générale :
```
donjon(i, j) = grille[i][j] + max(donjon(i-1, j), donjon(i, j-1))
```
*(On arrive en `(i,j)` depuis le haut ou depuis la gauche, on choisit le meilleur.)*
**Question 6.** Version naïve :
```python
def donjon_naif(grille, i, j):
if i == 0 and j == 0:
return grille[0][0]
if i == 0:
return grille[0][j] + donjon_naif(grille, 0, j - 1)
if j == 0:
return grille[i][0] + donjon_naif(grille, i - 1, 0)
return grille[i][j] + max(donjon_naif(grille, i - 1, j),
donjon_naif(grille, i, j - 1))
grille = [
[ 3, -1, 4, 1],
[ 5, 9, -2, 6],
[-3, 2, 8, -5]
]
n, m = len(grille), len(grille[0])
print(donjon_naif(grille, n - 1, m - 1)) # 22
```
## Partie 3 — Version Top-Down (mémoïsation)
**Question 7.** Oui, beaucoup de sous-problèmes sont recalculés. Par exemple, `donjon(1,1)` est appelé depuis `donjon(2,1)` et depuis `donjon(1,2)`.
```python
compteur = 0
def donjon_naif_compte(grille, i, j):
global compteur
compteur += 1
if i == 0 and j == 0:
return grille[0][0]
if i == 0:
return grille[0][j] + donjon_naif_compte(grille, 0, j - 1)
if j == 0:
return grille[i][0] + donjon_naif_compte(grille, i - 1, 0)
return grille[i][j] + max(donjon_naif_compte(grille, i - 1, j),
donjon_naif_compte(grille, i, j - 1))
donjon_naif_compte(grille, 2, 3)
print(f"Nombre d'appels : {compteur}") # bien plus que les 12 cases de la grille
```
**Question 8.** Version mémoïsation :
```python
def donjon_memo(grille):
n = len(grille)
m = len(grille[0])
memo = {}
def donjon(i, j):
if (i, j) in memo:
return memo[(i, j)]
if i == 0 and j == 0:
resultat = grille[0][0]
elif i == 0:
resultat = grille[0][j] + donjon(0, j - 1)
elif j == 0:
resultat = grille[i][0] + donjon(i - 1, 0)
else:
resultat = grille[i][j] + max(donjon(i - 1, j), donjon(i, j - 1))
memo[(i, j)] = resultat
return resultat
return donjon(n - 1, m - 1)
print(donjon_memo(grille)) # 22
```
**Question 9.** Les deux versions donnent 22 ✓
## Partie 4 — Version Bottom-Up (tableau)
**Question 10.** Tableau `t[i][j]` = or max depuis `(0,0)` jusqu'à `(i,j)` :
```
grille : tableau :
[ 3, -1, 4, 1] [ 3, 2, 6, 7]
[ 5, 9, -2, 6] [ 8, 17, 15, 21]
[-3, 2, 8, -5] [ 5, 19, 27, 22]
```
**Question 11.** Implémentation :
```python
def donjon_bottom_up(grille):
n = len(grille)
m = len(grille[0])
tableau = [[0] * m for _ in range(n)]
tableau[0][0] = grille[0][0]
for j in range(1, m): # première ligne
tableau[0][j] = tableau[0][j - 1] + grille[0][j]
for i in range(1, n): # première colonne
tableau[i][0] = tableau[i - 1][0] + grille[i][0]
for i in range(1, n): # reste du tableau
for j in range(1, m):
tableau[i][j] = grille[i][j] + max(tableau[i - 1][j],
tableau[i][j - 1])
return tableau[n - 1][m - 1]
print(donjon_bottom_up(grille)) # 22
```
**Question 12.** Les trois versions donnent 22 ✓
## Partie 5 — Bonus : retrouver le chemin optimal
**Question 13.** Reconstruction du chemin :
```python
def donjon_chemin(grille):
n = len(grille)
m = len(grille[0])
tableau = [[0] * m for _ in range(n)]
tableau[0][0] = grille[0][0]
for j in range(1, m):
tableau[0][j] = tableau[0][j - 1] + grille[0][j]
for i in range(1, n):
tableau[i][0] = tableau[i - 1][0] + grille[i][0]
for i in range(1, n):
for j in range(1, m):
tableau[i][j] = grille[i][j] + max(tableau[i - 1][j],
tableau[i][j - 1])
# Reconstruction : on remonte depuis (n-1, m-1)
chemin = []
i, j = n - 1, m - 1
while i > 0 or j > 0:
chemin.append((i, j))
if i == 0:
j -= 1
elif j == 0:
i -= 1
elif tableau[i - 1][j] > tableau[i][j - 1]:
i -= 1
else:
j -= 1
chemin.append((0, 0))
chemin.reverse()
return tableau[n - 1][m - 1], chemin
valeur, chemin = donjon_chemin(grille)
print(f"Or maximal : {valeur}") # 22
print(f"Chemin optimal : {chemin}") # [(0,0), (1,0), (1,1), (2,1), (2,2), (2,3)]
```
---
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>.

View File

@@ -0,0 +1,87 @@
# Exercices — Programmation Dynamique
## Exercice 1 — L'escalier
Vous souhaitez monter un escalier de `n` marches. À chaque étape, vous pouvez monter **1 marche** ou **2 marches**. Combien existe-t-il de façons différentes d'atteindre la n-ième marche ?
**Exemples :**
- `n = 1` : 1 façon (1)
- `n = 2` : 2 façons (1+1, 2)
- `n = 3` : 3 façons (1+1+1, 1+2, 2+1)
- `n = 4` : 5 façons
**1.** Calculez à la main le nombre de façons pour `n = 5`.
**2.** Quelle ressemblance observez-vous avec une suite mathématique vue dans le cours Récursivité ?
**3.** Écrivez la relation de récurrence pour `escalier(n)`.
**4.** Implémentez `escalier_memo(n)` en utilisant la mémoïsation.
**5.** Implémentez `escalier_bottom_up(n)` en utilisant un tableau.
**6.** Vérifiez que vos deux fonctions donnent les mêmes résultats pour `n` allant de 1 à 10.
## Exercice 2 — Rendu de monnaie revisité
On dispose de pièces de valeurs **[2, 5, 10]** (en centimes).
**1.** L'algorithme glouton peut-il rendre 6 centimes avec ce système ? Justifiez.
**2.** La programmation dynamique peut-elle rendre 6 centimes ? Justifiez.
**3.** Écrivez la fonction `rendu_bottom_up(pieces, somme)` (version bottom-up vue en cours) et testez-la avec :
- `pieces = [2, 5, 10]`, `somme = 6`
- `pieces = [1, 3, 4]`, `somme = 6`
- `pieces = [1, 5, 6, 9]`, `somme = 11`
**4.** Modifiez la fonction pour qu'elle retourne non seulement le nombre de pièces, mais aussi la **liste des pièces utilisées** (comme la reconstruction dans le sac à dos).
## Exercice 3 — Sac à dos : à la main et en code
On dispose des objets suivants, avec un sac de capacité **6 kg** :
| Objet | Poids | Valeur |
|-------|-------|--------|
| A | 1 kg | 2 |
| B | 2 kg | 5 |
| C | 3 kg | 8 |
| D | 4 kg | 9 |
**1.** Remplissez à la main le tableau `tableau[i][w]` pour `i` de 0 à 4 et `w` de 0 à 6.
**2.** Quelle est la valeur maximale atteignable ?
**3.** En remontant le tableau, déterminez quels objets sont dans la solution optimale.
**4.** Vérifiez vos réponses en exécutant `sac_reconstruction(objets, 6)`.
## Exercice 4 — Triangle de Pascal ★
Le **triangle de Pascal** est construit selon la règle suivante :
- Les bords valent toujours 1
- Chaque case intérieure est la somme des deux cases au-dessus d'elle
```
Ligne 0 : 1
Ligne 1 : 1 1
Ligne 2 : 1 2 1
Ligne 3 : 1 3 3 1
Ligne 4 : 1 4 6 4 1
```
**1.** Écrivez la relation de récurrence : `pascal(ligne, col) = ?`
**2.** Implémentez `triangle_pascal(n)` par programmation dynamique bottom-up, qui retourne les `n` premières lignes du triangle sous forme de liste de listes.
**3.** Vérifiez que `triangle_pascal(5)` donne `[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]`.
**4.** *(Bonus)* La somme des éléments de la ligne `n` est-elle liée à une valeur vue dans ce chapitre ?
---
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>.

View File

@@ -0,0 +1,156 @@
# 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 d'optimisation 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>.

View File

@@ -0,0 +1,162 @@
# Rendu de Monnaie
### Le problème
> **Énoncé** : On dispose d'un ensemble de pièces de valeurs données (par exemple : 1€, 3€, 5€). On souhaite rendre une somme exacte en utilisant le **moins de pièces possible**.
**Exemple :** Rendre 11€ avec des pièces de 1€, 3€ et 5€.
Une solution possible : 5 + 3 + 3 = 11€ → **3 pièces**
---
### Pourquoi l'algorithme glouton ne suffit pas
En classe de Première, vous avez étudié l'**algorithme glouton** : à chaque étape, on choisit la plus grande pièce possible.
```python
def rendu_glouton(pieces, somme):
pieces_triees = sorted(pieces, reverse=True)
nb_pieces = 0
for piece in pieces_triees:
while somme >= piece:
somme -= piece
nb_pieces += 1
return nb_pieces
```
Le glouton fonctionne bien avec les pièces européennes (1, 2, 5, 10, 20, 50 centimes...). Mais il échoue sur d'autres systèmes :
**Contre-exemple :** pièces = [1, 3, 4], somme = 6
- Glouton : 4 + 1 + 1 = **3 pièces**
- Optimal : 3 + 3 = **2 pièces**
Le glouton n'est **pas optimal** dans le cas général. La programmation dynamique, elle, trouve toujours la solution optimale.
---
### Formulation récursive
Notons `rendu(s)` le nombre minimum de pièces pour rendre la somme `s`.
- **Cas de base :** `rendu(0) = 0` (aucune pièce nécessaire pour rendre 0)
- **Cas récursif :** pour chaque pièce `p` disponible telle que `p ≤ s` :
```
rendu(s) = 1 + min( rendu(s - p) ) pour toute pièce p ≤ s
```
On essaie toutes les pièces et on garde le minimum.
---
### Version naïve (récursive)
```python
def rendu_naif(pieces, somme):
if somme == 0:
return 0
minimum = float('inf') # infini : pas encore de solution trouvée
for piece in pieces:
if piece <= somme:
nb = 1 + rendu_naif(pieces, somme - piece)
if nb < minimum:
minimum = nb
return minimum
```
**Problème :** Comme pour Fibonacci, de nombreux sous-problèmes sont recalculés plusieurs fois. La complexité est exponentielle.
---
### Version Top-Down (mémoïsation)
```python
def rendu_memo(pieces, somme):
memo = {}
def rendu(s):
if s == 0:
return 0
if s in memo:
return memo[s]
minimum = float('inf')
for piece in pieces:
if piece <= s:
nb = 1 + rendu(s - piece)
if nb < minimum:
minimum = nb
memo[s] = minimum
return memo[s]
return rendu(somme)
```
Chaque valeur de `rendu(s)` n'est calculée qu'une seule fois et mémorisée dans le dictionnaire.
---
### Version Bottom-Up (tableau)
On remplit un tableau `t``t[s]` représente le nombre minimum de pièces pour rendre la somme `s`, en partant de `s = 0` jusqu'à `s = somme`.
```python
def rendu_bottom_up(pieces, somme):
tableau = [float('inf')] * (somme + 1)
tableau[0] = 0 # cas de base : 0 pièce pour rendre 0€
for s in range(1, somme + 1):
for piece in pieces:
if piece <= s:
if tableau[s - piece] + 1 < tableau[s]:
tableau[s] = tableau[s - piece] + 1
return tableau[somme]
```
---
### Visualisation du tableau
**Exemple :** pièces = [1, 3, 4], somme = 6
On initialise : `tableau = [0, ∞, ∞, ∞, ∞, ∞, ∞]`
| s | Pièce testée | Calcul | tableau[s] |
|---|-------------|--------|------------|
| 1 | 1 | `tableau[0] + 1 = 1` | **1** |
| 2 | 1 | `tableau[1] + 1 = 2` | **2** |
| 3 | 1 | `tableau[2] + 1 = 3` | 3 |
| 3 | 3 | `tableau[0] + 1 = 1` | **1** |
| 4 | 1 | `tableau[3] + 1 = 2` | 2 |
| 4 | 3 | `tableau[1] + 1 = 2` | 2 |
| 4 | 4 | `tableau[0] + 1 = 1` | **1** |
| 5 | 1 | `tableau[4] + 1 = 2` | 2 |
| 5 | 3 | `tableau[2] + 1 = 3` | 2 |
| 5 | 4 | `tableau[1] + 1 = 2` | **2** |
| 6 | 1 | `tableau[5] + 1 = 3` | 3 |
| 6 | 3 | `tableau[3] + 1 = 2` | **2** |
| 6 | 4 | `tableau[2] + 1 = 3` | 2 |
Résultat final : `tableau[6] = 2` → 2 pièces (3 + 3). L'algorithme a bien trouvé la solution optimale que le glouton avait ratée.
---
### Complexité
| Version | Temps | Espace |
|---------|-------|--------|
| Naïve | exponentielle | O(somme) pile |
| Top-Down | O(n × somme) | O(somme) |
| Bottom-Up | O(n × somme) | O(somme) |
Avec n = nombre de pièces différentes.
---
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>.

View File

@@ -0,0 +1,200 @@
# Le Sac à Dos
### Le problème
> **Énoncé** : Vous partez en randonnée avec un sac à dos d'une capacité maximale de **W kilogrammes**. Vous avez devant vous **n objets**, chacun ayant un **poids** et une **valeur**. Vous souhaitez maximiser la valeur totale des objets emportés, sans dépasser la capacité du sac.
>
> Contrainte : chaque objet est **pris entièrement ou laissé** (pas de fraction possible). C'est le problème du **sac à dos 0/1**.
**Exemple :**
| Objet | Poids | Valeur |
|-------|-------|--------|
| Tente | 4 kg | 10 |
| Nourriture | 3 kg | 7 |
| Trousse médicale | 2 kg | 6 |
| Lampe | 1 kg | 3 |
Capacité du sac : **5 kg**
---
### Pourquoi le glouton échoue (encore)
L'idée "prendre en priorité les objets avec le meilleur ratio valeur/poids" ne donne pas toujours la solution optimale.
| Objet | Poids | Valeur | Ratio valeur/poids |
|-------|-------|--------|--------------------|
| Tente | 4 | 10 | 2,5 |
| Nourriture | 3 | 7 | 2,33 |
| Trousse | 2 | 6 | 3,0 ← meilleur |
| Lampe | 1 | 3 | 3,0 ← meilleur |
Glouton (capacité 5) : Trousse (2kg, val 6) + Lampe (1kg, val 3) + ... reste 2kg, Nourriture trop lourde → **valeur = 9**
Optimal : Nourriture (3kg, val 7) + Trousse (2kg, val 6) = 5kg → **valeur = 13**
---
### Formulation récursive
Notons `sac(i, w)` la valeur maximale qu'on peut atteindre en choisissant parmi les `i` premiers objets avec une capacité restante de `w`.
- **Cas de base :** `sac(0, w) = 0` (aucun objet disponible) et `sac(i, 0) = 0` (sac plein)
- **Cas récursif :** Pour l'objet `i` de poids `p_i` et de valeur `v_i` :
- Si `p_i > w` : on ne peut pas le prendre → `sac(i, w) = sac(i-1, w)`
- Sinon, on choisit le meilleur entre le prendre et ne pas le prendre :
```
sac(i, w) = max(
sac(i-1, w), ← on ne prend pas l'objet i
v_i + sac(i-1, w - p_i) ← on prend l'objet i
)
```
---
### Version Top-Down (mémoïsation)
```python
def sac_memo(objets, capacite):
"""
objets : liste de tuples (poids, valeur)
capacite : capacité maximale du sac (entier)
"""
n = len(objets)
memo = {}
def sac(i, w):
if i == 0 or w == 0:
return 0
if (i, w) in memo:
return memo[(i, w)]
poids, valeur = objets[i - 1]
if poids > w:
resultat = sac(i - 1, w)
else:
sans_objet = sac(i - 1, w)
avec_objet = valeur + sac(i - 1, w - poids)
resultat = max(sans_objet, avec_objet)
memo[(i, w)] = resultat
return resultat
return sac(n, capacite)
```
---
### Version Bottom-Up (tableau 2D)
On construit un tableau `tableau[i][w]` représentant la valeur maximale avec les `i` premiers objets et une capacité `w`.
```python
def sac_bottom_up(objets, capacite):
"""
objets : liste de tuples (poids, valeur)
capacite : capacité maximale du sac (entier)
"""
n = len(objets)
# tableau[i][w] = valeur max avec les i premiers objets, capacité w
tableau = [[0] * (capacite + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
poids, valeur = objets[i - 1]
for w in range(capacite + 1):
if poids > w:
tableau[i][w] = tableau[i - 1][w]
else:
tableau[i][w] = max(
tableau[i - 1][w],
valeur + tableau[i - 1][w - poids]
)
return tableau[n][capacite]
```
---
### Visualisation du tableau
**Exemple :** objets = [(4,10), (3,7), (2,6), (1,3)], capacité = 5
`tableau[i][w]` = valeur maximale avec les `i` premiers objets et capacité `w`
| | w=0 | w=1 | w=2 | w=3 | w=4 | w=5 |
|---|-----|-----|-----|-----|-----|-----|
| i=0 (aucun objet) | 0 | 0 | 0 | 0 | 0 | 0 |
| i=1 (Tente 4kg,10) | 0 | 0 | 0 | 0 | 10 | 10 |
| i=2 (+Nourriture 3kg,7) | 0 | 0 | 0 | 7 | 10 | 10 |
| i=3 (+Trousse 2kg,6) | 0 | 0 | 6 | 7 | 10 | 13 |
| i=4 (+Lampe 1kg,3) | 0 | 3 | 6 | 9 | 10 | **13** |
La valeur optimale est `tableau[4][5] = 13`.
---
### Reconstruction de la solution
Le tableau nous donne la valeur optimale, mais pas quels objets choisir. Pour le savoir, on **remonte le tableau** depuis `tableau[n][capacite]` :
```python
def sac_reconstruction(objets, capacite):
"""
Retourne (valeur_max, liste_objets_choisis)
"""
n = len(objets)
tableau = [[0] * (capacite + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
poids, valeur = objets[i - 1]
for w in range(capacite + 1):
if poids > w:
tableau[i][w] = tableau[i - 1][w]
else:
tableau[i][w] = max(
tableau[i - 1][w],
valeur + tableau[i - 1][w - poids]
)
# Reconstruction : on remonte le tableau
objets_choisis = []
w = capacite
for i in range(n, 0, -1):
# Si la valeur a changé entre i et i-1, c'est qu'on a pris l'objet i
if tableau[i][w] != tableau[i - 1][w]:
objets_choisis.append(objets[i - 1])
w -= objets[i - 1][0] # on réduit la capacité restante
return tableau[n][capacite], objets_choisis
# Exemple d'utilisation
objets = [(4, 10), (3, 7), (2, 6), (1, 3)]
valeur, choix = sac_reconstruction(objets, 5)
print(f"Valeur maximale : {valeur}")
print(f"Objets choisis : {choix}")
# Valeur maximale : 13
# Objets choisis : [(2, 6), (3, 7)] → Trousse + Nourriture
```
---
### Complexité
| Version | Temps | Espace |
|---------|-------|--------|
| Naïve | O(2ⁿ) | O(n) pile |
| Top-Down | O(n × W) | O(n × W) |
| Bottom-Up | O(n × W) | O(n × W) |
Avec n = nombre d'objets, W = capacité du sac.
> **Attention :** si W est très grand, cette complexité peut devenir prohibitive. Le sac à dos 0/1 est un problème NP-difficile — la programmation dynamique le résout efficacement pour des valeurs raisonnables de W.
---
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>.

View File

@@ -0,0 +1,128 @@
# TP — Le Donjon du Dragon
## Contexte
Vous jouez à un RPG en vue du dessus. Vous venez d'entrer dans un donjon représenté par une **grille de n lignes et m colonnes**.
Chaque case de la grille contient une quantité d'or :
- Une valeur **positive** : de l'or à ramasser 🪙
- Une valeur **négative** : un piège qui vous coûte de l'or 💀
**Règles du jeu :**
- Vous commencez en haut à gauche : case `(0, 0)`
- Vous devez atteindre la sortie en bas à droite : case `(n-1, m-1)`
- Vous ne pouvez vous déplacer **que vers la droite ou vers le bas**
- Vous souhaitez **maximiser l'or total ramassé**
**Exemple de grille 3×4 :**
```
[ 3, -1, 4, 1 ]
[ 5, 9, -2, 6 ]
[ -3, 2, 8, -5 ]
```
Un chemin possible : (0,0)→(1,0)→(1,1)→(1,2)→(2,2)→(2,3) → or = 3+5+9-2+8-5 = **18**
Est-ce le meilleur chemin ? C'est ce que vous allez découvrir.
## Partie 1 — Comprendre le problème
**Question 1.** Sur la grille ci-dessus, listez tous les chemins possibles de `(0,0)` à `(2,3)` en ne se déplaçant que vers la droite ou vers le bas. Combien y en a-t-il ?
**Question 2.** Pour chaque chemin, calculez l'or total ramassé. Quel est le chemin optimal ?
**Question 3.** Pour une grille de dimensions `n × m`, combien y a-t-il de chemins possibles en tout ? *(Indice : c'est une formule combinatoire.)*
## Partie 2 — Formulation récursive
Notons `donjon(i, j)` l'or maximum qu'on peut ramasser en partant de `(i, j)` pour atteindre `(n-1, m-1)`.
**Question 4.** Quels sont les cas de base de cette fonction récursive ? *(Pensez aux bords de la grille.)*
**Question 5.** Écrivez la relation de récurrence générale pour `donjon(i, j)` quand `i > 0` et `j > 0`.
**Question 6.** Implémentez la version naïve `donjon_naif(grille, i, j)` :
```python
def donjon_naif(grille, i, j):
# À compléter
pass
# Test
grille = [
[ 3, -1, 4, 1],
[ 5, 9, -2, 6],
[-3, 2, 8, -5]
]
n, m = len(grille), len(grille[0])
print(donjon_naif(grille, n-1, m-1)) # doit afficher 22
```
## Partie 3 — Version Top-Down (mémoïsation)
**Question 7.** La version naïve recalcule-t-elle des sous-problèmes plusieurs fois ? Pour répondre, ajoutez un compteur d'appels et testez sur la grille ci-dessus.
**Question 8.** Implémentez `donjon_memo(grille)` avec mémoïsation :
```python
def donjon_memo(grille):
n = len(grille)
m = len(grille[0])
memo = {}
def donjon(i, j):
# À compléter
pass
return donjon(n - 1, m - 1)
```
**Question 9.** Vérifiez que vous obtenez le même résultat que la version naïve.
## Partie 4 — Version Bottom-Up (tableau)
**Question 10.** Remplissez à la main le tableau `t[i][j]` représentant l'or maximum qu'on peut ramasser **depuis `(0,0)` jusqu'à `(i,j)`** pour la grille de l'exemple.
*(Attention : ici on part de `(0,0)` et on avance, pas de `(i,j)` vers la sortie.)*
**Question 11.** Implémentez `donjon_bottom_up(grille)` :
```python
def donjon_bottom_up(grille):
n = len(grille)
m = len(grille[0])
tableau = [[0] * m for _ in range(n)]
# Initialiser tableau[0][0]
# Initialiser la première ligne (on ne peut aller que vers la droite)
# Initialiser la première colonne (on ne peut aller que vers le bas)
# Remplir le reste du tableau
# À compléter
return tableau[n - 1][m - 1]
```
**Question 12.** Vérifiez que vous obtenez le même résultat que les versions précédentes.
## Partie 5 — Bonus : retrouver le chemin optimal
**Question 13.** *(Bonus)* Modifiez `donjon_bottom_up` pour qu'elle retourne non seulement la valeur optimale, mais aussi **la liste des cases du chemin optimal**.
*(Indice : une fois le tableau rempli, remontez depuis `(n-1, m-1)` vers `(0,0)` en choisissant à chaque étape la case voisine (gauche ou haut) qui a la plus grande valeur.)*
```python
def donjon_chemin(grille):
# À compléter
# Retourne (valeur_max, chemin)
# chemin = liste de tuples (i, j) de (0,0) à (n-1, m-1)
pass
```
---
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>.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB