Exercice : Optimisation combinatoire et Programmation Dynamique
🌐 Énoncé du problème / Cadre d’étude
Section titled “🌐 Énoncé du problème / Cadre d’étude”Dans le cadre de la conception d’un système de téléportation quantique interplanétaire, on s’intéresse à l’optimisation du transport d’échantillons minéraux rares. Chaque échantillon possède un poids entier et une valeur scientifique entière. Le vaisseau dispose d’une capacité de charge maximale limitée.
Parallèlement, le système de communication embarqué doit comparer des séquences de données brutes reçues avec des séquences de référence pour détecter d’éventuelles corruption de bits dues aux rayonnements cosmiques. Enfin, la station de contrôle au sol gère un automate de distribution de modules d’énergie utilisant un système monétaire particulier.
Ce problème se propose d’étudier trois classiques de la programmation dynamique à travers ces applications :
- Le problème du sac à dos (optimisation sous contrainte de poids).
- L’alignement de séquences (distance de Levenshtein / distance d’édition).
- Le rendu de monnaie (recherche du nombre minimal de pièces).
Toutes les fonctions demandées devront être écrites en langage Python 3. On portera une attention particulière à la complexité temporelle et spatiale des algorithmes proposés.
❓ Questions progressives
Section titled “❓ Questions progressives”Partie I : Le problème du sac à dos 0-1
Section titled “Partie I : Le problème du sac à dos 0-1”On dispose de objets. Chaque objet (pour ) a un poids et une valeur . On dispose d’un sac à dos pouvant supporter un poids maximal .
- (Modélisation) Soit la valeur maximale que l’on peut obtenir en considérant un sous-ensemble des premiers objets (objets 0 à ) avec une capacité de sac restante de . Établir la relation de récurrence vérifiée par .
- (Implémentation récursive avec mémoïsation) Écrire une fonction Python
sac_a_dos_memo(poids, valeurs, W)qui renvoie la valeur maximale atteignable. Préciser sa complexité. - (Programmation dynamique bottom-up) Écrire une fonction Python
sac_a_dos_it(poids, valeurs, W)utilisant un tableau itératif. Comment peut-on optimiser l’espace mémoire si l’on souhaite seulement connaître la valeur optimale (et non la composition du sac) ?
Partie II : Alignement de séquences (Distance d’édition)
Section titled “Partie II : Alignement de séquences (Distance d’édition)”Pour comparer deux chaînes de caractères et de longueurs respectives et , on définit la distance d’édition (ou distance de Levenshtein) comme le nombre minimal d’opérations élémentaires (insertion, suppression, substitution d’un caractère) pour transformer en .
- (Récurrence) Soit la distance d’édition entre le préfixe de de longueur et le préfixe de de longueur . Écrire la relation de récurrence pour en définissant explicitement le coût des opérations.
- (Code Python) Écrire une fonction Python
distance_edition(A, B)de complexité temporelle et spatiale (ou optimisée en ).
Partie III : Rendu de monnaie minimal
Section titled “Partie III : Rendu de monnaie minimal”- (Application connexe) On dispose d’un ensemble de pièces de monnaie de valeurs et d’une somme totale . Écrire une fonction
rendu_monnaie(C, S)qui renvoie le nombre minimal de pièces nécessaire pour obtenir la somme (en supposant la solution toujours possible, avec par exemple ).
💡 Pistes & Indications méthodologiques
Section titled “💡 Pistes & Indications méthodologiques”- Programmation dynamique : Identifiez toujours les deux piliers : la sous-structure optimale (la solution d’un grand problème dépend de la solution de sous-problèmes similaires) et le creusement de sous-problèmes redondants (nécessitant mémoïsation ou tabulation).
- Gestion des indices : Soyez rigoureux sur les dimensions des tableaux en Python (
len(A) + 1). - Complexité : Pour les questions de programmation dynamique tabulaire, la complexité est généralement le nombre de cases de la table multiplié par le coût du calcul d’une case (souvent ).
📋 Corrigé détaillé pas à pas
Section titled “📋 Corrigé détaillé pas à pas”Partie I : Le problème du sac à dos 0-1
Section titled “Partie I : Le problème du sac à dos 0-1”-
Relation de récurrence : Pour l’objet (le -ème objet, d’indice ), deux cas se présentent :
- Si son poids dépasse la capacité courante (), on ne peut pas l’inclure :
- S’il peut être inclus (), on choisit le maximum entre ne pas le prendre et le prendre : Avec les conditions initiales : pour tout et pour tout .
-
Version récursive avec mémoïsation :
def sac_a_dos_memo(poids, valeurs, W):n = len(poids)memo = {}def dp(i, w):if i == 0 or w == 0:return 0if (i, w) in memo:return memo[(i, w)]# Cas où l'on ne peut pas prendre l'objet i-1res = dp(i - 1, w)# Cas où l'on peut potentiellement le prendreif poids[i - 1] <= w:res = max(res, valeurs[i - 1] + dp(i - 1, w - poids[i - 1]))memo[(i, w)] = resreturn resreturn dp(n, W)Complexité : Temporelle en car il y a états distincts. Spatiale en pour la table de mémoïsation et la pile d’appels récursifs.
-
Version itérative (Bottom-Up) :
def sac_a_dos_it(poids, valeurs, W):n = len(poids)# Table de dimensions (n + 1) x (W + 1) initialisée à 0dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)]for i in range(1, n + 1):for w in range(1, W + 1):dp[i][w] = dp[i - 1][w]if poids[i - 1] <= w:val_avec = valeurs[i - 1] + dp[i - 1][w - poids[i - 1]]if val_avec > dp[i][w]:dp[i][w] = val_avecreturn dp[n][W]Optimisation spatiale : On observe que pour calculer la ligne , on a uniquement besoin de la ligne . On peut réduire l’espace à en utilisant deux lignes (ou même une seule ligne parcourue de droite à gauche).
Partie II : Alignement de séquences (Distance d’édition)
Section titled “Partie II : Alignement de séquences (Distance d’édition)”-
Relation de récurrence : Soient et . Si , aucun coût supplémentaire : . Sinon, on prend le minimum entre :
- Suppression dans :
- Insertion dans (ou suppression dans ) :
- Substitution de par :
Donc :
D(i-1, j-1) & \text{si } A[i-1] = B[j-1] \\ 1 + \min\left(D(i-1, j), D(i, j-1), D(i-1, j-1)\right) & \text{sinon} \end{cases}$$ -
Implémentation Python :
def distance_edition(A, B):m, n = len(A), len(B)# Tableau (m + 1) x (n + 1)dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]# Conditions initialesfor i in range(m + 1):dp[i][0] = ifor j in range(n + 1):dp[0][j] = jfor i in range(1, m + 1):for j in range(1, n + 1):if A[i - 1] == B[j - 1]:dp[i][j] = dp[i - 1][j - 1]else:dp[i][j] = 1 + min(dp[i - 1][j], # Suppressiondp[i][j - 1], # Insertiondp[i - 1][j - 1] # Substitution)return dp[m][n]Complexité : Temporelle et spatiale .
Partie III : Rendu de monnaie minimal
Section titled “Partie III : Rendu de monnaie minimal”-
Implémentation Python : Soit le nombre minimal de pièces pour faire la somme . Avec .
def rendu_monnaie(C, S):# dp[s] stocke le nombre min de pièces pour la somme s# On initialise avec une valeur infini symbolique (S + 1)dp = [float('inf')] * (S + 1)dp[0] = 0for s in range(1, S + 1):for c in C:if s - c >= 0:dp[s] = min(dp[s], dp[s - c] + 1)return dp[S] if dp[S] != float('inf') else -1Complexité : Temporelle en où est le nombre de pièces différentes. Spatiale en .