Skip to content

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 NN chapitres numérotés de 00 à N−1N-1.

Chaque chapitre ii nécessite un certain nombre de jours de travail di∈N∗d_i \in \mathbb{N}^* et rapporte un score de maîtrise si∈Ns_i \in \mathbb{N}. 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 MM règles de dépendance (j,i)(j, i) signifiant que le chapitre jj doit impérativement être validé avant d’entamer le chapitre ii.

On dispose d’un temps total imparti de TT 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 TT, et maximisant la somme des scores obtenus.

On formalise le problème ainsi :

  • Soit V⊂{0,…,N−1}V \subset \{0, \dots, N-1\} l’ensemble des chapitres validés.
  • La contrainte de temps s’écrit : ∑i∈Vdi≤T\sum_{i \in V} d_i \le T.
  • La contrainte de prérequis s’écrit : si (j,i)(j, i) est une règle et i∈Vi \in V, alors j∈Vj \in V.
  • Le score total est : ∑i∈Vsi\sum_{i \in V} s_i.

  1. 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 NN ? Pourquoi cette approche est-elle inefficace pour N=50N = 50 ?

  2. Formulation récursive et Principe d’optimalité On note ScoreMax(i,t)\text{ScoreMax}(i, t) le score maximal qu’il est possible d’obtenir en considérant un sous-problème restreint aux dépendances du chapitre ii avec un temps alloué tt (0≤t≤T0 \le t \le T). Énoncer la relation de récurrence (le principe d’optimalité de Bellman) permettant d’exprimer ScoreMax(i,t)\text{ScoreMax}(i, t) en fonction des sous-problèmes associés aux prérequis immédiats de ii.

  3. 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ù D est la liste des durées, S la liste des scores, et Pre_req une structure représentant les dépendances.

  4. 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 NN et TT ?

  5. 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 à NN éléments. Pour N=50N=50, 2502^{50} est hors de portée d’un ordinateur moderne.
  • Q2 : Pour valider un chapitre ii, il faut d’abord valider tous ses prérequis immédiats j∈Pred(i)j \in \text{Pred}(i) en répartissant le temps t−dit - d_i 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 N×(T+1)N \times (T+1)) pour éviter de recalculer plusieurs fois le même état (i,t)(i, t).
  • Q5 : Le tri topologique garantit que lorsque l’on calcule le résultat pour un chapitre ii, tous ses prérequis ont déjà été résolus.

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 {0,…,N−1}\{0, \dots, N-1\}, qui contient 2N2^N éléments. Pour chaque sous-ensemble, il faut O(N+M)\mathcal{O}(N+M) opérations pour vérifier la validité (respect des prérequis et du temps TT). La complexité temporelle est donc en O((N+M)2N)\mathcal{O}((N+M) 2^N). Pour N=50N = 50, 250≈1.1×10152^{50} \approx 1.1 \times 10^{15}, 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 ScoreMax(i,t)\text{ScoreMax}(i, t) (le score maximal en incluant le chapitre ii et ses prérequis avec un budget tt): Si di>td_i > t, il est impossible de valider le chapitre ii, la fonction renvoie −∞-\infty (ou 00 si le choix vide est permis, mais ici on cherche à inclure ii). Si di≤td_i \le t, le temps restant pour les prérequis est t′=t−dit' = t - d_i. Soit Pred(i)\text{Pred}(i) l’ensemble des prérequis immédiats de ii. Si l’on note R(Pred(i),t′)\mathcal{R}(\text{Pred}(i), t') la distribution optimale du temps t′t' parmi les prérequis, on a la relation de récurrence structurelle : ScoreMax(i,t)=si+CombinaisonOptimale(Pred(i),t−di)\text{ScoreMax}(i, t) = s_i + \text{CombinaisonOptimale}\left(\text{Pred}(i), t - d_i\right) (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 t−dit - d_i).

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_req

Corrigé 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 jj, et pour un même chapitre ii, on peut être amené à calculer plusieurs fois le score pour un même budget résiduel tt.

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 NN chapitres et T+1T+1 valeurs possibles pour le temps tt. Le nombre d’états distincts est donc O(N⋅T)\mathcal{O}(N \cdot T). Avec la mémoïsation, chaque état n’est calculé qu’une seule fois. La complexité passe de exponentielle à O(N⋅T×DegreˊMax)\mathcal{O}(N \cdot T \times \text{DegréMax}).
  • Complexité spatiale : O(N⋅T)\mathcal{O}(N \cdot T) pour stocker la table de mémoïsation memo, ainsi que la pile d’appels récursifs de profondeur O(N)\mathcal{O}(N).

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 ii avec un budget tt.

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.