La programmation dynamique repose sur une idée simple mais puissante : pourquoi recalculer un résultat si on l'a déjà trouvé ?
En décomposant un problème complexe en sous-problèmes plus simples, on peut mémoriser les solutions déjà obtenues et les réutiliser. Cela permet d'éviter des calculs redondants et d'améliorer l'efficacité des algorithmes.
A copier dans le cahier.
La programmation dynamique est une méthode de conception d’algorithmes utilisée pour résoudre efficacement des problèmes complexes en les décomposant en sous-problèmes qui se répètent. Elle repose sur l’idée de mémoriser les résultats des sous-problèmes déjà résolus afin d’éviter de refaire plusieurs fois les mêmes calculs.
La programmation dynamique est souvent utilisée pour optimiser des calculs répétitifs, par exemple dans la suite de Fibonacci, les problèmes de plus court chemin (algorithme de Dijkstra), ou les problèmes de sac à dos (knapsack).
On considère la suite de Fibonacci définie à l'aide d'une suite \(F\) définie ainsi :
fibo_itera(n) (version itérative).fibo_recur(n) (version récursive simple).fibo_recur_dyna(n) (récursif avec mémoïsation).fibo_itera_dyna(n) (itératif avec tableau dynamique).Exemples :
F(0)=0, F(1)=1, F(2)=1, F(5)=5, F(10)=55
Tests :
Affichage :
Console:
On dispose d’une grille rectangulaire de coûts (entiers positifs). On part de la case en haut à gauche et on veut atteindre la case en bas à droite.
À chaque étape, on peut se déplacer uniquement vers la droite ou vers le bas. Le coût d’un chemin est la somme des coûts des cases traversées (départ et arrivée inclus).
Objectif : écrire une fonction cout_chemin_min(grille, affiche_dp = False) qui renvoie le coût minimal pour aller du coin haut-gauche au coin bas-droit.
Idée (raisonnement dynamique) :
Exemples :
grille1 = [
[1, 3, 1],
[1, 5, 1],
[4, 2, 1]
]
# coût minimal attendu = 7
grille2 = [[5]] # coût = 5
Un paramètre affiche_dp qui vaut par défaut False permettra de visualiser les deux grilles (matrices) si on le souhaite.
Tests :
Affichage :
Console:
On modélise un plateau par une grille de 0 et 1 :
0 : case libre (on peut la traverser)1 : obstacle (on ne peut pas la traverser)On part du coin haut-gauche de la grille et on veut atteindre le coin bas-droit.
À chaque étape on peut se déplacer uniquement vers la droite ou vers le bas.
Objectif : écrire une fonction nombre_chemins(grille, affiche_dp=False) qui renvoie le nombre de chemins possibles de la case de départ à la case d’arrivée, en évitant les obstacles.
Idée (raisonnement dynamique) :
Exemples :
grilleA = [
[0, 0, 0],
[0, 0, 0]
]
# 2 lignes, 3 colonnes, pas d'obstacle → il n'existe qu'un seul chemin par la première ligne
# puis tout droit vers le bas, OU bien tout droit en bas puis à droite → ici le résultat est 3.
grilleB = [
[0, 1],
[0, 0]
]
# Un obstacle en (0,1) (première ligne, deuxième colonne) bloque un des chemins.
Le paramètre affiche_dp permettra, s’il vaut True, d’afficher la grille et la
matrice des nombres de chemins calculée.
Tests :
Affichage :
Console:
Vous préparez un sac de randonnée. Chaque objet possède un poids (combien il pèse) et une valeur (son utilité/intérêt). Le sac a une capacité maximale : la somme des poids des objets choisis ne doit pas dépasser cette capacité. Chaque objet peut être pris au plus une fois.
Votre sac a une capacité maximale capacite (même unité que les poids). Vous ne pouvez pas dépasser cette capacité et vous ne pouvez prendre chaque objet au plus une fois (problème du sac à dos 0/1).
Objectif : écrire une fonction valeur_max_sac(liste_poids, liste_valeurs, capacite, affiche_dp=False) qui renvoie la valeur totale maximale transportable sans dépasser la capacité.
liste_poids[i] : le poids de l’objet i (par exemple en kg ou en centaines de grammes).liste_valeurs[i] : l’utilité (ou score) de l’objet i. Plus la valeur est grande, plus l’objet est intéressant à emporter.affiche_dp=True, afficher la table pour visualiser la construction de la solution.Tests :
Affichage :
Console:
Cet exercice porte sur l’algorithmique, les listes et la programmation dynamique.
Dans un jeu de stratégie, la carte est un carré de \(n \times n\) cases, où \(n\) est un entier strictement positif. Certaines cases sont dites constructibles, d’autres ne le sont pas. Toutes les cartes contiennent au moins une case constructible.
Une telle carte est représentée en Python par une liste de listes. Les cases constructibles sont représentées par des cellules contenant la valeur 1, celles qui sont non constructibles par des cellules contenant la valeur 0.
On fournit ci-dessous la représentation d’une carte pour laquelle n est égal à 5 :
carte_A = [
[0, 0, 1, 1, 1],
[1, 1, 0, 1, 1],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
]
Une base dans la carte est un carré formé de cases constructibles. On cherche à construire la plus grande base possible, ce qui revient donc à trouver un plus grand carré, inclus dans la carte, ne contenant que des valeurs 1. Il peut en exister plusieurs.
Dans la carte précédente, la plus grande base possible est un carré de 3 cases de
côté, son coin supérieur gauche est la cellule carte_A[2][1], de coordonnées (2,1).
On dit que cette base est issue de la cellule carte_A[2][1] et que sa taille vaut 3.
On considère la carte_B donnée ci-dessous.
carte_B = [
[1, 1, 1, 1],
[0, 1, 1, 1],
[0, 1, 1, 1],
[1, 0, 1, 1],
]
Afin de répondre à ce problème, on envisage une recherche exhaustive de solution. Cela signifie que l’on teste tous les carrés inclus dans la carte initiale afin de vérifier s’ils ne contiennent que des cases constructibles. On considère dans un premier temps le carré de taille \(n\), puis les quatre carrés de taille \(n - 1\) et ainsi de suite jusqu’aux carrés de taille 1. Dès que l’on trouve un carré constructible, on renvoie sa taille et les coordonnées de son coin supérieur gauche.
taille strictement positif. Déterminer la somme des valeurs de toutes les cellules d’un carré constructible de taille taille cases de côté.
La fonction est_constructible prend en paramètres :
carte représentant la carte ;i_coin et j_coin indiquant les coordonnées de la cellule dont est issu le carré considéré ;taille indiquant la taille de ce carré.
Cette fonction renvoie True si le carré décrit par les paramètres est constructible,
False dans le cas contraire.
Cette fonction n’a pas besoin de vérifier que les coordonnées et la taille passées en paramètres définissent toujours un carré valide dont toutes les cases sont incluses dans la carte. On suppose que c’est toujours le cas.
est_constructible.def est_constructible(carte, i_coin, j_coin, taille):
s = 0
for i in range(i_coin, i_coin + taille):
for j in range(..., ...):
s = ...
... # Plusieurs lignes possibles
La fonction plus_grande_base_exhaustive prend en paramètre la liste de listes
carte représentant une carte et renvoie un triplet (taille, i, j) dans lequel
taille est la taille d’un carré constructible de taille maximale et i et j
sont les coordonnées de son coin supérieur gauche.
Cette fonction utilise la méthode exhaustive décrite plus haut.
On rappelle qu’une carte contient toujours au moins une case constructible et que cette fonction renverra donc toujours un résultat.
plus_grande_base_exhaustive.def plus_grande_base_exhaustive(carte):
n = len(carte)
# les tailles vont de n à 1, de -1 en -1
for taille in range(n, 0, -1):
i = 0
while i + taille <= n:
j = ...
while ...:
if est_constructible(...):
return (taille, i, j)
j = ...
i = i + 1
Un décompte du nombre de carrés à étudier dans le pire des cas en fonction de la taille de la carte initiale permet de construire la figure 1 ci-dessous.
On souhaite désormais résoudre ce problème en utilisant la programmation dynamique.
Pour cela, on construit une liste de listes auxiliaire aux de mêmes dimensions
que la carte et telle que aux[i][j] contienne la taille de la plus grande base
(carrée) issue de carte[i][j].
En reprenant l’exemple de carte_A, on obtient la liste de listes aux_A :
carte_A = [
[0, 0, 1, 1, 1],
[1, 1, 0, 1, 1],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 0],
]
aux_A = [
[0, 0, 1, 2, 1],
[1, 1, 0, 1, 1],
[0, 3, 2, 1, 0],
[0, 2, 2, 1, 0],
[0, 1, 1, 1, 0],
]
aux_A[2][1] contient la valeur 3 car la plus grande base issue de la cellule
carte_A[2][1] a une taille de 3.
Une fois la liste de listes auxiliaire aux remplie, on détermine la taille et les
coordonnées de la plus grande base (carrée) de la carte en cherchant la valeur
maximale dans aux.
aux_B associée à la carte représentée par carte_B.carte_B = [
[1, 1, 1, 1],
[0, 1, 1, 1],
[0, 1, 1, 1],
[1, 0, 1, 1],
]
On considère une carte carte_C de taille 6 ainsi que la liste auxiliaire
aux_C associée. Seules certaines valeurs sont données ci-dessous.
carte_C = [
[..., ..., ..., 0, ..., ...],
[1, ..., ..., ..., ..., ...],
[..., ..., ..., ..., ..., ...],
[..., ..., ..., 1, ..., ...],
[1, ..., ..., ..., ..., ...],
[..., ..., ..., ..., ..., ...],
]
aux_C = [
[..., ..., ..., a, 2, ...],
[b, 0, ..., 1, 1, ...],
[3, 2, ..., ..., ..., ...],
[..., ..., ..., c, 2, ...],
[d, 2, ..., 2, 2, ...],
[1, 1, ..., ..., ..., ...],
]
a, b, c et d.
Pour une liste de listes carte donnée, lorsqu’on construit la liste auxiliaire
aux associée, on commence par remplir les cellules de la dernière ligne et de la
dernière colonne de aux en recopiant celles de carte.
On admet de façon générale que, pour toutes les autres cellules, si
carte[i][j] vaut 0 alors aux[i][j] prend aussi la valeur 0, et, si
carte[i][j] = 1, alors aux[i][j] se calcule à l’aide de l’expression suivante :
1 + min(aux[i + 1][j], aux[i][j + 1], aux[i + 1][j + 1])
calcule_aux qui prend en paramètre une liste de liste carte et renvoie la liste auxiliaire associée.def calcule_aux(carte):
n = len(carte)
aux = [[0 for j in range(n)] for i in range(n)]
# Remplissage de la dernière ligne
# et de la dernière colonne
for k in range(n):
aux[n - 1][k] = ...
aux[...][...] = ...
# On complète les lignes de bas en haut
for i in range(n - 2, -1, -1):
# On complète les colonnes de droite à gauche
for j in range(...):
if ...:
aux[i][j] = 1 + min(aux[i + 1][j],
aux[i][j + 1],
aux[i + 1][j + 1])
return aux
La fonction plus_grande_base prend en paramètre une liste de listes
carte et renvoie la taille et les coordonnées du coin supérieur gauche d’une
base de taille maximale. Cette fonction utilise la liste auxiliaire aux renvoyée
par l’appel calcule_aux(carte).
plus_grande_base à partir de la ligne 7. Il est possible d’écrire plusieurs lignes.def plus_grande_base(carte):
n = len(carte)
aux = calcule_aux(carte)
taille_max = aux[0][0]
i_max = 0
j_max = 0
...
carte_1000 est la liste représentant une carte de 1 000 × 1 000 cases et
carte_3000 celle représentant une carte de 3 000 × 3 000 cases.
L’appel plus_grande_base(carte_1000) s’exécute en 0,4 seconde.
plus_grande_base(carte_3000) :