Académique Documents
Professionnel Documents
Culture Documents
La programmation dynamique met en jeu les valeurs associées aux solutions, qui permettent de sélec-
tionner les solutions optimales, et les solutions elles-mêmes qui sont de différente nature (et construites
une fois seulement qu’elles ont été identifiées comme optimales, car de valeur optimale).
Complément recherche : Il y a ainsi deux passes, une pour calculer les valeurs à utiliser pour le critère d’optimalité, et une pour
calculer la solution qui correspond à la valeur optimale calculée lors de la première passe.
quelques notes sur la programmation dynamique
Si les solutions recherchées sont précisément les valeurs mises en jeu pour le critère d’optimalité, alors
la dernière étape devient inutile (i.e. une seule passe suffit).
Pascale Le Gall
Nous verrons que face à problème qui se décompose en sous-problèmes, la programmation dynamique
est à privilégier si le problème fait apparaître :
16 octobre 2018
Sous-problèmes optimaux. Parmi les possibilités de décomposer le problème en sous-problèmes,
certaines décompositions sont à privilégier, car elles mettent en jeu des sous-problèmes optimaux
au regard du critère d’optimalité ;
1 Introduction Sous-problèmes avec recouvrement. Les décompositions font apparaître des sous-problèmes qui
partagent eux-mêmes des sous-problèmes, autrement dit, des sous-sous-problèmes du problème
La programmation dynamique et la programmation par mémoïation partagent la même idée simple : original, en commun. Il convient de ne les traiter (au sens résoudre) qu’une seule fois. De plus,
ne pas recalculer deux fois la même instance d’un (sous-)problème. pour que la complexité résultante soit raisonnable (polynomiale), il faut que l’espace des sous-
problèmes ne soit pas trop grand (polynomial).
La programmation dynamique ajoute une dimension, à savoir que la solution n’est pas directement 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
obtenue en consultant la table de mémoïsation, mais sera en recherchant parmi plusieurs familles de
De cette définition, on peut en déduire un algorithme récursif évident :
sous-problèmes.
Algorithme 1 : FIBONACCI (n)
1 2 Pascale Le Gall
2 EXEMPLE DE LA FONCTION FIBONACCI 2 EXEMPLE DE LA FONCTION FIBONACCI
√
Soit T (n) ≥ ( 2)n ≈ (1.4)n et T (n) croît exponentiellement avec n.
L’utilisation d’un tableau A de valeurs pour mémoriser les calculs intermédiaires F IBON ACCI(i)
La complexité en temps est en Θ(n), mais elle est associée à une complexité en espace linéaire (taille
du tableau A).
La version par mémoïsation consiste à systématiquement consulter la table en aveugle avant de faire
toute calcul d’une nouvelle solution. Une version possible en Python est : Figure 1 – Graphe de dépendances entre les appels de la fonction Fibonacci
memo = {}
à plusieurs reprises (différents chemins de la racine au nœud en question), l’objectif étant de ne calculer
def f i b ( n ) :
if n in memo : qu’une fois chacun des nœuds atteignables.
return memo [ n ]
elif n == 0 :
memo[ 0 ] = 0
return 0
elif n == 1 : Si les solutions des sous-problèmes sont sauvegardées, il suffit de s’y référer à chaque nouvelle demande
memo[ 1 ] = 1 de résolution d’un sous-problème déjà rencontré. On échange ainsi du temps de calcul contre de l’espace
return 1 mémoire. La résolution des sous-problèmes peut se faire selon une approche descendante (top-down)
else : ou ascendante (bottom-up).
memo [ n]= f i b ( n−1)+ f i b ( n−2)
return memo [ n ] Peut-on envisager une meilleure complexité. Remarquons les égalités suivantes :
L’algorithme peut être amélioré, en n’utilisant que deux variables pour mémoriser les valeurs intermé- Fn+1 = 1 × Fn + 1 × Fn−1
diaires.
Fn = 1 × Fn + 0 × Fn−1
Algorithme 3 : FIBONACCI-ITER(n)
Montrons alors :
1 if n ≤ 1 then ! !n
Fn+1 Fn 1 1
2 return n =
Fn Fn−1 1 0
3 else
4 pprev ← 1; Par induction :
5 prev ← 1; — Cas n = 1 : trivial (F0 = 0, F1 = 1, F2 = 1).
6 for i ← 2 to n do
7 f ← prev + pprev; — Cas inductif : n ≥ 2
8 pprev ← prev; ! ! ! !n−1 !
prev←f; Fn+1 Fn Fn Fn−1 1 1 1 1 1 1
9 = . = .
Fn Fn−1 Fn−1 Fn−2 1 0 1 0 1 0
10 return f
! !n
Fn+1 Fn 1 1
La figure 1 illustre toutes les dépendances entre appels de la fonction Fibonacci : en partant de l’idée =
Fn Fn−1 1 0
que le nœud i représente le calcul de Fibonacci de i, tous les nœuds atteignables à partir du nœud
5 participent au calcul de Fibonnaci de 5, et à ce tître doivent être exécutés une fois au moins. Les
nœuds représentent les sous-problèmes qu’il s’agit de résoudre. Ces sous-problèmes pouvant apparaître
!n
1 1 Complexité globale en Θ(n3 )
Le calcul direct de est en Θ(n).
1 0
Algorithme 4 : MAX-SUBARRAY-BRUTE-FORCE(A : array)
Il est possible d’adopter une approche "divide and conquer" pour calculer la fonction puissante an . Data : A an array of n items
( n n Résultat : The subarray such that A[j] + ... + A[k] is maximized, index j and index k
a 2 .a 2 si n est pair
an = n−1 n−1
1 n ← A.length
a 2 .a 2 .a si n est impair
2 max-so-far ← −∞ (ou bien 0)
3 for j ← 1 to n do
La complexité est alors de Θ(log n).
4 for k ← j to n do
5 sum ← 0
6 for i ← j to k do
3 Sous-tableau de somme maximale 7 sum ← sum + A[i]
Dans l’exemple du tableau A suivant : 8 if sum > max-so-far then
9 max-so-far ← sum
10 low ← j
[13, −3, −25, 20, −3, −16, −23, 18, 20, −7, 12, −5, −22, 15, −4, 7]
11 high ← k
A[8..11] est le sous-tableau de somme maximale (sum = 43).
12 return max-so-far, low, high
Par convention, le sous-tableau vide est de somme 0 (ce qui constitue de facto une borne inférieure de
la somme maximale d’un sous-tableau).
Etant donné un tableau A = [a1 , a2 , ...an ] d’entiers positifs ou négatifs, il s’agit de trouver les indices 3.2 Approche force brute optimisée
j et k avec j < k qui maximisent la somme :
En remarquant que si l’on connait sj,k , il est possible de calculer sj,k+1 en temps constant :
k
X
sj,k = aj + aj+1 + ... + ak = ai sj,k+1 = sj,k + A[k + 1]
i=j
il est possible d’améliorer l’algorithme "force brute" pour obtenir un algorithme en Θ(n2 ).
Différentes solutions algorithmiques sont possibles :
Algorithme 5 : MAX-SUBARRAY-BRUTE-FORCE-OPTIM(A : array)
— O(n3 ) : force brute.
Data : A an array of n items
— O(n2 ) : force brute avec optimisation (évidente). Résultat : The subarray such that A[j] + ... + A[k] is maximized, index j and index k
— O(n log(n)) : approche "divide and conquer". 1 n ← A.length
— O(n) : programmation dynamique. 2 max-so-far ← −∞
3 for j ← 1 to n do
4 sum ← 0
3.1 Approche force brute 5 for i ← j to n do
6 sum ← sum + A[i]
Il s’agit de calculer séparément la somme sj,k de tous les sous-tableaux A[j, k] pour les comparer. 7 if sum > max-so-far then
8 max-so-far ← sum
Etude de la complexité
9 low ← j
— Generation of tous les sous-tableaux Θ(n2 ) 10 high ← k
— Calcul de la somme des éléments pour chacun des sous-tableaux : Θ(n).
11 return m, low, high
— Trouver le maximun des sommes calculées Θ(n)
recherché. if c u r > 0 :
cur = cur + a [ i ]
Algorithme 10 : DEBFIN-SUBARRAY-LINEAR(A : array) else :
deb_cur = i
1 m[1..n] : new array cur = a [ i ]
2 max-so-far ← A[1] if c u r > maxsofar :
3 m[1] ← A[1] maxsofar = c u r
4 deb-so-far ← 1 deb = deb_cur
5 fin-so-far ← 1 fin = i
6 cur-deb ← 1 return maxsofar , deb , f i n , a [ deb : ( f i n + 1 ) ] , s u m _ l i s t ( a [ deb : ( f i n + 1 ) ] )
7 for i ← 2 to A.length do
8 if m[i − 1] > 0 then
9 m[i] ← m[i − 1] + A[i]
10 else 4 Plus longue sous-séquence commune à deux sé-
cur-deb ← i
11
12 m[i] ← A[i]
quences
13 if m[i] > max-so-far then Etant donnée une séquence X = x1 , x2 , . . . , xm , Z = z1 , z2 , . . . , zk est une sous-séquence de X s’il
14 max-so-far ← m[i] existe une sous-suite croissante i1 , i2 , . . . ik vérifiant ∀j ∈ [1, k], zj = xij .
15 deb-so-far ← cur-deb Par exemple, A, C, A est une sous-séquence de B, A, A, C, B, A, B.
16 fin-so-far ← i
La proximité entre deux séquences X et Y peut être mesurée par la longueur de la plus longue sous-
17 return deb-so-far,fin-so-far séquence commune (PLSSC) à X et Y .
Les parties en noir sont exactement celles héritées de l’algorithme 8 tandis que les parties bleues sont Une approche force brute pourrait consister à tester toutes les sous-séquences possibles. Comme il y
intercalées pour remonter les informations relatives à la solution optimale, dans le cas présent un a un nombre exponentiel de sous-séquences à considérer, l’algorithme résultant est exponentiel.
couple d’entiers, représentant les indices de début et de fin du sous-tableau solution. Etant donnée une séquence X = x1 , x2 , . . . , xm de longueur m, Xi = x1 , x2 , . . . , xi dénote pour 0 ≤
i ≤ m la séquence préfixe de X de longueur i.
Deux implémentations possibles (la seconde étant plaquée sur le second calcul des valeurs optimales) :
Soit Z = z1 , z2 , . . . zk la PLSSC de X = x1 , x2 , . . . xm et de Y = y1 , y2 , . . . yn . On a alors :
def ind_subarray ( a ) :
maxsofar = a [ 0 ]
— Si xm = yn , alors zk = xm = yn , et Zk−1 est une PLSSC de Xm−1 et de Yn−1 .
deb = 0 — Si xm 6= yn , alors, si zk 6= xm , alors Z est une PLSSC de Xm−1 et de Y .
deb_cur = 0
— Si xm 6= yn , alors, si zk 6= yn , alors Z est une PLSSC de X et de Yn−1 .
fin = 0
m = [a[0]] Ces propriétés s’établissent à partir des observations :
for i in range ( 1 , len ( a ) ) : — Supposons xm = yn . Si zk 6= xm , alors il est possible d’ajouter xm à Z pour obtenir une sous-
if m[ i −1] > 0 :
séquence commune à X et Y (puisque xm termine aussi Y ). C’est en contradiction avec le fait
m= m + [m[ i −1]+a [ i ] ]
else : que Z est une PLSSC à X et à Y . Donc xk = xm et Zk−1 est une séquence de Xm−1 et de
deb_cur = i Yn−1 . Supposons qu’il existe une PLSSC de Xm−1 et de Yn−1 de longueur strictement supérieur
m= m + [ a [ i ] ] à k − 1. Dans ce cas, en ajoutant xm à W , alors on obtiendrait une sous-séquence de X et Y de
if m[ i ] > maxsofar : longueur strictement supérieure à k. Contradiction avec k longueur de Z PLSSC de X et de Y .
maxsofar = m[ i ] Donc Zk−1 est une PLSSC de Xm−1 et de Yn−1 .
deb = deb_cur
Remarque : si xm 6= yn , alors nécessairement zk 6= xm ou zk 6= yn .
fin = i
return maxsofar , deb , f i n , a [ deb : ( f i n + 1 ) ] , s u m _ l i s t ( a [ deb : ( f i n + 1 ) ] ) — Supposons xm 6= yn et zk 6= xm , alors Z est une sous-séquence de Xm−1 et de Y . Et Z est
nécessairement une PLSSC de Xm−1 et Y (sinon Z ne serait pas une PLSSC de X et de Y ).
def ind_subarray_bis ( a ) :
— Supposons xm 6= yn et zk 6= yn , alors Z est une sous-séquence de Xm et de Yn−1 (par symétrie
maxsofar = a [ 0 ]
deb = 0 avec le point précédent).
deb_cur = 0 Ainsi si les deux séquences X et Y terminent par le même élément, alors la recherche d’une PLSSC se
fin = 0 ramène à la recherche d’une PLSSC pour Xm−1 et Yn−1 , sinon la recherche d’une PLSSC se ramène
cur = a [ 0 ] à la recherche de deux PLSSC, pour Xm−1 et Y et pour X et Yn−1 .
for i in range ( 1 , len ( a ) ) :
Les deux sous-problèmes, la recherche d’une PLSSC pour Xm−1 et Y et pour X et Yn−1 possèdent de X, autrement dit, on laisse tomber le dernier élément de X.
des sous-problèmes communs, notamment la recherche d’une PLSSC de Xm−1 et Yn−1 .
Algorithme 12 : PATH_INDICATIONS(X,Y)
Notons l(i, j) la longueur d’une PLSSC à Xi et Yj . Data : X array of m items, Y array of n items
Résultat : d an array of dimensions [1..m,1..n] of path indications
0
si i = 0 ou j = 0 1 m ← X.length
l(i, j) = l(i − 1, j − 1) + 1 si i > 0, j > 0 et Xi = Yj 2 n ← Y.length
max(l(i, j − 1), l(i − 1, j))
si i > 0, j > 0 et Xi 6= Yj 3 for i ← 0 to m do
4 c[i, 0] ← 0
En partant de ces relations de récurrence, et en stockant les longueurs dans un tableau à deux dimen-
sions, on peut calculer la longueur d’une PLSSC de deux séquences X et Y 5 for j ← 0 to n do
6 c[0, j] ← 0
Algorithme 11 : LENGTH_PLSSC(X,Y) 7 for i ← 1 to m do
Data : X array of m items, Y array of n items 8 for j ← 1 to n do
Résultat : The length of a longuest sub sequence common to X and Y 9 if X[i] = Y [j] then
1 m ← X.length 10 c[i, j] ← c[i − 1, j − 1] + 1
2 n ← Y.length 11 d[i, j] ← ’z’
3 c : new array of dimensiosn[0..m, 0..n] 12 else if c[i − 1, j] ≥ c[i, j − 1] then
4 for i ← 0 to m do 13 c[i, j] ← c[i − 1, j]
5 c[i, 0] ← 0 14 d[i, j] ← ’x’
6 for j ← 0 to n do 15 else
7 c[0, j] ← 0 16 c[i, j] ← c[i, j − 1]
8 for i ← 1 to m do 17 d[i, j] ←, ’y’
9 for j ← 1 to n do
10 if X[i] = Y [j] then 18 return d
11 c[i, j] ← c[i − 1, j − 1] + 1
12 else if c[i − 1, j] ≥ c[i, j − 1] then Algorithme 13 : PLSSC(X,Y)
13 c[i, j] ← c[i − 1, j] Data : X array of m items, Y array of n items, d path indications for (X,Y )
Résultat : Z the longest subsequence of common elements
14 else
15 c[i, j] ← c[i, j − 1] 1 i ← X.length
2 ← Y.length
16 return c[m, n] 3 for i ← 0 to m do
4 c[i, 0] ← 0
Cet algorithme ci-dessus calcule bien la longueur de la plus longue sous-séquence commune à deux 5 for j ← 0 to n do
séquences X et Y . Mais on n’a pas accès à la sous-séquence commune proprement dite. 6 c[0, j] ← 0
Pour cela, il suffit de remarquer que la longueur augmente lorsque X[i] et Y [j] sont identiques. Lorsque 7 for i ← 1 to m do
c[i − 1, j] ≥ c[i, j − 1], alors le cas [i, j] se ramène au cas [i − 1, j], en laissant tomber la dernière lettre 8 for j ← 1 to n do
9 if X[i] = Y [j] then
10 c[i, j] ← c[i − 1, j − 1] + 1
11 d[i, j] ← ’z’
12 else if c[i − 1, j] ≥ c[i, j − 1] then
13 c[i, j] ← c[i − 1, j]
14 d[i, j] ← ’x’
15 else
16 c[i, j] ← c[i, j − 1]
17 d[i, j] ←, ’y’
18 return d
En partant de d[n, m] et en suivant les indications fournis par les éléments de d : else :
— si l’élément d[i, j] est ’z’, alors on garde X[i] et on passe à d[i − 1, j − 1] ; print ( x [ i − 1 ] . r j u s t ( 3 ) , end=" " )
for j in range ( 0 ,m+1):
— si l’élément d[i, j] est ’x’, alors on passe à d[i − 1, j] ; if type ( c [ ( i , j ) ] ) == int :
— si l’élément d[i, j] est ’y’, alors on passe à d[i, j − 1]. print ( repr ( c [ ( i , j ) ] ) . r j u s t ( 3 ) , end= " " )
else :
Le calcul de la plus longue sous-séquence commune à X et Y est donné par P LSSC(X, Y, X.length, Y.length, d) print ( c [ ( i , j ) ] . r j u s t ( 3 ) , end= " " )
avec d résultat de P AT H_IN DICAT ION S(X, Y ) print ( )
t | | | | | | = - - |
i | | | | | = | | | |
def p l s s c _ s t r ( x , y ) : e | | | | | | | | | =
return p l s s c _ r e s ( list ( x ) , list ( y ) ) l = | | | | | | | | |
[’o’, ’t’, ’e’]
Ce qui donne par exemple >>> plssc_str(’optiminisme’,’pessimisme’)
ij p e s s i m i s m e
>>> plssc_str("python","cobra")
ij c o b r a o | | | | | | | | | |
p = - - - - - - - - -
p | | | | | t | | | | | | | | | |
y | | | | | i | | | | = - = - - -
t | | | | | m | | | | | = - - = -
h | | | | | i | | | | = | = - - -
o | = - - - n | | | | | | | | | |
n | | | | | i | | | | = | = | | |
[’o’] s | | = = | | | = - -
>>> plssc_str("pomme ananas","poire banane") m | | | | | = | | = -
ij p o i r e b a n a n e e | = | | | | | | | =
[’p’, ’i’, ’m’, ’i’, ’s’, ’m’, ’e’]
p = - - - - - - - - - - - >>> plssc_str(’centrale’,’supelec’)
o | = - - - - - - - - - - ij s u p e l e c
m | | | | | | | | | | | |
m | | | | | | | | | | | | c | | | | | | =
e | | | | = - - - - - - = e | | | = - = |
| | | | | = - - - - - - n | | | | | | |
a | | | | | | | = - = - - t | | | | | | |
n | | | | | | | | = - = - r | | | | | | |
a | | | | | | | = | = - - a | | | | | | |
n | | | | | | | | = | = - l | | | | = - -
a | | | | | | | = | = | | e | | | = | = -
s | | | | | | | | | | | | [’e’, ’l’, ’e’]
[’p’, ’o’, ’e’, ’ ’, ’a’, ’n’, ’a’, ’n’]
>>>
avec "=" pour "z", "|" pour "x" et "-" pour "y".
Et pour finir avec d’autres exemples :
>>> plssc_str(’exponentiel’,’logarithme’)
ij l o g a r i t h m e
e | | | | | | | | | =
x | | | | | | | | | |
p | | | | | | | | | |
o | = - - - - - - - |
n | | | | | | | | | |
e | | | | | | | | | =
n | | | | | | | | | |