Programmation Dynamique

Transcription

Programmation Dynamique
T ROIS PARADIGMES D ’ ALGORITHMES
A LGORITHMIC D ESIGN PATTERN
Programmation Dynamique
ACT
I
Rappels
I
Paradigmes d’algorithme
I
I
Sophie Tison
Master Informatique
Université de Lille
L A PROGRAMMATION DYNAMIQUE
I
Diviser pour régner
Programmation Dynamique
Algorithmes Gloutons
I
Complexité des problèmes
I
Algorithmique Avancée
C ONCEPTION D ’ UN ALGORITHME DE
PROGRAMMATION DYNAMIQUE
. Un schéma d’algorithme exhibé dans les années
1950 par Bellman.
. Elle s’utilise quand:
. La solution (optimale) d’un
problème de taille n s’exprime en
fonction de la solution (optimale) de
problèmes de taille inférieure à n
-c’est le principe d’optimalité-.
. Une implémentation récursive
”naı̈ve” conduit à calculer de
nombreuses fois la solution de
mêmes sous-problèmes.
L’essentiel du travail conceptuel réside dans l’expression d’une
solution d’un problème en fonction de celles de problèmes
”plus petits”!!!
L’ EXEMPLE DE LA PLUS LONGUE SOUS - SUITE
E XEMPLES
COMMUNE
Si u = act et v = archi, ac est la sous-suite de longueur maximale
et donc lcs(u, v) vaut 2.
Le problème: Une sous-suite (ou sous-mot) d’un mot u est un
mot w obtenu à partir de u en effaçant des lettres:
act est sous-suite de pacte, de manchot, de laı̈cité, de accentuer.
Soient deux mots u et v. On cherche la longueur de la (ou
d’une) plus longue sous-suite commune des deux mots ainsi
qu’une telle sous-suite. On notera lcs(u, v) cette longueur.
Si u = professeur et v = dinosaure, osur est la sous-suite de
longueur maximale et donc lcs(u, v) vaut 4.
Si u = premier et v = dernier, rier et eier sont les deux sous-suites
de longueur maximale et donc lcs(u, v) vaut 4.
Si u = ruse et v = ourse, rse et use sont les deux sous-suites de
longueur maximale et donc lcs(u, v) vaut 3.
Analyse du problème
Notons LCS(i, j) la longueur maximale d’une sous-suite des
mots u1 ..ui -les i premières lettres de u- et v1 ..vj -les j premières
lettres de v. On a donc:
I
I
Les cas de base: LCS(i, 0) = 0 = LCS(0, j)
la récurrence:
I
I
si ui = vj , LCS(i, j) = 1 + LCS(i − 1, j − 1)
si ui 6= vj , LCS(i, j) = max(LCS(i − 1, j), LCS(i, j − 1))
Le cœur de l’algorithme
I
LCS(i, 0) = 0 = LCS(0, j), 0 ≤ i ≤ |u|, 0 ≤ j ≤ |v|
I
1 ≤ i ≤ |u|, 1 ≤ j ≤ |v|
si ui = vj LCS(i, j) = 1 + LCS(i − 1, j − 1)
sinon LCS(i, j) = max(LCS(i − 1, j), LCS(i, j − 1))
Version récursive naı̈ve
I
LCS(i, 0) = 0 = LCS(0, j), 0 ≤ i ≤ |u|, 0 ≤ j ≤ |v|
I
1 ≤ i ≤ |u|, 1 ≤ j ≤ |v|
si ui = vj LCS(i, j) = 1 + LCS(i − 1, j − 1)
sinon LCS(i, j) = max(LCS(i − 1, j), LCS(i, j − 1))
Input: u,v deux mots
Output: retourne LCS(i,j) pour u et v
int solvpart(int i, int j)
if ((i == 0)||(j == 0)) then
return 0;
else if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
return (1+solvpart(i-1, j-1));
else
return max(solvpart(i-1,j),solvpart(i, j-1));
Complexité de la version récursive naı̈ve?
Colmplexité de la version récursive naı̈ve?
Input: u,v deux mots
Output: retourne LCS(i,j) pour u et v
int solvpart(int i, int j)
if ((i == 0)||(j == 0)) then
return 0;
else if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
return (1+solvpart(i-1, j-1));
else
return max(solvpart(i-1,j),solvpart(i, j-1));
Dans le pire des cas, le nombre d’appels est au moins de l’ordre
de 2min(u.length(),v.length()) .
Dans le meilleur des cas, le nombre d’appels est
min(u.length(), v.length()).
Version dynamique itérative
I NITIALISATION
La complexité dans le pire des cas (en nombre d’appels) est au
moins de l’ordre de 2min(u.length(),v.length())
Le nombre d’appels différents est au plus (1 + |u|) ∗ (1 + |v|).
On est dans le cadre de la programmation dynamique!
On va utiliser une table à deux dimensions indexée par (i, j)
avec 0 ≤ i ≤ |u|, 0 ≤ j ≤ |v|.
I
LCS(i, 0) = 0 = LCS(0, j), 0 ≤ i ≤ |u|, 0 ≤ j ≤ |v|
I
1 ≤ i ≤ |u|, 1 ≤ j ≤ |v|
si ui = vj LCS(i, j) = 1 + LCS(i − 1, j − 1)
sinon LCS(i, j) = max(LCS(i − 1, j), LCS(i, j − 1))
int T[][]=new int[pb.u.length()+1][pb.v.length()+1];
//T[i][j] memorisera LCS(u[0..i-1],v[0,..j-1])
for (int i = 0; i ≤ pb.u.length(); i + +) do
T[i][0] = 0;
end
for (int j = 0; j ≤ pb.v.length(); j + +) do
T[0][j] = 0;
end
...
Version dynamique itérative
L A R ÉCURRENCE
I
LCS(i, 0) = 0 = LCS(0, j), 0 ≤ i ≤ |u|, 0 ≤ j ≤ |v|
I
1 ≤ i ≤ |u|, 1 ≤ j ≤ |v|
si ui = vj LCS(i, j) = 1 + LCS(i − 1, j − 1)
sinon LCS(i, j) = max(LCS(i − 1, j), LCS(i, j − 1))
...
for (int i = 1; i ≤ pb.u.length(); i + +) do
for (int j = 1; j ≤ pb.v.length(); j + +) do
if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
T[i][j] = 1 + T[i − 1][j − 1];
else
T[i][j] = max(T[i][j − 1], T[i − 1][j]);
end
end
...
Version dynamique itérative
...
for (int i = 0; i ≤ pb.u.length(); i + +) do
T[i][0] = 0;
end
for (int j = 0; j ≤ pb.v.length(); j + +) do
T[0][j] = 0;
end
for (int i = 1; i ≤ pb.u.length(); i + +) do
for (int j = 1; j ≤ pb.v.length(); j + +) do
if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
T[i][j] = 1 + T[i − 1][j − 1];
else
T[i][j] = max(T[i][j − 1], T[i − 1][j]);
end
end
return T[pb.u.length()][pb.v.length()] ;
Complexité en O(|u| ∗ |v|).
Version dynamique itérative
int T[][]=new int[pb.u.length()+1][pb.v.length()+1];
//T[i][j] memorisera LCS(u[0..i-1],v[0,..j-1])
for (int i = 0; i ≤ pb.u.length(); i + +) do
T[i][0] = 0;
end
for (int j = 0; j ≤ pb.v.length(); j + +) do
T[0][j] = 0;
end
for (int i = 1; i ≤ pb.u.length(); i + +) do
for (int j = 1; j ≤ pb.v.length(); j + +) do
if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
T[i][j] = 1 + T[i − 1][j − 1];
else
T[i][j] = max(T[i][j − 1], T[i − 1][j]);
end
end
return T[pb.u.length()][pb.v.length()] ;
La remontée, ou comment récupérer une sous-suite
commune de longueur maxi
Input: TCalc[i][j]=LCS(i,j) pour les valeurs “nécessaires”
String s =””; //s contiendra une sous-suite maxi
int i = pb.u.length(), j = pb.v.length();
//(i,j) représentent la “case courante”
// on part de la case “finale” et remonte jusqu’au cas de base
while ((i > 0)&&(j > 0)) do
if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
s = (pb.u.substring(i − 1, i)).concat(s);
i − −; j − −;
else if (T[i][j] == T[i − 1][j]) then
i − −;
else
j − −;
end
return s;
La remontée, ou comment récupérer une sous-suite
commune de longueur maxi
if (pb.u.charAt(i
− 1) == pb.v.charAt(j − 1)) then
.
...
else if (T[i][j] == T[i − 1][j]) then
i − −;
else
j − −;
ou
if (pb.u.charAt(i
− 1) == pb.v.charAt(j − 1)) then
.
...
else if (T[i][j] == T[i][j − 1]) then
j − −;
else
i − −;
Complexité de la version récursive dynamique
...
int solvpartDyn(int i, int j)
if ((i == 0)||(j == 0)) then
return 0;
if (T[i][j] == −1) then
if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
T[i][j]=1+solvpartDyn(i-1, j-1);
else
T[i][j]=max(solvpartDyn(i-1,j),solvpartDyn(i, j-1));
return T[i][j];
Un T[i][j] est calculé au plus une fois et génère au plus deux
appels à solvpart.
Le nombre d’appels est en O(|u| × |v|).
Remarque: si les mots sont identiques, le nombre d’appels est
en O(|u|).
Version récursive dynamique?
Version récursive ”naı̈ve” de solvpart(int i, int j)
if ((i == 0)||(j == 0)) then
return (0);
else if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
return (1+solvpart(i-1, j-1));
else
return ((solvpart(i-1,j),solvpart(i, j-1)));
Version récursive dynamique , solvpartDyn(int i, int j)
T[][]=new int[pb.u.length()+1][pb.v.length()+1] initialisée à -1
if ((i == 0)||(j == 0)) then
return 0;
if (T[i][j] == −1) then
if (pb.u.charAt(i − 1) == pb.v.charAt(j − 1)) then
T[i][j]=1+solvpartDyn(i-1, j-1);
else
T[i][j]=max(solvpartDyn(i-1,j),solvpartDyn(i, j-1));
return T[i][j];
U N DERNIER EXEMPLE :
LE PARTAGE
Le but est de diviser l’intervalle [0, m] en k morceaux en
minimisant le coût du découpage.
Le coût du découpage est ici la somme des carrés des
longueurs des morceaux: si les longueurs des morceaux sont
P
y1 , ...yk , on veut minimiser ki=1 y2i .
On impose la contrainte que les découpes soient effectuées à
des endroits ”prédécoupés” 0 < x1 . . . < xn < m.
LE CO ÛT ?
F ORMALISATION DU PROBL ÈME
Entrée:
Le but est de diviser l’intervalle [0, m] en k morceaux en
minimisant le coût du découpage. Le coût du découpage est ici
la somme des carrés des longueurs des morceaux: si les
longueurs des morceaux sont y1 , ...yk , on veut minimiser
Pk
2
i=1 yi .
Si il n’y a aucune contrainte, il suffit de découper en k parties
égales. Pourquoi?
En minimisant la somme des carrés, on exprime qu’on veut
”équilibrer” les morceaux.
PARENTH ÈSE :
LES PROBL ÈMES D ’ OPTIMISATION
m, entier, la taille de l’intervalle
k, entier, le nombre de morceaux
souhaités
n >= k − 1, entier, le nombre de
prédécoupes
0 < x1 . . . < xn < m les prédécoupes.
Sortie:
k − 1 points parmi les n points xi ,
xi1 < xi2 < . . . < xik−1 ,
qui minimisent le coût du découpage
associé -la fonction objectif-soit
Pk
2
j=1 (xij − xij−1 ) .
(On pose xi0 = 0, xik = m)
D ÉCOMPOSITION DU PROBL ÈME : L ES CAS SIMPLES ?
On est dans un cadre classique: celui des problèmes
d’optimisation.
On cherche une solution correcte - ici un découpage qui
respecte les contraintes- qui minimise un coût (maximise un
bénéfice).
n
Ici, combien de solutions possibles? k−1
(=Cnk−1 )
p
Ce nombre peut être très grand; par exemple, 2p
p ≥2
k = 1 Le coût est alors m2 .
n = k − 1 On n’a pas le choix.
C OMMENT CONSTRUIRE UNE SOLUTION
Une solution est souvent une suite de choix: par exemple, ici,
on choisit un xi qui sera la découpe la plus à droite: il reste
ensuite à découper à gauche du xi en k − 1 parties.
On peut a priori choisir n’importe quel xi avec k − 1 ≤ i ≤ n.
Remarque fondamentale: Soit un découpage optimal en k
parties dont le dernier élément est xi : il donne un découpage
optimal en k − 1 parties de [0, xi ];
C’est le principe d’optimalité.
La solution optimale d’un problème s’exprime en fonction de
solution optimale de sous problèmes.
C OMMENT CONSTRUIRE UNE SOLUTION
Donc un sous-problème est défini par deux paramètres i et j.
P(i, j) est le découpage optimal de [0, xi ] en j éléments, avec:
1i ≤ n + 1 -on pose x0 = 0, xn+1 = n.
1≤j≤k
i ≤ j - il faut avoir assez de points de découpages!
On a donc:
P(i, 1) = x2i
P(i, j) = Minj−1<=l<i (P(l, j − 1) + (xi − xl )2 ) si 2 ≤ j ≤ i
La valeur recherchée est P(n + 1, k) (on rajoute xn1 = m)
E XEMPLE
Le cœur de l’algorithme
P(i, 1) = x2i
P(i, j) = Minj−1<=l<i (P(l, j − 1) + (xi − xl )2 ) pour 2 ≤ j ≤ i
2 4 5 9 10
1 4 16 25 81 100
2
8 13 41 50
3
9 29 38
8 = 4 + (4 − 2)2
13 = min(4 + 32 , 16 + 12 )
41 = min(4 + 72 , 16 + 52 , 25 + 42 )
50 = min(4 + 82 , 16 + 62 , 25 + 52 , 81 + 1)
......
D’ O Ù L’ ALGORITHME
P(i, 1) = x2i
P(i, j) = Minj−1<=l<i (P(l, j − 1) + (xi − xl )2 ), 2 ≤ j ≤ i
int P[][]=new int[n+2][k+1];
for (int i = 1; i <= n + 1; j + +) do
P[i][1]= x[i]2 ;
for (int j = 2; (j ≤ k)&&(j ≤ i); j + +) do
P[i][j]=MaxInt;
for (int l = j − 1; l < i; l + +) do
P[i][j]=min(P[i][j], P[l][j − 1] + (x[i] − x[l])2 );
end
end
end
return P[n+1][k];
C OMMENT SAVOIR O Ù D ÉCOUPER ?
2 4
5
9
10
1 44 16 25 81 100
2
8 1313 41 50
3
9
29 3838
38 = 13 + 52
13 = 4 + 32
C OMPLEXIT É DE L’ ALGORITHME :
int P[][]=new int[n+2][k+1];
for (int i = 1; i <= n + 1; j + +) do
P[i][1]= x[i]2 ;
for (int j = 2; (j ≤ k)&&(j ≤ i); j + +) do
P[i][j]=MaxInt;
for (int l = j − 1; l < i; l + +) do
P[i][j]=min(P[i][j], P[l][j − 1] + (x[i] − x[l])2 );
end
end
end
return P[n+1][k];
Complexité en O(n2 × k)
C OMMENT SAVOIR O Ù D ÉCOUPER ?
for (int i = 1; i <= n + 1; j + +) do
P[i][1]= x[i]2 ;
for (int j = 2; (j ≤ k)&&(j ≤ i); j + +) do
P[i][j]=MaxInt;
for (int l = j − 1; l < i; l + +) do
if (P[i][j] > P[l][j − 1] + (x[i] − x[l])2 ) then
P[i][j] = P[l][j − 1] + (x[i] − x[l])2 ;
Dec[i][j] = l;
end
end
end
//remontée
int i = n + 1;
for (int j = k; j > 1; j − −) do
découper en Dec[i][j]; i=Dec[i][j];
end
L A PROCHAINE FOIS
I
Diviser pour régner
I
Programmation Dynamique
I
Algorithmes Gloutons