Énoncé

informatique commune
Travaux dirigés
Programmation dynamique
Le plus souvent, un algorithme récursif résout un problème en combinant des solutions de sous-problèmes. C’est par
exemple le cas du tri fusion qui a été étudié au chapitre 3 pour trier un tableau de n cases :
(i) scission du tableau en deux parties sensiblement égales ;
(ii) tri récursif de chacun de ces deux tableaux ;
(iii) fusion des deux demi-tableaux triés.
Dans ce cas, la programmation récursive se révèle particulièrement efficace car le problème initial a été partitionné en
sous-problèmes indépendants (illustration figure 1 ; chaque arête représente un appel récursif).
Figure 1 – Résolution récursive de sous-problèmes
indépendants.
Figure 2 – Résolution récursive de sous-problèmes
dépendants.
Lorsque les sous-problèmes ne sont pas indépendants (c’est-à-dire lorsque les sous-problèmes ont des sous-sous-problèmes
communs) la programmation récursive se révèle le plus souvent très inefficace car elle conduit à résoudre de nombreuses
fois les sous-sous-problèmes communs (illustration figure 4).
Nous avons déjà évoqué le problème à la fin du chapitre 2 consacré à la récursivité, en fournissant une réponse simple :
adjoindre à la fonction récursive un dictionnaire pour mémoriser les résultats aux fur et à mesure de leur obtention.
La programmation dynamique ne procède pas autrement, mais en utilisant un tableau pour mémoriser les résultats
plutôt qu’un dictionnaire, ce qui exige de prévoir à l’avance l’espace mémoire nécessaire à la mémorisation des résultats
intermédiaires. En contrepartie, la programmation de l’algorithme dans un style impératif devient très facile à réaliser.
Problèmes d’optimisation et heuristique gloutonne
La programmation dynamique est souvent utilisée pour résoudre des problèmes d’optimisation. Dans ce type de problème,
il peut y avoir de nombreuses solutions possibles. Chaque solution a une valeur, et on souhaite trouver une solution ayant
la valeur optimale (elle n’est pas nécessairement unique). Ainsi, le développement d’un algorithme de programmation
dynamique peut être découpé en trois étapes :
(i) obtenir une relation de récurrence liant une solution optimale du problème à celles de sous-problèmes ;
(ii) initialiser le tableau de mémorisation des résultats ;
(iii) remplir le tableau de manière ascendante à l’aide de la relation de récurrence obtenue au premier point.
Cependant, dans le cas de problèmes complexes, la stratégie dynamique peut s’avérer encore trop lourde pour déterminer
le meilleur choix. Dans ce cas, on utilise une heuristique gloutonne consistant à faire toujours le choix qui semble le meilleur
sur le moment. En d’autres termes, on effectue des choix localement optimaux dans l’espoir que ces choix mèneront à une
solution globalement optimale.
Le terme d’heuristique 1 est employé puisque bien évidemment, cette approche ne mène pas systématiquement à une
solution optimale ; cependant dans de nombreux cas cette stratégie s’avère payante et fournit une solution satisfaisante,
voire optimale.
1. En informatique on désigne ainsi une méthode de calcul qui fournit rapidement une solution réalisable mais pas nécessairement optimale.
page 1
Figure 3 – Programmation dynamique : les sousproblèmes sont résolus en ordre ascendant.
Figure 4 – Programmation gloutonne : l’heuristique choisit les sous-problèmes à résoudre en ordre descendant.
Exercice 1. rendu de monnaie
Le problème du rendu de monnaie va nous fournir un premier exemple simple pour comprendre les démarche dynamique
et gloutonne : comment décomposer une somme d’argent en utilisant un nombre minimal de pièces à choisir parmi un nombre
limité de valeurs possibles ?
Considérons par exemple les unités monétaires européennes c = [1, 2, 5, 10, 20, 50, 100, 200, 500] (en nombre entier d’€).
On suppose disposer d’un nombre illimité de pièces / billets de chaque type, et on souhaite connaitre la décomposition en
nombre de pièces / billets minimale d’une certaine somme, par exemple n = 493€.
Une stratégie gloutonne consiste à rendre la pièce / billet v de valeur maximale parmi celles de valeurs inférieures ou
égales à n (ici v = 200) puis à réitérer le procédé avec n−v. Avec cet exemple, on obtient : 493 = 200+200+50+20+20+2+1.
a) Rédiger une fonction glouton qui prend en arguments une somme n et un système de monnaie c supposé trié par
ordre croissant et qui retourne la décomposition obtenue en suivant la stratégie gloutonne décrite ci-dessus sous la
forme d’une liste de valeurs à utiliser. Par exemple :
>>> glouton(493, [1, 2, 5, 10, 20, 50, 100, 200, 500])
[200, 200, 50, 20, 20, 2, 1]
Pour qu’il y ait toujours une solution on supposera que c[0] = 1.
Cette stratégie gloutonne fournit souvent des solutions optimales (c’est systématiquement le cas avec le système monétaire
européen) mais il y a des exceptions : le système britannique d’avant 1971 utilisait les multiples suivants du penny :
c = [1, 3, 6, 12, 24, 30]. Avec ce système, l’algorithme glouton décompose 48 pennies en : 48 = 30 + 12 + 6 alors que la
décomposition optimale est : 48 = 24 + 24. Dans ce cas, uns stratégie dynamique s’impose.
b) On note f (n, p) le nombre de billets / pièces minimal à utiliser pour décomposer une somme n à l’aide des unités
monétaires contenues dans le tableau [c0 , c1 , . . . , cp ]. Justifier que :



f (n,p − 1)
si n < cp
f (n, p) = 

min f (n, p − 1), 1 + f (n − cp , p) sinon
La mise en œuvre de la stratégie dynamique consiste à créer un tableau t de taille (n + 1) × (p + 1) initialement vide puis à
le remplir pour qu’à terme t[i, j] = f (i, j).
Pour savoir dans quel ordre remplir les cases de ce tableau, il peut être utile de représenter le diagramme des dépendances :
t[i, j] = min(t[i, j − 1], t[i − cj , j])
j
j −1
i − cj
i
On observe que pour le remplir à l’aide de la formule de récurrence il est nécessaire de fixer les valeurs de t[0, j] = 1 (j > 0)
et de t[i, 0] = i (i > 1), sachant qu’on a imposé c0 = 1.
page 2
c) Rédiger une fonction dynamique qui prend en arguments une somme n et un système de monnaie c = [c0 , c2 , . . . , cp ]
(avec c0 = 1) et qui retourne le nombre f (n, p) de billets / pièces minimal à utiliser pour décomposer la somme n.
Notre solution n’est pas encore satisfaisante : nous connaissons grâce à elle le nombre minimal d’unités monétaires à
utiliser, mais pas le détail d’une décomposition minimale. La solution consiste à stocker dans la case (i, j) tableau t non
seulement la valeur de f (i, j) mais aussi une liste contenant les pièces à utiliser pour réaliser ce minimum.
d) Modifier la fonction dynamique pour qu’elle retourne maintenant la réalisation d’une décomposition minimale.
Observons enfin le diagramme des dépendances : on peut constater que le calcul de la ligne t[: , j] ne nécessite d’avoir
mémorisé que la ligne précédente t[: , j − 1]. On peut donc se contenter de stocker les résultats intermédiaires dans un
tableau uni-dimensionnel et utiliser les relations :
t[i] ←− min(t[i], t[i − cj ])
si i > cj
pour remplacer les valeurs de la ligne de rang j − 1 stockées dans t par les valeurs de la ligne de rang j (à condition de
remplir cette ligne de la gauche vers la droite 2 ).
e) Modifier une dernière fois la fonction dynamique en utilisant cette fois un tableau uni-dimensionnel.
Exercice 2. Programmation de festival
Un festival de musique programme un certain nombre de concerts c1 , . . . , cn , chaque concert ci étant défini par une heure
de début di et une heure de fin fi > di . Il est possible d’assister aux concerts ci et cj dès lors que les intervalles [di , fi [ et
[dj , fj [ ne se chevauchent pas, c’est-à-dire si fi 6 dj ou fj 6 di . Le but du festivalier est d’assister à un plus grand nombre de
concerts possibles. On convient de plus de poser d0 = f0 = −∞ et dn+1 = fn+1 = +∞.
[2, 13]
[5, 9]
[3, 8]
[0, 6]
[8, 12]
[3, 5]
[6, 10]
[1, 4]
0
[5, 7]
[8, 11]
5
10
[12, 14]
15
Figure 5 – Dans cet exemple, il est possible d’assister à quatre concerts durant le festival.
On suppose les concerts rangés par ordre croissant d’heure de fin : f1 6 f2 6 · · · 6 fn et on note Sij l’ensemble des concerts
auxquels il est possible d’assister entre les heures fi et dj .
a) Que vaut Sij lorsque i > j ?
b) On note mij le nombre maximal de concerts auxquels on peut assister parmi ceux de Sij . Prouver que lorsque i < j



0 n
o si Sij = ∅
alors mij = 

max mik + mkj + 1 ck ∈ Sij sinon
c) En déduire une fonction festival qui prend en arguments deux tableaux d et f de même taille contenant respectivement les dates d1 , . . . , dn de début et f1 , . . . , fn de fin des différents concerts (le tableau f étant trié par ordre
croissant), et qui retourne une liste maximale de concerts (sous la forme d’une liste d’entiers i ∈ ~1, n) auxquels il
est possible d’assister.
Nous allons maintenant montrer qu’il existe une solution gloutonne.
d) Montrer que parmi les séries de concerts de cardinal maximal il en existe au moins une qui contient c1 , et en déduire
un algorithme glouton qui résout le problème.
Exercice 3. Impression équilibrée
Le problème qui nous intéresse est celui de l’impression équilibrée d’un paragraphe sur une imprimante. Le texte en
entrée est une suite de n mots de longueurs w1 , w2 , . . . , wn . On désire imprimer ce paragraphe de manière équilibrée sur
des lignes ne pouvant contenir qu’un maximum de m caractères chacune 3 . Le critère d’équilibre est le suivant : si une
ligne contient les mots de i à j inclus et que nous laissons un caractère d’espacement entre chaque mot, le nombre de
caractères d’espacements supplémentaires à la fin de la ligne est :
m−j +i −
j
X
wk .
k=i
2. Essayez de bien comprendre pourquoi.
3. On suppose que pour tout i ∈ ~1, n, wi 6 m.
page 3
L’objectif est de minimiser la somme, sur toutes les lignes hormis la dernière, des cubes des nombres de caractères
d’espacement présents à la fin de chaque ligne.
Par exemple, si on cherche à découper le roman À la recherche du temps perdu de Marcel Proust (« Longtemps, je me suis
couché de bonne heure. . . . ») en lignes de 20 caractères, il n’y a que trois possibilités pour la première ligne (car le mot
suivant est trop long pour tenir sur celle-ci) :
Longtemps,
je . . . . . . . . . . . . . . . . . . . .
........................
........................
Longtemps, je
me . . . . . . . . . . . . . . . . . . . .
........................
........................
Longtemps, je me
suis . . . . . . . . . . . . . . . . . .
........................
........................
Le déséquilibre de la ligne en question est la quantité
j
X
3
f (i, j) = m − j + i −
wk .
k=i
Dans le premier cas, le déséquilibre de la première ligne est égal à f (1, 1) = (m − w1 )3 = (20 − 10)3 = 103 ; dans le second
cas il est égal à f (1, 2) = (m − 1 − w1 − w2 )3 = 73 ; dans le dernier cas il est égal à f (1, 3) = (20 − 2 − w1 − w2 − w3 )3 = 43 .
La stratégie gloutonne consiste à remplir les lignes une par une en mettant à chaque fois le plus de mots possibles sur la
ligne en cours.
a) Définir une fonction Python qui met en œuvre cette stratégie gloutonne. Cette fonction devra prendre en paramètres
l’entier m et le tableau [w1 , w2 , . . . , wn ] et retourner la valeur du déséquilibre total.
b) Montrer sur un exemple simple que cette stratégie gloutonne ne donne pas nécessairement lieu au découpage
optimal.
On désire maintenant appliquer une stratégie dynamique. Pour cela, on note α(i) le plus grand entier j pour lequel
wi + · · · + wj 6 m (il s’agit de l’indice maximal du dernier mot d’une ligne débutant par le mot wi ) et d(i) la valeur minimale
du déséquilibre total occasionné par le formatage du paragraphe débutant par le mot wi (autrement dit le paragraphe
constitué des mots wi , . . . , wn ).
c) Exprimer d(i) en fonction de α(i), des valeurs d(j) pour j > i et de la fonction f .
d) En déduire un algorithme dynamique qu’on rédigera en Python calculant le plus petit déséquilibre qu’on puisse
obtenir pour une valeur de m et un tableau [w1 , w2 , . . . , wn ] donnés.
e) Prouver enfin que si nous avions choisi pour mesure du déséquilibre la quantité
f (i, j) = m − j + i −
j
X
wk
k=i
alors l’algorithme glouton aurait produit le même résultat que l’algorithme dynamique.
page 4