Bonjour,
Dans la leçon précédente, nous avons disséqué la logique du problème du sac à dos 0/1 pour aboutir à sa relation de récurrence. Vous avez compris que pour chaque objet, la décision optimale se résume à comparer la meilleure solution sans cet objet avec la meilleure solution avec cet objet, si sa capacité le permet.
Aujourd'hui, nous allons mettre cette théorie en pratique. L'objectif est de transformer cette relation récursive en un algorithme itératif efficace, une technique que vous maîtrisez : la tabulation. Nous allons donc implémenter cette solution en Python, puis nous analyserons rigoureusement sa complexité en temps et en espace.
1. De la Récurrence à la Table de Programmation Dynamique
Pour rappel, la relation de récurrence que nous avons établie est la suivante. Si nous notons la valeur maximale que l'on peut obtenir en utilisant les premiers objets avec une capacité de sac de , alors :
où et sont le poids et la valeur de l'objet .
L'approche par tabulation (ou bottom-up) consiste à construire une table, ou une matrice, pour stocker les solutions des sous-problèmes.
- Les lignes de la table correspondront aux objets considérés (de 0 à ).
- Les colonnes correspondront aux capacités du sac (de 0 à ).
La cellule K[i][w] de la table contiendra donc la valeur de . Nous remplirons cette table itérativement, en partant des cas de base (K[0][w] = 0 et K[i][0] = 0) jusqu'à atteindre la cellule K[n][W], qui contiendra notre solution finale.
L'image ci-dessous illustre la structure de cette table.

Pour voir ce processus de remplissage en action, la vidéo suivante est un excellent support. Elle détaille, case par case, comment la table est construite.
4.5 0/1 Knapsack - Two Methods - Dynamic Programming
Regardez cette vidéo d'Abdul Bari qui explique pas à pas comment remplir la table de programmation dynamique. Concentrez-vous sur la logique de calcul de chaque cellule à partir des précédentes.
Regardez de 02:24 à 15:39. La première partie (jusqu'à 05:43) explique la mise en place de la table. La partie suivante (jusqu'à 15:39) montre le remplissage effectif, en introduisant et en appliquant la formule de récurrence. Observez bien comment chaque nouvelle valeur est le maximum entre 'ne pas prendre l'objet' (valeur de la case du dessus) et 'prendre l'objet' (valeur de l'objet + valeur d'une case précédente dans la ligne du dessus).
2. Implémentation de l'Algorithme et Analyse de Complexité
Maintenant que le principe de la tabulation est clair, nous pouvons passer à l'implémentation. Le document suivant, issu d'un support de préparation français, propose une implémentation Python très directe.
Programmation Dynamique - Problème du Sac à Dos
Ce document contient une implémentation Python claire et concise de l'algorithme de tabulation pour le sac à dos. Nous allons nous baser sur celle-ci.
Étudiez la section 2.4, qui formalise la relation de récurrence, puis la section 2.5, qui présente l'implémentation en Python (Listing 1). Analysez la structure du code : l'initialisation de la table K, les deux boucles for imbriquées et la manière dont la récurrence est traduite en une condition if/else.
Le code que vous venez d'étudier se compose de deux boucles imbriquées. Analysons sa complexité :
- Complexité temporelle : La boucle externe itère sur les objets, de 1 à . La boucle interne itère sur les capacités, de 1 à . Le corps de la boucle effectue des opérations en temps constant. La complexité temporelle est donc de .
- Complexité spatiale : Nous stockons une table de taille . La complexité spatiale est donc également de .
Une remarque importante pour l'Agrégation : cette complexité est qualifiée de pseudo-polynomiale. Bien qu'elle semble polynomiale en et , elle dépend de la valeur de . Si est très grand (par exemple, de l'ordre de ), l'algorithme devient exponentiel par rapport à la taille de l'entrée en binaire.
3. Reconstruire la Solution
L'algorithme nous donne la valeur maximale, mais ne nous dit pas quels objets ont été choisis. Pour le savoir, il faut "remonter" la table à partir de la dernière case K[n][W].
L'idée est simple : pour chaque objet (en partant de ), si K[i][w] != K[i-1][w], cela signifie que la valeur a changé à l'étape , et donc que l'objet a été inclus dans la solution optimale pour cette capacité. On l'ajoute à notre liste de solution, on décrémente la capacité de son poids, et on continue à l'étape .
Le document Programmation Dynamique - Problème du Sac à Dos (ressource LINK) que vous avez consulté précédemment contient, dans sa section 2.6 (Listing 2), une version modifiée du code qui implémente cette logique de reconstruction. Jetez-y un œil pour voir comment cela se traduit concrètement.
4. Optimisation de la Complexité Spatiale
Une complexité spatiale en peut être problématique si est grand. Observons notre récurrence : pour calculer la ligne , nous n'avons besoin que des informations de la ligne . Il est donc inutile de conserver toute la table en mémoire.
Optimisation à 2 lignes :
On peut réduire l'espace à en n'utilisant que deux tableaux d'_ taille : un pour la ligne i-1 (précédente) et un pour la ligne i (actuelle).
Optimisation à 1 ligne :
On peut pousser l'optimisation encore plus loin et n'utiliser qu'un seul tableau de taille . L'astuce est de parcourir la boucle interne (sur les capacités ) à l'envers, de vers le poids de l'objet courant. Ceci garantit que lorsqu'on calcule K[w], la valeur K[w - p_i] que l'on utilise est bien celle de l'itération précédente (la ligne ) et non celle que l'on viendrait de mettre à jour à l'itération actuelle.
Cette technique est subtile mais très classique. La vidéo suivante l'explique de manière exceptionnellement claire.
DP 19. 0/1 Knapsack | Recursion to Single Array Space Optimised Approach | DP on Subsequences
Cette vidéo explique comment passer de la solution tabulée à une version optimisée en espace utilisant un seul tableau 1D. La démonstration est très visuelle et met en lumière le point crucial de l'itération inversée.
Regardez la partie sur l'optimisation de l'espace de 32:11 à 41:09. Suivez attentivement les étapes : L'idée de passer de 2D à deux tableaux 1D (previous et current) (32:11 - 33:35). La transition vers un seul tableau 1D et pourquoi la boucle interne doit être parcourue de droite à gauche (de max_weight vers 0) pour ne pas utiliser des valeurs déjà mises à jour dans la même itération (33:35 - 40:00). La démonstration finale du code optimisé (40:00 - 41:09).
Avec cette optimisation, la complexité spatiale est réduite à , tandis que la complexité temporelle reste .
Conclusion
Aujourd'hui, vous avez accompli une étape fondamentale en passant de la théorie à la pratique.
Points clés à retenir :
- La solution par tabulation pour le sac à dos consiste à remplir une table de manière itérative, en appliquant la relation de récurrence à chaque cellule.
- L'algorithme de base a une complexité temporelle et spatiale de . C'est une complexité dite pseudo-polynomiale.
- Il est possible (et souvent attendu) d'optimiser la complexité spatiale à en observant que le calcul de chaque ligne ne dépend que de la précédente.
- L'optimisation la plus élégante n'utilise qu'un seul tableau et requiert une itération inversée sur les capacités pour garantir la correction.
Pour un concours comme l'Agrégation, il ne suffit pas de savoir implémenter un algorithme. Il faut aussi être capable de prouver qu'il est correct. Dans notre prochaine leçon, nous nous attacherons à cet aspect formel en rédigeant une preuve de correction par récurrence pour l'algorithme du sac à dos, une compétence essentielle pour démontrer la profondeur de votre compréhension.