• Aucun résultat trouvé

Figure 2.8 – Arbre d’appels pour le calcul de p(n,k). En notant G/D les branches gauche/droite issues d’un nœud (correspondant respectivement aux diagrammes de type 1 et de type 2), on remarque que les branches GDDG et DGGD, si elles existent, mènent toujours aux mêmes appels.

La figure2.9montre un exemple concrêt d’arbre d’appels avec des calculs inutiles.

(10,4)

Figure 2.9 – Arbre d’appels pour le calcul de p(22,6) avec ses 348 nœuds, ses 136 =p(22,6) feuilles et ses calculs inutiles. Par exemple, l’appel(10,4) (et son sous-arbre), apparaît deux fois et à distance quatre de la racine. Dans cette illustration, la racine est en bas et que l’ordre des fils n’est pas respecté.

2.5 Programmation dynamique

La programmation dynamique est l’implémentation améliorée de la version récur-sive d’un algorithme. Au lieu de faire des appels récursifs, on utilise la mémorisation qui économise ainsi des calculs (et du temps) au détriment de l’espace mémoire. On utilise une table où les valeurs sont ainsi calculées « dynamiquement » en fonction des précédentes.

Appliquée à un problème, la programmation dynamique consiste donc à trouver un

66 CHAPITRE 2. PARTITION D’UN ENTIER algorithme basé sur une récurrence puis de l’implémenter en utilisant la technique de mémorisation pour supprimer les calculs inutiles. Résoudre un problème par program-mation dynamique c’est donc procéder à ces deux étapes de réflexions.

Pour notre problème de partition, l’étape de la formule de récurrence est déjà fait, c’est l’équation (2.3). Reste d’étape de mémorisation. Calculons toutes les valeurs néces-saires aux calculs dep(n) présentées sous la forme d’une table similaire à la table2.1.

p(n, k) k total

n 1 2 3 4 5 6 7 8 p(n)

1 1 1

2 1 1 2

3 1 1 1 3

4 1 2 1 1 5

5 1 2 2 1 1 7

6 1 3 3 2 1 1 11

7 1 3 4 3 2 1 1 15

8 1 4 5 5 3 2 1 1 22

Table2.1 – Le calcul de la lignep(n,·) se fait à partir des lignes précédentes.

Icip(8,3) =p(7,2)+p(5,3), et en bleu toutes les valeurs utilisées dans son cal-cul. On remarque aussi que le calcule dep(n, k) se fait non seulement à partir des lignes < n et mais aussi des colonnes 6 k. [Exercice. Construire l’arbre des appels pour (8,3) afin de retrouver les coordonnées (ligne,colonne) des valeurs en bleu de la table.]

Pour notre programme, on va donc utiliser une tableP[n][k]similaire à la table2.1 que l’on va remplir progressivement grâce à la formule de récurrence. Pour simplifier, dans la tablePon n’utilisera pas l’indice 0, si bien queP[n][k]va correspondre àp(n, k).

Toute la difficulté est de parcourir la table dans un ordre permettant de calculer chaque élément en fonction de ceux précédemment calculés.

long p_prog_dyn(int n){

long P[n+1][n+1],s=0; // indices 0,1,...,n int i,k;

for(i=1;i<=n;i++){ // pour chaque ligne i P[i][1]=1; // cas 1

for(k=2;k<=i;k++) P[i][k] = P[i-1][k-1]; // cas 2 & début cas 3 for(k=2;k<=i/2;k++) P[i][k] += P[i-k][k]; // fin cas 3

}

for(k=1;k<=n;k++) s += P[n][k]; // calcule la somme return s;

}

2.5. PROGRAMMATION DYNAMIQUE 67 Dans ce code on retrouve les trois cas de l’équation (2.3), mais les cas 2 et 3 ne sont en partie factorisés : on commence par le cas 2 et partiellement le cas 3. Puis on complète le cas 3 seulement lorsqu’il se produit, c’est-à-dire lorsquenk>kk6n/2. Car pour être ajouté àP[n][k], il que fautP[n-k][k]existe.

Complexité. Dans la boucle principale for(i...), il y a deux boucles for(k...) cha-cune de complexité O(i). Cela fait donc un total de Pn

i=1O(i) 6cPn

i=1i = O(n2) pour une certaine constantec >0. La dernière bouclefor(k...)enO(n) pour le calcul de la somme des p(n, k) ne change pas cette complexité. La complexité en nombre d’opéra-tions arithmétiques dep_prog_dyn()est doncO(n2). C’est aussi la complexité en temps si les résultats des calculs tiennent sur desint, c’est-à-dire sinn’est pas trop grand.

[Exercice. Donnez une variante de p_prog_dyn() permettant de calculer p(n, k) en tempsO(nk), en supposant toujours quen n’est pas trop grand.] [Exercice. De combien de lignes de la table a-t-on vraiment besoin pour le calcul de la dernière lignep(n,·) ? En déduire une implémentation moins gourmande en mémoire et toujours de complexité O(n2) (en opérations arithmétiques).]

Parenthèse.Le code dep_prog_dyn()utilise la déclarationlong P[n+1][n+1]sans allo-cation mémoire (malloc()). Mais quelle est la différence entre

long A[n];et

long *B=malloc(n*sizeof(*B));

qui dans les deux cas déclarent un tableau denentiers longs7? Certes dans les deux cas, les déclarations sont locales à la fonction qui les déclarent, c’est-à-dire que niAniBn’existent en dehors de leur bloc de déclaration. Mais les différences sont :

A[]est stocké sur la pile, comme toutes les autres variables locales. Cette zone mé-moire est allouée dynamiquement (donc lors de l’exécution) à l’entrée de la fonction, à l’aide d’une simple manipulation du pointeur de pile (une simple addition ou sous-traction). Puis elle est libérée à la sortie de la fonction avec la manipulation inverse du pointeur la pile. Il n’y a pas defree(A)à faire ; il ne faut surtout pas le faire d’ailleurs.

B[]est stocké sur letas, une zone de mémoire permanente, différente de la pile, où sont stockées aussi les variables globales. Elle n’est pas automatiquement libérée à la sortie de la fonction. Cependant les valeurs stockées dansB[]sont préservées à la sortie de la fonction. Il faut explicitement faire unfree(B)si ont veut libérer cette zone mémoire.

Dans les deux cas, même après libération et sortie de la fonction, les valeurs stockées dans les tableaux ne sont pas spécialement effacées. Mais la zone mémoire (de la pile ou du tas) est libre d’être réallouée par la suite du programme (ou l’exécution d’autres programmes), et donc perdue pour l’utilisateur.

À première vue, la déclaration deA[]peut paraître plus simple pour un tableau local puisqu’aucune libération explicite avecfree(A)n’est nécessaire. Elle est aussi plus efficace qu’unmalloc()qui généralement fait appel au système d’exploitation, le gestionnaire de 7. Notez quesizeof(*B)est la taille en octets du type de*B, soit ce qui est pointé parB, icilong. De manière générale, lorsqu’on écritP=malloc(...),Pest bien évidemment un pointeur, et la taille du mallocdoit dépendre de la taille de ce qui est pointé parP, ce qui est en principe toujourssizeof(*P).

68 CHAPITRE 2. PARTITION D’UN ENTIER mémoire. Cependant, la pile est une zone mémoire beaucoup plus limitée que le tas (typi-quement 8192 Ko vs. 4 Go voir beaucoup plus). Sinest trop grand, on arrive vite au fameux stack overflow.

Cette taille peut être donnée par des commandes systèmes comme ulimit(options -s pour la pile,-mpour la mémoire) pour les OS à base Unix (BSD). Il existe aussi des com-mandesCcorrespondantes, voirman -s3 ulimit, mais c’est système-dépendant.

Plus rapide encore. Il existe d’autres formules de récurrence donnant des calculs en-core plus performants. Attention ! Il faut poserp(0) = 1 etp(n) = 0 sin <0. Par exemple, celle connue d’Euler il y a plus de 250 ans,

p(n) = (p(n−1) +p(n−2))− (p(n−5) +p(n−7)) + (p(n−12) +p(n−15))− (p(n−22) +p(n−26)) +

· · ·

où les entiers 1,2,5,7,12,15, . . . sont les nombres pentagonaux d’Euler, des entiers de la formei·(3i±1)/2. La formule de récurrence s’exprime donc comme :

p(n) = X

i>1

(−1)i1(p(n−i·(3i−1)/2) +p(ni·(3i+ 1)/2)) .

Il faut bien sûr que l’argumentni·(3i±1)/2>0 (rappelons quep(0) = 1). On en déduit alors une valeur maximum pouri et que la somme comprend 2

2n/3 termes environ.

C’est le nombre de branchements de l’arbre des appels.

[Exercice. Quelle serait la complexité de la fonction récursive résultant de cette for-mule ? En utilisant la programmation dynamique, et donc une table, que devient alors sa complexité ?]

[Exercice. Considérons la fonctionk(n)décrite page 19. Montrez qu’il y a des calculs inutiles. Proposez une solution par programmation dynamique.]

Parenthèse.En, une autre récurrence a été découverte ([Ewe04]) : p(n) = X

i>0

p(n/4−i·(i+ 1)/8) + 2X

i>1

(−1)i1p(n−2i2). Le nombre de branchements est d’environ2

n/2, soit un peu mieux que celle d’Euler, mais cela reste du même ordre.

En, un algorithme de complexité en temps enO(

nlog4+o(1)n)a été mise au point par [Joh12]. C’est plus rapide est les méthodes précédentes basées sur les récurrences et la programmation dynamique, et c’est surtout proche de l’optimal.[Question. Pourquoi ?]

Pour les pationnés, il y a une très bonne vidéo (cf. la figure 2.10) qui ré-explique les différentes formules liées aux partitions d’un entier.