Problème
Le problème est le suivant : on dispose d'un grand sac de monnaie (de billets, ou de ce que vous voulez). L'objectif est de diviser le sac en deux sacs plus petits contenant les sommes les plus rapprochées possibles.
L'approche la plus « simple » serait d'essayer toutes les combinaisons et d'en retenir la meilleure. Néanmoins, après un petit calcul, on s'aperçoit que cela n'est pas très réaliste. En effet, supposons que nous avons 100 pièces, il existe alors 2
100 combinaisons possibles. Il faut donc obligatoirement trouver une meilleure solution.
Méthode
C'est là qu'intervient le concept de programmation dynamique. Pour connaître toutes les combinaisons possibles, il existe un autre moyen : traiter le problème « à l'envers ».
Prenons un cas concret.
Dans un sac de pièces, nous avons :
Code : Autre1
| PIÈCES : 5, 9, 3, 8, 2, 5 |
Nous allons rechercher une sous-liste de pièces optimale dont la somme se rapprochera le plus possible de la moitié de la somme de toutes les pièces. Raisonnons :
1. Avec la première pièce, quel est l'assemblage possible ?
2. Et avec les deux premières pièces, quels sont les assemblages possibles ? Quelle est la meilleure solution ?
3. Et ainsi de suite jusqu'à
n pièces...
Code : Autre1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| ÉTAPE 1. avec 5 :
- COMBINAISONS CONNUES : néant.
- PIÈCE À ÉVALUER : 5.
- NOUVELLES COMBINAISONS : 5.
ÉTAPE 2. avec 5 et 9 :
- COMBINAISONS CONNUES : 5.
- PIÈCE À ÉVALUER : 9.
- NOUVELLES COMBINAISONS : 9, 14 (9+5).
ÉTAPE 3. avec 5, 9 et 3 :
- COMBINAISONS CONNUES : 5, 9, 14.
- PIÈCE À ÉVALUER : 3.
- NOUVELLES COMBINAISONS : 3, 8 (3+5), 12 (3+9)...
ÉTAPE 4. avec 5, 9, 3 et 8 :
- COMBINAISONS CONNUES : 3, 5, 8, 9, 12, 14.
- PIÈCE À ÉVALUER : 8.
- NOUVELLES COMBINAISONS : 11 (3+8), 13 (5+8), 16 (8+8)...
On a trouvé 16, alors on s'arrête ! |
En pratique, on peut connaître les assemblages possibles avec deux pièces à partir des assemblages possibles avec une pièce, et ainsi de suite. C'est ce que l'on appelle une relation de récurrence : la solution du problème sur
n instances est récurrente à la solution du problème sur
n-1 instances.
On constate que, pour travailler, le programme a besoin de connaître les données déjà trouvées. C'est le fondement même de la programmation dynamique : on élimine les recalculs en mémorisant ce qui a déjà été calculé.
C'est ce qui fait l'efficacité de cette méthode, mais c'est aussi son point faible :
la programmation dynamique requiert de la mémoire et ne sera pas toujours envisageable à cause de cela.
Et concrètement, on fait comment ?
Mise en oeuvre
Passons maintenant à l'implémentation de tout cela.
Nous allons devoir créer une matrice booléenne M de dimensions nombrePieces * (miSommeGlobale+1), que nous appellerons M[i][j].
Dans le cas où sommeGlobale est paire, miSommeGlobale vaut sommeGlobale/2. Dans le cas inverse, on prendra (sommeGlobale-1)/2.
Code : Autre1
2
3
4
5
6
7
8
9
| DEFINIT sommePieces, miSommeGlobale
sommePieces = somme(listePieces)
SI sommePieces EST PAIRE
miSommeGlobale = sommePieces/2
SINON
miSommeGlobale = (sommePieces-1)/2
DEFINIT M[nombrePieces][miSommeGlobale+1] |
Puis on remplit la première ligne comme ceci :
Code : Autre1
2
3
4
5
6
7
| M[0][0] = VRAI
POUR j=1 TANT QUE j < miSommeGlobale
SI piece[0] EGALE j
M[0][j] = VRAI
SINON
M[0][j] = FAUX
incrémente j |
Ce qui nous donne :
| i\j | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|
| 1re pièce |
V |
F |
F |
F |
F |
V |
F |
F |
F |
F |
F |
F |
F |
F |
F |
F |
F |
Cette ligne représente les combinaisons possibles avec la première pièce.
On peut avoir soit 0 avec zéro pièces, soit 5 avec une pièce.
Ensuite, nous allons remplir les autres lignes du tableau (ligne par ligne) en suivant la condition suivante :
Code : Autre1
2
3
4
| M[i][j] EST VRAI SI
- M[i-1][j] EST VRAI
OU
- piece[i] <= j ET M[i-1][j-piece[i]] EST VRAI |
Ce qui nous donne bien :
| i\j | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|
| 1re pièce |
V |
F |
F |
F |
F |
V |
F |
F |
F |
F |
F |
F |
F |
F |
F |
F |
F |
| 2 premières pièces |
V |
F |
F |
F |
F |
V |
F |
F |
F |
V |
F |
F |
F |
F |
V |
F |
F |
| 3 premières pièces |
V |
F |
F |
V |
F |
V |
F |
F |
V |
V |
F |
F |
V |
F |
V |
F |
F |
| 4 premières pièces |
V |
F |
F |
V |
F |
V |
F |
F |
V |
V |
F |
V |
V |
V |
V |
F |
V |
Chaque ligne représente les combinaisons possibles avec les i premières pièces.
Dans ce cas précis, on s'arrête à la ligne 4 car la solution idéale a été trouvée.
Sinon, on continuera à remplir le tableau jusqu'à la dernière ligne, et nous connaîtrons de ce fait la solution optimale : la valeur maximale de la dernière ligne du tableau représente l'une des deux sous-sommes optimales (la plus petite), l'autre sous-somme optimale étant alors la somme globale moins cette valeur.
Code : Autre1
2
3
4
5
6
7
8
9
| DEFINIT sousListe1, sousListe2, ecart
i = nombrePieces, j = miSommeGlobale + 1
TANT QUE M[i][j] EST FAUX
décrémente j
sousListe1 = j
sousListe2 = sommePieces - j
ecart = sommePieces-(j*2) |
Et si je veux récupérer la sous-liste en question, y a-t-il un moyen ?
Eh bien en fait, si tu as gardé le tableau ci-dessus, je pense qu'il doit y avoir un moyen.
Récupérer la sous-liste
Nous allons récupérer la sous-liste obtenue en suivant le procédé à l'envers.
Analysons le tableau :
- En M[3][16], nous avons le résultat optimal. Or le quatrième nombre (piece[3]) est 8.
- Passons alors en M[2][16-8] ; à nouveau, le résultat est optimal (la case au-dessus est
false). De plus, nous savons que le troisième nombre (piece[2]) est 3.
- On passe à M[1][8-3] ; le résultat n'est pas optimal.
- Puis on passe à M[0][5] ; le résultat est optimal, donc on ajoute le premier nombre (piece[0]) à notre sous-liste.
- On retrouve bien notre sous-liste optimale : 8, 3 et 5.
Algorithme
Une fois que l'on a trouvé la sous-liste optimale, on fait :
Code : Autre1
2
3
4
5
6
7
| TANT QUE j > 0
TANT QUE i > 0 ET M[i-1][j] EST VRAI
décrémente i
j = j - piece[i]
SI j > 0
Ajoute-a-petite-sous-liste ( piece[i] )
décrémente i |