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