ajout chapitre programmation dynamique, enfin
This commit is contained in:
177
Programmation_Dynamique/Corrige_Exercices.md
Normal file
177
Programmation_Dynamique/Corrige_Exercices.md
Normal 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>.
|
||||
194
Programmation_Dynamique/Corrige_TP_Donjon.md
Normal file
194
Programmation_Dynamique/Corrige_TP_Donjon.md
Normal 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>.
|
||||
87
Programmation_Dynamique/EXERCICES.md
Normal file
87
Programmation_Dynamique/EXERCICES.md
Normal 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>.
|
||||
156
Programmation_Dynamique/README.md
Normal file
156
Programmation_Dynamique/README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Programmation Dynamique
|
||||
|
||||
|
||||
|
||||
### Le programme
|
||||
|
||||

|
||||
|
||||
> 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>.
|
||||
162
Programmation_Dynamique/RENDU_MONNAIE.md
Normal file
162
Programmation_Dynamique/RENDU_MONNAIE.md
Normal 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` où `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>.
|
||||
200
Programmation_Dynamique/SAC_A_DOS.md
Normal file
200
Programmation_Dynamique/SAC_A_DOS.md
Normal 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>.
|
||||
128
Programmation_Dynamique/TP_Donjon.md
Normal file
128
Programmation_Dynamique/TP_Donjon.md
Normal 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>.
|
||||
0
Programmation_Dynamique/assets/.gitkeep
Normal file
0
Programmation_Dynamique/assets/.gitkeep
Normal file
BIN
Programmation_Dynamique/assets/bo.png
Normal file
BIN
Programmation_Dynamique/assets/bo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Reference in New Issue
Block a user