Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Prog Dyn

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 10

Programmation dynamique

La récursivité et la méthode diviser pour régner permettent de résoudre des pro-


blèmes de manière descendante en divisant un exemplaire en sous-exemplaires
puis de fusionner ensuite les solutions obtenues sur les sous-exemplaires pour résoudre
l’exemplaire de départ.
Par contre pour certains problèmes cette manière de procéder est inefficace car on
retombe souvent sur les mêmes sous-exemplaires et on refait toujours les mêmes calculs,
ce qui est une perte de temps. Nous avons déjà vu ce problème par exemple lors du
calcul des termes de la suite de Fibonnacci de manière récursive.
Par ailleurs nous venons de voir l’algorithme de Bellman-Ford (Richard Bellman
est l’inventeur de la méthode de programmation appelée "programmation dynamique")
où le calcul des plus courts chemins d’une source donnée vers les autres sommets d’un
graphe orienté pondéré se fait de manière ascendante.
La programmation dynamique est une méthode ascendante itérative : On
initialise la méthode dans un tableau par les sous-exemplaires les plus petits, puis on
obtient les sous-exemplaires plus grands et ainsi de suite on remplit le tableau jusqu’à
obtenir la solution de l’exemplaire du départ.

1 Exemples
1.1 La suite de Fibonnacci
La suite de Fibonnacci est définie par récurrence par
F0 = 0, F1 = 1 et Fn = Fn−1 + Fn−2 pour n > 2

1.1.1 Une solution récursive


On suit la définition mathématique
def fibo ( n ):
assert n >= 0
if n == 0:
return 0
if n == 1:
return 1
return fibo (n -1)+ fibo (n -2)

1.1.2 Une solution par programmation dynamique


Dans un tableau T on initialise T[0] avec 0 et T[1] avec 1
def fibo_dyn ( n : int ) - > int :
assert n >= 0
T = [0]*( n +1)
T [1] = 1
for i in range (2 , n +1):
T [ i ] = T [i -1] + T [i -2]

1
return T [ n ]

1.2 Les coefficients binomiaux


Les coefficients binomiaux sont définis par récurrence ainsi :
n n

0
= n =  1 n−1
n n−1
p
= p + p−1 pour n > 2 et p > 1

1.2.1 Une solution récursive


On suit la définition mathématique
def coeff_bin (n , p ):
assert n >= 2 and p >= 0
if p == 0:
return 1
if p == n :
return 1
return coeff_bin (n -1 , p )+ coeff_bin (n -1 ,p -1)

1.2.2 Une solution par programmation dynamique


Une première approche : On utilise un tableau à deux dimensions
def coeff_bin_dyn ( n : int , p : int ) - > int :
assert n >= 1 and p >= 0

T = [[0]*( n +1) for i in range ( n +1)]


for lig in range (1 , n +1):
T [ lig ][0] , T [ lig ][ lig ] = 1 ,1
for col in range (1 , lig ):
T [ lig ][ col ] = T [ lig -1][ col ]+ T [ lig -1][ col -1]
return T [ n ][ p ]

Une deuxième approche : On n’utilise que deux listes, on utilise ainsi moins d’em-
placement dans la mémoire
def coeff_bin_dyn2 ( n : int , p : int ) - > int :
assert n >= 1 and p >= 0

T1 = [0]*( n +1)
T1 [0] , T1 [1] = 1 ,1
for lig in range (2 , n +1):
T2 = [0]*( n +1)
T2 [0] , T2 [ lig ]=1 ,1
for col in range (1 , lig ):
T2 [ col ] = T1 [ col ]+ T1 [ col -1]
T1 = T2

2
return T1 [ p ]

2 Le problème du découpage des barres

longueur i 1 2 3 4 5 6 7 8 9 10
prix pi (euros) 1 5 8 9 10 17 17 20 34 30

Problème Etant donné une barre de longueur n déterminer le revenu maximal rn


que l’on puisse obtenir en coupant la barre puis en vendant les morceaux
Pour n = 1 r1 = 1
Pour n = 2 soit on coupe la barre en deux morceaux pour un prix de 2 euros soit
on ne la coupe pas dans ce cas on a une valeur plus grande 5 euros donc r2 = 5
Pour n = 3 si on ne coupe pas on a une valeur de 8 euros si on coupe en 3 morceaux
on a une valeur de 3 euros soit on coupe en deux morceaux de valeur r2 + r1 donc de 6
euros
Par conséquent r3 = 8
On a donc une formule de récurrence

rn = max(pn , r1 +rn−1 , r2 +rn−2 , ....., rn−1 +r1 ) = max(pn , r1 +rn−1 , r2 +rn−2 , ....., rk +
rn−k ) avec k 6 n//2 et r1 = 1

2.0.1 Une solution récursive


On suit la formule de récurrence
def revenu_max ( prix : list , n : int ) - > int :
if n == 0:
return 0
return max ([ prix [ n ]]+[ revenu_max ( prix , i )+ revenu_max ( prix , n - i ) \
for i in range (1 , n //2 + 1)])

L’inconvénient de cette méthode est le calcul de valeurs déjà calculées.

2.0.2 Une solution par programmation dynamique


En remplissant un tableau de manière ascendante
def revenu_max_dyn ( prix : list , n : int ) - > int :
revenu = [0]*( n +1)
for k in range (1 , n +1):
revenu [ k ] = max ([ prix [ k ]]+[ revenu [ i ]+ revenu [k - i ]\
for i in range (1 , k //2 + 1)])
return revenu [ n ]

Remarque : On aimerait avoir en plus du revenu maximal, la découpe


sous forme d’une liste conduisant à ce revenu maximal (Voir exercice)

3
3 Propriété de la sous structure optimale
Définition 1. On dit qu’un problème vérifie la propriété de la sous-structure optimale
si une solution optimale contient en elle des solutions optimales de sous-problèmes
Exemple :
Dans un graphe pondéré le chemin le plus court de la source s à un sommet v passant
par w contient le chemin le plus court de s à w et le chemin le plus court de w à v
Preuve
Par l’absurde :
c c
Supposons que s → v soit le plus court et on décompose s → v en
c1 c2
s→ w→ v
c1 c01
Si s → w n’est pas le plus court alors il existe s → w plus court (soit en poids soit
1 c 1 2c0 c 1 2 c c
en nombre d’arcs) que s → w et dans ce cas s → w→ v est plus court que s → w→ v
ce qui est absurde
Théorème 1. Lorsqu’un problème vérifie la propriété de la sous-structure optimale on
peut penser à la programmation dynamique pour le coder par une formule de récurrence

4 Le problème du rendu de monnaie


Imaginons que nous devons rendre 43 euros et que nous avons à notre disposition des
pièces de 1,2,5,10 euros avec la contrainte supplémentaire de rendre le moins de
pièces possible (problème d’optimisation) (pour simplifier il n’y a pas de billets
mais que des pièces)
Nous avons vu l’année dernière une stratégie gloutonne pour résoudre ce problème
La solution est 43 = 4 × 10 + 1 × 2 + 1 × 1
Par contre cette stratégie ne marche plus si on change l’ensemble des pièces par
exemple :
Imaginons que nous devons toujours rendre 43 euros et que cette fois ci nous avons
à notre disposition des pièces de 1,3,7,21,30 euros avec la contrainte supplémentaire de
rendre le moins de pièces possible
On rend donc 1 pièce de 30 et une pièce de 7 et deux pièces de 3 euros.
On a donc rendu 4 pièces mais on peut faire mieux avec 3 pièces car 43 = 2×21+1
On va chercher une formule de récurrence pour résoudre ce problème de
manière générale et de manière ascendante
Dans un premier temps il faut nommer correctement la quantité que l’on veut mi-
nimiser.
C’est un nombre de pièces qui est entier et qui est fonction d’une valeur entière v.
Pourquoi pas nb_pieces(v) ?
On cherche une formule pour le nombre de pièces minimal à rendre pour la valeur v
1. Que vaut nb_pieces(1) ?
Evidement nb_pieces(1) = 1.
2. Que vaut nb_pieces(2) ?
Quel est le lien entre nb_pieces(2) et nb_pieces(1) et nb_pieces(0) ?
Comment passer de nb_pieces(2) à nb_pieces(1) et nb_pieces(0) ?

4
On part de nb_pieces(0) qui vaut 0 en ajoutant une pièce de valeur 2, pour
aller vers nb_pieces(2), ce qui fait donc 1 pièce en tout.
On part de nb_pieces(1) qui vaut 1 en ajoutant une pièce de valeur 1, pour
aller vers nb_pieces(2), ce qui fait en tout 2 pièces.
On prend le minimum de ces deux nombres et on obtient nb_pieces(2) = 1.
3. Que vaut nb_pieces(3) ?
En procédant comme précédemment on compare nb_pieces(3 - 2) + 1 et 1 +
nb_pieces(3 - 1) et on prend le plus petit des deux nombres.
On obtient nb_pieces(3) = 2.
Dans le cas général on obtient la formule de récurrence suivante :

Si P est un ensemble de pièces où P = {p1 <, p2 , ... < pn } les pi sont entiers, si v est
une valeur entière et strictement positive, alors :
Si v = 0 alors on prend 0 pièces
Sinon nb_pieces(v) = min(1 + nb_pieces(v-p)) où p ∈ P et v − p > 0
= 1 + min(nb_pieces(v-p))) où p ∈ P et v − p > 0

4.0.1 Une solution récursive

def nb_pieces (v , P ):
"""
v un entier >= 0
P une liste d ’ entiers strictement positifs
"""
assert v >= 0
if v == 0:
return 0
return 1 + \
min ([ nb \ _pieces (v - p ) for p in P if v - p >= 0])

4.0.2 Programmation dynamique


Comment mettre en action la relation de récurrence de manière ascendante ?
def nb_pieces_dyn (v , P ):
"""
v un entier >= 0
P une liste d ’ entiers strictement positifs
"""
assert v >= 0
T = [0]*( v +1)
for i in range (1 , v +1):
T [ i ] = 1 + min ([ T [i - p ] for p in P if v - i >= 0])
return T [ v ]

5
5 Le problème du sac à dos
Problème
Etant donné un sac à dos de charge maximale W = 30 kg il s’agit d’optimiser en
valeur le contenu du sac à dos en remplissant le sac avec les objets suivants de prix en
euros pi et de poids wi en kg.
Chaque objet est unique et ne peut être pris qu’une fois.

Objets 0 1 2 3
pi (euros) 7 4 3 3
wi (kg) 13 12 8 10

5.1 Approche "gloutonne"


1. Calculer pour chaque objet son prix au kg
2. Ecrire en pseudo-code un algorithme glouton pour résoudre le problème
3. Définir une fonction Python sac_a_dos_glouton(prix,poids) où prix et poids
sont deux listes de longueur n et chaque objet i tel que 0 6 i 6 n − 1 a un prix
prix[i] et un poids poids[i]
4. Sur ce cas particulier quelle est la solution fournie par l’algorithme ?

5.2 Programmation dynamique


Notations :
1. Données : prix et poids sont deux listes de longueur n et chaque objet i tel que
0 6 i 6 n − 1 a un prix prix[i] et un poids poids[i], W la charge maximale
du sac.
2. Résultat : une liste xi pour i de 0 à n − 1 où xi = 0 si on ne met pas l’objet i
dans le sac, et xi = 1 si on met l’objet i dans le sac
3. On note P (i, w) le prix maximal obtenu si on charge le sac uniquement avec les
objets de 0 à i et pour un poids total inférieur ou égal à w
Une solution au problème est P (n − 1, W ) où W est la contenance maximale du
sac

On travaille maintenant avec les données suivantes avec W = 7


Objets 0 1 2 3
pi (euros) 100 55 18 70
wi (kg) 5 3 1 4

Remplir le tableau suivant pour P(i,w)

w=0 w=1 w=2 w=3 w=4 w=5 w=6 w=7


i = 0 0 0 0 0 0 100 100 100
i = 1 0 0 0 55 55 100 100 100
i = 2 0
i = 3 0

6
Explication de la formule de récurrence pour le problème du sac à dos.
Admettons que le problème du sac à dos vérifie la propriété de la sous-structure
optimale
Dans ce cas si dans la solution optimale P(i,w) l’objet i s’y trouve alors on peut
isoler cet objet de poids poids[i] et de prix prix[i] et écrire suivant le principe de
la sous-structure optimale
P (i, w) = P (i − 1, w − poids[i]) + prix[i]
Si l’objet i ne s’y trouve pas c’est parce que :
1. Soit il est trop lourd dans le sens où w < poids[i], dans ce cas P (i, w) = P (i−1, w)
2. Soit P (i − 1, w − poids[i]) + prix[i] < P (i − 1, w)
D’où la formule de récurrence :

P (i, 0) = 0 pour tout i


P (0, w) = 0 pour w < poids[0] et P (0, w) = prix[0] pour w > poids[0]
Si w < poids[i] alors P (i, w) = P (i − 1, w)
Sinon P (i, w) = max(P (i − 1, w), P (i − 1, w − p[i]) + prix[i]))

Exercice
1. Ecrire une fonction Python prix_max(prix,poids,W) qui renvoie le prix maxi-
mal obtenu avec les objets pour un poids total 6 W
2. Ecrire une fonction sol_max(prix,poids,W) qui renvoie une solution maximale
au problème.
Indication :
(a) Construire le tableau P comme dans la fonction sac_a_dos(prix,poids)
(b) Initialiser un tableau solution de longueur égale au tableau poids unique-
ment avec des 0
(c) Partir du coin inférieur droit du tableau P, c’est à dire de P(n-1,W) et faire
marche arrière jusqu’à arriver à P(0,0) en appliquant la règle suivante :
Si P(i-1,w) > P(i,w) alors on a pris l’objet i donc solution[i] = 1 et ensuite
aller à P(i-1,w-poids[i])
Sinon aller à P(i-1,w)

6 Les grandes étapes de la programmation dynamique


1. Première étape : Est ce que le problème d’optimisation se prête à la program-
mation dynamique ? Est ce qu’une solution optimale au problème contient en
elle des solutions optimales de sous-problèmes ?
2. Deuxième étape : Si le problème vérifie la propriété de la sous-structure op-
timale alors on cherche une définition récursive de la valeur d’une solution opti-
male.
3. Troisième étape : A partir de la définition récursive on calcule de manière
ascendante et itérative la valeur d’une solution optimale en s’aidant d’un tableau
à une ou deux dimensions.

7
4. Quatrième étape : A l’aide du tableau et de manière descendante construire
une solution optimale

7 Le problème du touriste à Manhattan


Vous êtes un touriste à Manhattan et avant de repartir chez vous, étant pressé par
le temps, vous voulez visiter le plus possible de sites intéressants sur la carte (vecteurs)
entre l’intersection de la 59e rue et de la 8e avenue (en haut à gauche) et l’intersection
de la 42e rue et de la 3e avenue (en bas à droite).
Vous ne pouvez vous déplacer que vers le Sud ou l’Est.

1. Combien y-a-t-il de chemins possibles ?


2. Résoudre le problème par programmation dynamique

8 Le problème de la comparaison de séquences


Lire le texte suivant concernant le problème de l’alignement de séquences
https://interstices.info/alignement-optimal-et-comparaison-de-sequences-genomiques-
Conclusion :
1. Une première façon de comparer deux séquences de même longueur est de comp-
ter le nombre de différences entre les deux séquences, caractère par caractère.

8
Ecrire une fonction Python nb_differences(sequ1:str,sequ2,str) qui ren-
voie le nombre de différences entre deux chaînes de même longueur
2. Or les séquences ne sont pas toujours de même longueur, et il n’est pas forcément
pertinent de comparer les séquences dans l’ordre des caractères.
Pour se donner la possibilité de comparer des caractères de position différente
dans leur séquence respective on introduit dans l’une ou l’autre des séquences
un caractère spécial, le gap noté -
Par exemple ACTGTA et CTGTAC ont un nombre de différences de 6 si on
les compare dans l’ordre des caractères, par contre on se rend compte intuitive-
ment que le bloc CTGTA est dans les deux séquences ce qui montre une grande
familiarité entre les deux séquences.
En introduisant un gap à la fin de la première séquence et au début de la
deuxième séquence
ACTGTA -
- CTGTAC
puis en comptant le nombre de différences comme en 1) on arrive cette fois ci à
un nombre de différences de 2.

9
9 Exercices
Ex 1
Utiliser le module time et chronométrer le temps d’exécution de
1. fibo(20) et fibo(30)
2. fibo_dyn(20) et fibo_dyn(30)
3. coeff_bin(10,2) et coeff_bin(30,6)
4. coeff_bin_dyn(30,6) et coeff_bin_dyn(100,20)
5. coeff_bin_dyn2(30,6) et coeff_bin_dyn2(100,20)
Ex 2
Utiliser le module time et chronométrer le temps d’exécution de nb_pieces(v) et
nb_pieces_dyn(v)
Ex 3
1. Définir une fonction récursive nb_barres(n) qui retourne le nombre optimal de
barres
2. Définir une fonction itérative nb_barres_dyn(n) qui retourne le nombre optimal
de barres obtenu par programmation dynamique
Ex 4
Ecrire une fonction itérative decoupe_barres(n) qui retourne sous forme de liste
la découpe de la barre donnant un revenu maximal.
Ex 5
Dans un tableau triangulaire sont rangés des nombres par exemple :
tab = [[3] ,
[4 , 8] ,
[5 , 2 , 1] ,
[2 , 4 , 3 , 9]]

En partant du sommet du tableau tab[0][0] puis en descendant de cellule en cellule


adjacentes, trouver le chemin générant la plus grande somme possible où :
tab[i-1][j] est adjacent avec tab[i][j-1], tab[i][j] et tab[i][j+1] sauf si j = 0 dans ce cas
tab[i-1][j] est adjacent avec tab[i][j] et tab[i][j+1]
Sur l’exemple qui est donné un chemin possible est 3 → 8 → 5 → 4 dont la somme
vaut 20
1. Trouver sur l’exemple qui est donné le chemin donnant la somme la plus grande
2. Proposer une méthode de programmation dynamique permettant de trouver la
somme la plus grande de tous les chemins possibles dans le tableau (Ecrire une
fonction Python)
3. Ecrire une fonction Python donnant le chemin correspondant à la somme maxi-
male.

10

Vous aimerez peut-être aussi