Exercice : Le problème du changement d orientation optimal (Mémoïsation et Programmation Dynamique)
🌐 Énoncé du problème / Cadre d’étude
Section titled “🌐 Énoncé du problème / Cadre d’étude”On s’intéresse à un problème d’optimisation de parcours dans un réseau orienté sans cycles (DAG). Un étudiant en classes préparatoires souhaite planifier ses révisions pour chapitres numérotés de à .
Chaque chapitre nécessite un certain nombre de jours de travail et rapporte un score de maîtrise . Cependant, l’étudiant est soumis à des contraintes de prérequis : pour certains chapitres, d’autres doivent être entièrement maîtrisés au préalable. Le cursus est modélisé par un ensemble de règles de dépendance signifiant que le chapitre doit impérativement être validé avant d’entamer le chapitre .
On dispose d’un temps total imparti de jours. L’objectif est de trouver un sous-ensemble de chapitres à valider (respectant les prérequis) dont la somme des durées ne dépasse pas , et maximisant la somme des scores obtenus.
On formalise le problème ainsi :
- Soit l’ensemble des chapitres validés.
- La contrainte de temps s’écrit : .
- La contrainte de prérequis s’écrit : si est une règle et , alors .
- Le score total est : .
❓ Questions progressives
Section titled “❓ Questions progressives”-
Modélisation et Complexité Brute Justifier que le choix des chapitres peut être vu comme un problème d’optimisation combinatoire. Si l’on teste tous les sous-ensembles possibles de chapitres, quelle est la complexité temporelle de l’algorithme naïf en fonction de ? Pourquoi cette approche est-elle inefficace pour ?
-
Formulation récursive et Principe d’optimalité On note le score maximal qu’il est possible d’obtenir en considérant un sous-problème restreint aux dépendances du chapitre avec un temps alloué (). Énoncer la relation de récurrence (le principe d’optimalité de Bellman) permettant d’exprimer en fonction des sous-problèmes associés aux prérequis immédiats de .
-
Implémentation naïve (Récursivité simple) Écrire une fonction en Python
score_naif(i, t, D, S, Pre_req)qui calcule ce score maximal sans optimisation, oùDest la liste des durées,Sla liste des scores, etPre_requne structure représentant les dépendances. -
Optimisation par Mémoïsation (Top-Down) Expliquer pourquoi l’approche naïve présente des redondances de calculs (sous-problèmes chevauchants). Écrire une version optimisée par mémoïsation en Python :
score_memo(i, t, D, S, Pre_req, memo). Quelle est la complexité temporelle et spatiale de cette nouvelle approche en fonction de et ? -
Approche Bottom-Up (Programmation dynamique itérative) Proposer une stratégie d’évaluation itérative (bottom-up) si le graphe des dépendances est un ordre partiel (on suppose pour simplifier que les chapitres sont déjà triés par ordre topologique). Préciser l’initialisation et l’ordre des boucles.
💡 Pistes & Indications méthodologiques
Section titled “💡 Pistes & Indications méthodologiques”- Q1 : Pensez au nombre de parties d’un ensemble à éléments. Pour , est hors de portée d’un ordinateur moderne.
- Q2 : Pour valider un chapitre , il faut d’abord valider tous ses prérequis immédiats en répartissant le temps disponible entre ces prérequis.
- Q4 : La mémoïsation consiste à stocker les résultats intermédiaires dans un dictionnaire ou une matrice (ici de taille ) pour éviter de recalculer plusieurs fois le même état .
- Q5 : Le tri topologique garantit que lorsque l’on calcule le résultat pour un chapitre , tous ses prérequis ont déjà été résolus.
📋 Corrigé détaillé pas à pas
Section titled “📋 Corrigé détaillé pas à pas”Corrigé Q1 : Complexité de l’approche naïve
Section titled “Corrigé Q1 : Complexité de l’approche naïve”L’ensemble des choix possibles de chapitres correspond à l’ensemble des parties de , qui contient éléments. Pour chaque sous-ensemble, il faut opérations pour vérifier la validité (respect des prérequis et du temps ). La complexité temporelle est donc en . Pour , , ce qui rend l’algorithme complètement impracticable (ralentissement exponentiel).
Corrigé Q2 : Principe d’optimalité de Bellman
Section titled “Corrigé Q2 : Principe d’optimalité de Bellman”Pour calculer (le score maximal en incluant le chapitre et ses prérequis avec un budget ): Si , il est impossible de valider le chapitre , la fonction renvoie (ou si le choix vide est permis, mais ici on cherche à inclure ). Si , le temps restant pour les prérequis est . Soit l’ensemble des prérequis immédiats de . Si l’on note la distribution optimale du temps parmi les prérequis, on a la relation de récurrence structurelle : (Note : si les prérequis sont indépendants dans leur consommation de temps, on somme les scores maximaux de chaque prérequis sous réserve que la somme des temps alloués à chaque prérequis ne dépasse pas ).
Corrigé Q3 : Implémentation naïve en Python
Section titled “Corrigé Q3 : Implémentation naïve en Python”def score_naif(i, t, D, S, Pre_req): if D[i] > t: return -float('inf') # Impossible de faire ce chapitre
temps_restant = t - D[i] score_pre_req = 0
# On suppose ici pour simplifier que chaque prérequis j nécessite un sous-budget # Dans un sac-à-dos généralisé, la répartition du temps est combinatoire. for j in Pre_req[i]: # Version simplifiée où chaque prérequis doit être fait au max de sa rentabilité # ou exploration de toutes les partitions de temps (très lourd en naïf) pass
return S[i] + score_pre_reqCorrigé Q4 : Optimisation par Mémoïsation
Section titled “Corrigé Q4 : Optimisation par Mémoïsation”Les sous-problèmes se répètent car plusieurs chapitres peuvent partager un même prérequis , et pour un même chapitre , on peut être amené à calculer plusieurs fois le score pour un même budget résiduel .
def score_memo(i, t, D, S, Pre_req, memo): if (i, t) in memo: return memo[(i, t)]
if D[i] > t: return -float('inf')
t_restant = t - D[i] # Supposons une fonction auxiliaire qui résout la répartition du temps sur les prérequis # En pratique, on combine la programmation dynamique par "tranche de sac à dos".
res = S[i] + somme_optimale_prerequis(i, t_restant, D, S, Pre_req, memo)
memo[(i, t)] = res return res- Complexité temporelle : Il y a chapitres et valeurs possibles pour le temps . Le nombre d’états distincts est donc . Avec la mémoïsation, chaque état n’est calculé qu’une seule fois. La complexité passe de exponentielle à .
- Complexité spatiale : pour stocker la table de mémoïsation
memo, ainsi que la pile d’appels récursifs de profondeur .
Corrigé Q5 : Approche Bottom-Up (Itérative)
Section titled “Corrigé Q5 : Approche Bottom-Up (Itérative)”En triant les sommets par ordre topologique (les prérequis avant les successeurs), on remplit un tableau dp[i][t] représentant le score maximal pour le sous-graphe enraciné en avec un budget .
def programmation_dynamique_bottom_up(N, T, D, S, ordre_topo): # dp[i][t] : score max pour le sous-arbre de i avec un temps t dp = [[0教师 for t in range(T + 1)] for i in range(N)] # Correction typographique conceptuelle
# En pratique, allocation d'une matrice (N, T+1) initialisée à -inf dp = [[-float('inf')] * (T + 1) for _ in range(N)]
for i in ordre_topo: for t in range(T + 1): if D[i] <= t: # Cas de base sans prérequis val = S[i] # Ajout des contributions optimales des prérequis j de i # ... (combinaison des sous-problèmes déjà résolus dans dp[j][t']) dp[i][t] = val
return max(dp[i][T] for i in range(N))Cette approche élimine le risque de débordement de pile (StackOverflow) inhérent à la récursivité profonde et permet une optimisation fine des boucles itératives, très appréciée des examinateurs aux concours.