{
  "chapter": {
    "id": "programmation-dynamique",
    "level": "terminale",
    "theme": "Algorithmique",
    "title": "Programmation dynamique",
    "description": "Sous-problèmes recouvrants, sous-structure optimale,\nmémoïsation descendante, tabulation ascendante,\nexemples classiques (suite de Fibonacci, plus longue\nsous-séquence commune, sac à dos, distance d'édition,\nrendu de monnaie).",
    "prerequisites": [],
    "references": []
  },
  "questions": [
    {
      "id": "q01",
      "difficulty": 1,
      "skills": [
        "definition"
      ],
      "title": "Idée centrale de la programmation dynamique",
      "statement": "Quel est le principe fondamental de la\nprogrammation dynamique ?",
      "options": [
        {
          "text": "Diviser le problème en deux moitiés et résoudre chacune indépendamment",
          "correct": false,
          "feedback": "Cette description correspond au paradigme\n« diviser pour régner ». La différence\nessentielle est que les sous-problèmes y sont\n**disjoints**, alors qu'en programmation\ndynamique ils se **recouvrent**.\n"
        },
        {
          "text": "Faire à chaque étape le choix qui semble localement le meilleur",
          "correct": false,
          "feedback": "Cette description correspond au principe des\nalgorithmes gloutons. La programmation\ndynamique, au contraire, explore toutes les\npossibilités de manière organisée, en\nmémorisant les solutions des sous-problèmes.\n"
        },
        {
          "text": "Modifier le programme à mesure qu'il s'exécute",
          "correct": false,
          "feedback": "Cette description évoque l'auto-modification\nde code, qui n'a aucun rapport avec la\nprogrammation dynamique. Le mot\n« dynamique » désigne ici le caractère\ntemporel de la décision, pas une\nmodification du programme.\n"
        },
        {
          "text": "Mémoriser les solutions des sous-problèmes pour ne pas avoir à les recalculer",
          "correct": true,
          "feedback": "La programmation dynamique exploite la\nprésence de sous-problèmes qui se répètent\ndans la décomposition récursive. En les\nmémorisant, on évite la croissance\nexponentielle du nombre de recalculs.\n"
        }
      ],
      "explanation": "Le terme « programmation » dans « programmation\ndynamique » vient de l'optimisation linéaire\n(Bellman, années $1950$), et non de la\nprogrammation informatique. Il signifie « plan,\ntableau », ce qui justifie la notion de\ntabulation."
    },
    {
      "id": "q02",
      "difficulty": 1,
      "skills": [
        "memoisation"
      ],
      "title": "Mémoïsation",
      "statement": "Qu'est-ce que la **mémoïsation** ?",
      "options": [
        {
          "text": "La comparaison de deux algorithmes pour choisir le plus rapide",
          "correct": false,
          "feedback": "Cette opération relève de la mesure de\nperformance, et n'a aucun rapport avec la\nmémoïsation.\n"
        },
        {
          "text": "La conversion automatique d'une fonction récursive en boucle itérative",
          "correct": false,
          "feedback": "Cette transformation s'appelle la\n**dérécursivation**, et c'est une technique\ndifférente. La mémoïsation conserve la\nstructure récursive de la fonction, en y\najoutant simplement un cache.\n"
        },
        {
          "text": "La compression d'un programme en mémoire pour gagner de la place",
          "correct": false,
          "feedback": "La mémoïsation n'a aucun rapport avec la\ntaille du programme. Elle concerne le\n**temps de calcul**, en évitant de\nrecalculer plusieurs fois la même valeur.\n"
        },
        {
          "text": "Le stockage des résultats déjà calculés dans un cache pour les réutiliser",
          "correct": true,
          "feedback": "À chaque appel récursif, l'algorithme\nvérifie si le résultat est déjà connu\n(typiquement dans un dictionnaire ou un\ntableau) avant de le calculer. Si la valeur\nest déjà disponible, elle est renvoyée\ndirectement.\n"
        }
      ],
      "explanation": "Le mot « mémoïsation » associe les idées de\nmémoire et de récursivité. C'est l'approche\n« descendante » de la programmation dynamique :\non part du problème complet, on descend dans les\nsous-problèmes en mémorisant les résultats au\npassage."
    },
    {
      "id": "q03",
      "difficulty": 1,
      "skills": [
        "tabulation"
      ],
      "title": "Tabulation",
      "statement": "Qu'appelle-t-on **tabulation** en programmation\ndynamique ?",
      "options": [
        {
          "text": "Le découpage d'un problème en sous-problèmes égaux",
          "correct": false,
          "feedback": "Cette description correspond au paradigme\n« diviser pour régner ». La tabulation\nconsiste à remplir un tableau ; elle ne\ndécoupe pas le problème.\n"
        },
        {
          "text": "La création d'une table HTML pour afficher les résultats",
          "correct": false,
          "feedback": "La « tabulation » dans ce contexte\nalgorithmique n'a aucun rapport avec une\nmise en forme HTML. Il s'agit du\nremplissage progressif d'un tableau de\nvaleurs.\n"
        },
        {
          "text": "Une optimisation des comparaisons utilisée dans un algorithme de tri",
          "correct": false,
          "feedback": "La tabulation est spécifique à la\nprogrammation dynamique. Elle n'a pas de\nrôle particulier dans les algorithmes de\ntri.\n"
        },
        {
          "text": "La construction d'un tableau de résultats, en partant des plus petits sous-problèmes pour aller vers les plus grands",
          "correct": true,
          "feedback": "C'est l'approche dite « ascendante » : on\npart des cas de base et on remplit\nprogressivement un tableau, sans aucune\nrécursion.\n"
        }
      ],
      "explanation": "La mémoïsation et la tabulation sont les deux\nfaces de la programmation dynamique : la\nmémoïsation est descendante (récursive, avec un\ncache) ; la tabulation est ascendante\n(itérative, avec un tableau). En pratique, la\ntabulation est souvent plus rapide, car elle\névite le surcoût des appels récursifs."
    },
    {
      "id": "q04",
      "difficulty": 1,
      "skills": [
        "fibonacci"
      ],
      "title": "Suite de Fibonacci en programmation dynamique",
      "statement": "Quelle est la complexité de la suite de Fibonacci\ncalculée avec mémoïsation ?",
      "options": [
        {
          "text": "$O(n^2)$",
          "correct": false,
          "feedback": "La programmation dynamique appliquée à\nFibonacci ne nécessite qu'un parcours\nlinéaire des valeurs, et non une double\nitération qui justifierait une complexité\nquadratique.\n"
        },
        {
          "text": "$O(\\log n)$",
          "correct": false,
          "feedback": "Avec la récurrence $f(n) = f(n-1) + f(n-2)$,\non ne peut pas descendre en dessous de la\ncomplexité linéaire avec une mémoïsation\nstandard. Une complexité logarithmique\nexiste, mais elle requiert des techniques\nplus avancées (comme l'exponentiation\nmatricielle).\n"
        },
        {
          "text": "$O(2^n)$",
          "correct": false,
          "feedback": "Cette complexité correspond à la version\n**récursive naïve**, sans mémoïsation. Le\nrecours à un cache réduit drastiquement ce\ncoût.\n"
        },
        {
          "text": "$O(n)$",
          "correct": true,
          "feedback": "Avec mémoïsation, chaque valeur\n$\\text{fib}(k)$ pour $k$ allant de $0$ à\n$n$ n'est calculée qu'une seule fois, ce\nqui donne $n$ calculs au total.\n"
        }
      ],
      "explanation": "Le passage de $O(2^n)$ à $O(n)$ pour la suite\nde Fibonacci est l'exemple canonique de la\npuissance de la programmation dynamique. Pour\n$n = 50$, on passe ainsi de plus d'un million\nd'appels à seulement $51$."
    },
    {
      "id": "q05",
      "difficulty": 1,
      "skills": [
        "sous-problemes-recouvrants"
      ],
      "title": "Sous-problèmes recouvrants",
      "statement": "Que signifie « le problème a des sous-problèmes\n**recouvrants** » ?",
      "options": [
        {
          "text": "Les sous-problèmes ont tous la même solution",
          "correct": false,
          "feedback": "Les sous-problèmes peuvent avoir des\nsolutions très différentes, ce qui est\nd'ailleurs le cas le plus courant. Le\ncaractère recouvrant signifie qu'ils\n**apparaissent plusieurs fois** dans\nl'arbre des appels récursifs, et non qu'ils\npartagent leur solution.\n"
        },
        {
          "text": "Les sous-problèmes sont triés par taille décroissante",
          "correct": false,
          "feedback": "L'ordre dans lequel on considère les\nsous-problèmes n'a rien à voir avec leur\ncaractère recouvrant. Le mot « recouvrant »\nrenvoie à la notion de **doublon**, pas\nd'ordre.\n"
        },
        {
          "text": "Les sous-problèmes sont indépendants les uns des autres",
          "correct": false,
          "feedback": "Si les sous-problèmes étaient indépendants,\non appliquerait plutôt le paradigme\n« diviser pour régner ». La programmation\ndynamique exploite précisément le contraire.\n"
        },
        {
          "text": "Plusieurs sous-problèmes peuvent apparaître plus d'une fois dans la décomposition récursive",
          "correct": true,
          "feedback": "C'est le cas typique de la suite de\nFibonacci, où la valeur $\\text{fib}(k)$ est\ncalculée plusieurs fois dans la version\nnaïve. La programmation dynamique évite\nprécisément ces recalculs.\n"
        }
      ],
      "explanation": "Cette propriété est essentielle pour que la\nprogrammation dynamique soit utile. Si les\nsous-problèmes sont uniques, la mémoïsation\nn'apporte rien et on retombe sur le paradigme\n« diviser pour régner » classique."
    },
    {
      "id": "q06",
      "difficulty": 1,
      "skills": [
        "sous-structure-optimale"
      ],
      "title": "Sous-structure optimale",
      "statement": "Que signifie « le problème possède une\n**sous-structure optimale** » ?",
      "options": [
        {
          "text": "La taille des sous-problèmes diminue de manière logarithmique",
          "correct": false,
          "feedback": "Cette description évoque la diminution\nlogarithmique du paradigme « diviser pour\nrégner » équilibré, mais elle ne correspond\npas à la définition de la sous-structure\noptimale.\n"
        },
        {
          "text": "La solution optimale du problème global est constituée des solutions optimales aux sous-problèmes",
          "correct": true,
          "feedback": "Cette propriété permet de construire la\nsolution optimale en combinant les solutions\noptimales des sous-problèmes. C'est elle qui\nrend la programmation dynamique applicable.\n"
        },
        {
          "text": "Le problème peut être résolu en parallèle sur plusieurs processeurs",
          "correct": false,
          "feedback": "La sous-structure optimale est une propriété\nmathématique du problème, et non une\ncaractéristique algorithmique. Le\nparallélisme relève de l'implémentation.\n"
        },
        {
          "text": "Les sous-problèmes ont tous la même taille",
          "correct": false,
          "feedback": "Cette uniformité de taille n'est pas\nrequise. La sous-structure optimale parle de\nla composition des solutions, pas des\ntailles des sous-problèmes.\n"
        }
      ],
      "explanation": "La sous-structure optimale et le caractère\nrecouvrant des sous-problèmes sont les deux\ningrédients qui rendent un problème adapté à la\nprogrammation dynamique. Sans la première, on\nne peut pas combiner les solutions ; sans la\nseconde, la mémoïsation ne sert à rien."
    },
    {
      "id": "q07",
      "difficulty": 1,
      "skills": [
        "comparaison-glouton"
      ],
      "title": "Programmation dynamique et algorithme glouton",
      "statement": "Quelle est la différence principale entre un\n**algorithme glouton** et un algorithme de\n**programmation dynamique** ?",
      "options": [
        {
          "text": "L'algorithme glouton fait un choix local optimal sans revenir en arrière, tandis que la programmation dynamique explore toutes les possibilités via les sous-problèmes",
          "correct": true,
          "feedback": "L'algorithme glouton est plus rapide mais\npeut manquer l'optimum global. La\nprogrammation dynamique est plus coûteuse,\nmais elle garantit l'optimum global lorsque\nle problème possède la sous-structure\noptimale.\n"
        },
        {
          "text": "L'algorithme glouton est récursif, alors que la programmation dynamique est itérative",
          "correct": false,
          "feedback": "Les deux approches peuvent être implémentées\naussi bien récursivement qu'itérativement.\nC'est le **principe** qui les distingue, et\nnon leur forme syntaxique.\n"
        },
        {
          "text": "La programmation dynamique est utilisée uniquement pour les jeux vidéo",
          "correct": false,
          "feedback": "La programmation dynamique a un champ\nd'application très large : alignement de\nséquences, optimisation économique,\ntraitement automatique du langage, et bien\nd'autres domaines.\n"
        },
        {
          "text": "L'algorithme glouton ne fonctionne que sur les graphes",
          "correct": false,
          "feedback": "Les algorithmes gloutons s'appliquent à de\ntrès nombreux types de problèmes : rendu de\nmonnaie, planification d'activités, sac à\ndos fractionnaire, et bien d'autres.\n"
        }
      ],
      "explanation": "Quand un problème possède la **propriété de\nchoix glouton** (un choix local mène à\nl'optimum global), un algorithme glouton suffit\net reste plus efficace. Sinon, la programmation\ndynamique est souvent nécessaire pour\natteindre l'optimum."
    },
    {
      "id": "q08",
      "difficulty": 1,
      "skills": [
        "memoisation",
        "structure"
      ],
      "title": "Structure de mémoïsation",
      "statement": "Quelle structure Python convient le mieux pour\nmémoïser une fonction prenant un seul paramètre\nentier positif ?",
      "options": [
        {
          "text": "Une liste indexée par la valeur du paramètre",
          "correct": true,
          "feedback": "Pour des paramètres entiers positifs dans\nune plage connue, une liste donne un accès\nen $O(1)$ très efficace. C'est la structure\nla plus rapide.\n"
        },
        {
          "text": "Un fichier sur le disque",
          "correct": false,
          "feedback": "Le disque est beaucoup trop lent pour de la\nmémoïsation en mémoire. Il s'utiliserait\nplutôt pour persister un cache entre deux\nexécutions du programme.\n"
        },
        {
          "text": "Un dictionnaire avec les paramètres comme clés",
          "correct": false,
          "feedback": "Un dictionnaire fonctionne, mais avec un\nsurcoût lié au calcul des empreintes. Pour\ndes indices entiers, la liste reste plus\nrapide.\n"
        },
        {
          "text": "Une variable globale unique",
          "correct": false,
          "feedback": "Une seule variable ne peut stocker qu'une\nseule valeur, alors que la mémoïsation\ndemande d'en mémoriser plusieurs.\n"
        }
      ],
      "explanation": "Pour des paramètres entiers, la liste est\nidéale. Pour des paramètres plus complexes\n(chaînes, $p$-uplets, objets), le dictionnaire\ndevient nécessaire. Python propose aussi le\ndécorateur `@functools.lru_cache`, qui\nautomatise tout cela."
    },
    {
      "id": "q09",
      "difficulty": 1,
      "skills": [
        "bellman"
      ],
      "title": "Origine du terme",
      "statement": "Le terme « programmation dynamique » a été\nintroduit en $1953$ par :",
      "options": [
        {
          "text": "Tim Berners-Lee",
          "correct": false,
          "feedback": "Tim Berners-Lee est l'inventeur du World\nWide Web. Son travail n'a aucun rapport\navec la programmation dynamique.\n"
        },
        {
          "text": "Alan Turing",
          "correct": false,
          "feedback": "Alan Turing est principalement connu pour\nla machine de Turing et le décryptage\nd'Enigma, mais pas pour la programmation\ndynamique.\n"
        },
        {
          "text": "Donald Knuth",
          "correct": false,
          "feedback": "Donald Knuth est connu pour son traité\nmonumental « The Art of Computer\nProgramming », mais ce n'est pas lui qui\nest à l'origine de la programmation\ndynamique.\n"
        },
        {
          "text": "Richard Bellman",
          "correct": true,
          "feedback": "Richard Bellman a inventé la programmation\ndynamique pour traiter des problèmes\nd'optimisation séquentielle. Le mot\n« programmation » vient du jargon militaire\nde l'époque, dans le sens de\n« planification ».\n"
        }
      ],
      "explanation": "Bellman a expliqué qu'il avait choisi le terme\n« dynamique » pour évoquer le caractère\ntemporel de la décision, et « programmation »\nau sens de planification. L'équation de Bellman\nreste aujourd'hui un outil central en\noptimisation et en apprentissage par\nrenforcement."
    },
    {
      "id": "q10",
      "difficulty": 1,
      "skills": [
        "exemples"
      ],
      "title": "Exemple classique de programmation dynamique",
      "statement": "Lequel des problèmes suivants se résout\ntypiquement par programmation dynamique ?",
      "options": [
        {
          "text": "Tester si un nombre est premier",
          "correct": false,
          "feedback": "Les tests de primalité ne se prêtent pas à\nla programmation dynamique, car ils ne font\npas apparaître naturellement de\nsous-problèmes recouvrants.\n"
        },
        {
          "text": "Inverser une chaîne de caractères",
          "correct": false,
          "feedback": "C'est une opération en $O(n)$ qui ne\nrequiert aucune mémoïsation. Aucune\ndécomposition en sous-problèmes ne\ns'impose.\n"
        },
        {
          "text": "Trier un tableau",
          "correct": false,
          "feedback": "Les algorithmes de tri usuels (tri par\ninsertion, par sélection, par fusion) ne\nrelèvent pas de la programmation\ndynamique.\n"
        },
        {
          "text": "Calculer le plus court chemin dans un graphe pondéré",
          "correct": true,
          "feedback": "L'algorithme de Bellman-Ford repose sur la\nprogrammation dynamique. Il exploite la\nsous-structure optimale : le plus court\nchemin de A à C contient un plus court\nchemin de A à B, pour tout sommet B\nintermédiaire.\n"
        }
      ],
      "explanation": "Autres exemples typiques : sac à dos, plus\nlongue sous-séquence commune, distance\nd'édition, plus longue sous-suite croissante,\nrendu de monnaie. Tous ont en commun la\nsous-structure optimale et les sous-problèmes\nrecouvrants."
    },
    {
      "id": "q11",
      "difficulty": 2,
      "skills": [
        "fibonacci",
        "code"
      ],
      "title": "Mémoïsation manuelle de Fibonacci",
      "statement": "On écrit la fonction suivante :\n\n```\nmemo = {}\ndef fib(n):\n    if n in memo:\n        return memo[n]\n    if n <= 1:\n        return n\n    memo[n] = fib(n - 1) + fib(n - 2)\n    return memo[n]\n```\n\nCombien d'appels effectifs (qui calculent\nréellement, sans utiliser le cache) sont\neffectués pour `fib(10)` ?",
      "options": [
        {
          "text": "Exactement un",
          "correct": false,
          "feedback": "Il faut bien calculer chaque valeur\nintermédiaire au moins une fois. La\nmémoïsation évite les recalculs, pas les\ncalculs initiaux.\n"
        },
        {
          "text": "Environ onze",
          "correct": true,
          "feedback": "Chaque valeur `fib(0)`, `fib(1)`, ...,\n`fib(10)` est calculée une seule fois grâce\nau cache. Les appels suivants sur une\nvaleur déjà calculée sont satisfaits\nimmédiatement par `memo`.\n"
        },
        {
          "text": "Plus de mille",
          "correct": false,
          "feedback": "Cet ordre de grandeur correspond à la\nversion sans mémoïsation. Avec un cache,\nle nombre de calculs est bien plus\nmodeste.\n"
        },
        {
          "text": "Environ cent",
          "correct": false,
          "feedback": "C'est trop. Le nombre de valeurs distinctes\nà calculer est seulement $n + 1$, soit\n$11$, et non $n^2$.\n"
        }
      ],
      "explanation": "La mémoïsation transforme la complexité\n$O(2^n)$ en $O(n)$ : c'est un gain\nalgorithmique considérable. Le code reste\npourtant très proche de la version récursive\nnaïve."
    },
    {
      "id": "q12",
      "difficulty": 2,
      "skills": [
        "tabulation",
        "fibonacci"
      ],
      "title": "Tabulation pour Fibonacci",
      "statement": "Quelle est la version **tabulée** (itérative) de\nla suite de Fibonacci ?",
      "options": [
        {
          "text": "```\ndef fib(n):\n    return round((1.618 ** n) / 2.236)\n```\n",
          "correct": false,
          "feedback": "Cette formule est celle de Binet, qui\nutilise le nombre d'or. Elle ne relève pas\nde la programmation dynamique et devient\nimprécise pour de grandes valeurs de $n$ à\ncause des arrondis sur les flottants.\n"
        },
        {
          "text": "```\ndef fib(n):\n    t = [0, 1]\n    for i in range(2, n + 1):\n        t.append(t[i-1] + t[i-2])\n    return t[n]\n```\n",
          "correct": true,
          "feedback": "On remplit un tableau `t` de taille $n + 1$\nen partant des cas de base ($t[0] = 0$ et\n$t[1] = 1$), puis on construit les valeurs\nsuivantes par récurrence. La complexité\nest $O(n)$ en temps comme en espace.\n"
        },
        {
          "text": "```\ndef fib(n):\n    if n <= 1: return n\n    return fib(n-1) + fib(n-2)\n```\n",
          "correct": false,
          "feedback": "Il s'agit de la version récursive **naïve**,\nsans mémoïsation ni tabulation. Sa\ncomplexité est $O(2^n)$.\n"
        },
        {
          "text": "```\ndef fib(n):\n    return n * (n - 1) / 2\n```\n",
          "correct": false,
          "feedback": "Cette formule donne la somme des entiers\nde $0$ à $n - 1$, et non la suite de\nFibonacci.\n"
        }
      ],
      "explanation": "L'approche tabulée évite la récursion et le\ncache. Pour Fibonacci, on peut même optimiser\nl'espace en ne gardant que les deux dernières\nvaleurs : `a, b = b, a + b`."
    },
    {
      "id": "q13",
      "difficulty": 2,
      "skills": [
        "sac-a-dos"
      ],
      "title": "Problème du sac à dos",
      "statement": "Le problème du **sac à dos $0/1$** consiste à\nchoisir un sous-ensemble d'objets à mettre dans\nun sac de capacité fixée pour maximiser leur\nvaleur totale. Quelle est sa complexité en\nprogrammation dynamique avec un sac de\ncapacité $W$ et $n$ objets ?",
      "options": [
        {
          "text": "$O(n!)$",
          "correct": false,
          "feedback": "Aucune raison particulière n'amène à une\ncomplexité factorielle pour ce problème.\n"
        },
        {
          "text": "$O(n)$",
          "correct": false,
          "feedback": "Il faut considérer chaque objet pour chaque\ncapacité possible du sac. La complexité\ndépend nécessairement des deux paramètres.\n"
        },
        {
          "text": "$O(2^n)$",
          "correct": false,
          "feedback": "Cette complexité correspond à l'approche\npar recherche exhaustive (essayer tous les\nsous-ensembles d'objets). La programmation\ndynamique est bien plus rapide.\n"
        },
        {
          "text": "$O(n \\cdot W)$",
          "correct": true,
          "feedback": "On remplit un tableau à deux dimensions de\ntaille $n \\times W$. Pour chaque case, le\ncalcul est en $O(1)$. La complexité totale\nest donc $O(nW)$.\n"
        }
      ],
      "explanation": "La complexité $O(nW)$ est dite\n**pseudo-polynomiale** : elle est polynomiale\nen la **valeur** de $W$, mais exponentielle en\nsa **taille en bits**. Le sac à dos reste donc\nun problème difficile en taille d'entrée\nstricte."
    },
    {
      "id": "q14",
      "difficulty": 2,
      "skills": [
        "comparaison",
        "glouton"
      ],
      "title": "Sac à dos : comparer les deux approches",
      "statement": "Pour le problème du sac à dos $0/1$,\nl'algorithme glouton qui prend les objets par\nordre décroissant du ratio « valeur / poids »\ndonne-t-il toujours la solution optimale ?",
      "options": [
        {
          "text": "L'algorithme glouton est plus rapide mais moins précis",
          "correct": false,
          "feedback": "Il s'agit ici d'optimum exact, pas\nd'approximation. Soit l'algorithme garantit\nl'optimum, soit il ne le garantit pas. Il\nn'y a pas de notion de précision\nintermédiaire.\n"
        },
        {
          "text": "Non, l'algorithme glouton peut manquer l'optimum dans le cas $0/1$",
          "correct": true,
          "feedback": "Pour le sac à dos $0/1$, l'algorithme\nglouton peut être trompé. Avec un sac de\ncapacité $50$ et les objets\n$\\{(60, 10), (100, 20), (120, 30)\\}$\n(valeur, poids), l'algorithme glouton prend\n$(120, 30)$ en premier, alors que la\ncombinaison optimale est\n$(60, 10) + (100, 20)$, pour une valeur\ntotale supérieure.\n"
        },
        {
          "text": "Oui, c'est l'algorithme correct pour ce problème",
          "correct": false,
          "feedback": "Cette approche gloutonne est optimale pour\nle **sac à dos fractionnaire** (où l'on\npeut couper un objet), mais pas pour la\nversion $0/1$, où chaque objet est pris\nentièrement ou pas du tout.\n"
        },
        {
          "text": "Cela dépend du langage de programmation utilisé",
          "correct": false,
          "feedback": "La propriété est purement mathématique. Elle\nne dépend pas du langage choisi pour\nl'implémentation.\n"
        }
      ],
      "explanation": "Le sac à dos $0/1$ ne possède **pas** la\npropriété de choix glouton. Il faut donc\nexplorer plusieurs possibilités, ce que fait la\nprogrammation dynamique en $O(nW)$, à comparer\naux $O(2^n)$ de la recherche exhaustive."
    },
    {
      "id": "q15",
      "difficulty": 2,
      "skills": [
        "bottom-up",
        "top-down"
      ],
      "title": "Approche descendante et approche ascendante",
      "statement": "Quelle est la différence entre l'approche\n**descendante** et l'approche **ascendante** en\nprogrammation dynamique ?",
      "options": [
        {
          "text": "L'approche descendante est récursive avec mémoïsation, l'approche ascendante est itérative avec tabulation",
          "correct": true,
          "feedback": "L'approche descendante part du problème\nglobal et descend dans les sous-problèmes,\nen utilisant un cache. L'approche\nascendante part des cas de base et\nconstruit progressivement la table des\nrésultats jusqu'au problème global.\n"
        },
        {
          "text": "L'approche descendante concerne les graphes, l'approche ascendante concerne les listes",
          "correct": false,
          "feedback": "Les deux approches s'appliquent à toutes\nles structures de données. La distinction\nn'a rien à voir avec le type des éléments\nmanipulés.\n"
        },
        {
          "text": "L'approche descendante est toujours plus rapide",
          "correct": false,
          "feedback": "En pratique, l'approche ascendante est\nsouvent **plus rapide**, car elle évite le\nsurcoût des appels récursifs. La complexité\nasymptotique est cependant la même dans les\ndeux cas.\n"
        },
        {
          "text": "Aucune, ces termes sont synonymes",
          "correct": false,
          "feedback": "Ce sont deux approches distinctes, même si\nelles permettent toutes deux de résoudre\nles mêmes problèmes.\n"
        }
      ],
      "explanation": "L'approche descendante (mémoïsation) est plus\nnaturelle quand la définition du problème est\nrécursive. L'approche ascendante (tabulation)\nest en général plus économe en mémoire et plus\nrapide en pratique. Le choix dépend du\ncontexte."
    },
    {
      "id": "q16",
      "difficulty": 2,
      "skills": [
        "decorator"
      ],
      "title": "Le décorateur `lru_cache` en Python",
      "statement": "En Python, quel décorateur de la bibliothèque\n`functools` automatise la mémoïsation d'une\nfonction ?",
      "options": [
        {
          "text": "`@dynamic`",
          "correct": false,
          "feedback": "Ce décorateur n'existe pas dans la\nbibliothèque standard de Python.\n"
        },
        {
          "text": "`@cache_me`",
          "correct": false,
          "feedback": "Ce nom de décorateur n'existe pas en\nPython. Le décorateur attendu se trouve\ndans le module `functools`.\n"
        },
        {
          "text": "`@lru_cache`",
          "correct": true,
          "feedback": "Le décorateur\n`@functools.lru_cache(maxsize=None)` ajoute\nautomatiquement un cache à n'importe\nquelle fonction. C'est très pratique pour\ndes fonctions récursives, sans avoir à\ncoder soi-même la mémoïsation.\n"
        },
        {
          "text": "`@memoize`",
          "correct": false,
          "feedback": "Le décorateur `@memoize` n'est pas dans la\nbibliothèque standard. Certaines\nbibliothèques tierces le proposent, mais le\nstandard Python est `lru_cache`.\n"
        }
      ],
      "explanation": "Le sigle LRU signifie « *Least Recently\nUsed* », soit « le moins récemment utilisé ».\nLe cache stocke un nombre limité de résultats\n($128$ par défaut, ou un nombre illimité avec\n`maxsize=None`) et expulse les moins\nrécemment consultés."
    },
    {
      "id": "q17",
      "difficulty": 2,
      "skills": [
        "monnaie"
      ],
      "title": "Rendu de monnaie en programmation dynamique",
      "statement": "Pour le **rendu de monnaie** avec un système de\npièces arbitraire, la programmation dynamique\ncalcule, en $O(n \\cdot s)$ (où $s$ est la somme\nà rendre et $n$ le nombre de pièces) :",
      "options": [
        {
          "text": "La somme totale rendue",
          "correct": false,
          "feedback": "La somme à rendre est donnée en entrée du\nproblème, elle n'est pas calculée par\nl'algorithme.\n"
        },
        {
          "text": "Le nombre minimal de pièces nécessaires pour atteindre la somme",
          "correct": true,
          "feedback": "Pour chaque sous-somme $k \\in [0, s]$,\nl'algorithme calcule le nombre minimal de\npièces nécessaires. La récurrence utilisée\nest\n$\\text{ND}(k) = 1 + \\min_{p}\\, \\text{ND}(k - p)$,\noù $p$ parcourt les pièces disponibles.\n"
        },
        {
          "text": "La pièce la plus grande utilisée",
          "correct": false,
          "feedback": "Cette description correspond plutôt à\nl'approche gloutonne, qui choisit toujours\nla plus grande pièce disponible.\n"
        },
        {
          "text": "Une séquence ordonnée de pièces",
          "correct": false,
          "feedback": "La programmation dynamique calcule un\nnombre minimal, et non une séquence\nordonnée. La séquence des pièces\neffectivement utilisées peut être\nreconstruite ensuite, par un parcours à\nrebours du tableau.\n"
        }
      ],
      "explanation": "Le rendu de monnaie est l'exemple emblématique\nd'un problème où l'algorithme glouton **échoue**\nsur certains systèmes monétaires (par exemple\n$\\{1, 3, 4\\}$), alors que la programmation\ndynamique garantit l'optimum."
    },
    {
      "id": "q18",
      "difficulty": 2,
      "skills": [
        "comparaison-dpr"
      ],
      "title": "Programmation dynamique et diviser pour régner",
      "statement": "En quoi la programmation dynamique se\ndistingue-t-elle du paradigme « diviser pour\nrégner » classique ?",
      "options": [
        {
          "text": "Le paradigme « diviser pour régner » utilise toujours la récursivité, contrairement à la programmation dynamique",
          "correct": false,
          "feedback": "Les deux paradigmes peuvent utiliser la\nrécursivité. La programmation dynamique\npeut aussi être itérative (par tabulation),\nmais ce n'est pas la différence\nessentielle.\n"
        },
        {
          "text": "La programmation dynamique ne s'applique qu'aux problèmes portant sur des entiers",
          "correct": false,
          "feedback": "La programmation dynamique s'applique à\ntout problème possédant les deux propriétés\nrequises (sous-structure optimale et\nsous-problèmes recouvrants), quel que soit\nle type des données manipulées.\n"
        },
        {
          "text": "La programmation dynamique est plus simple à coder",
          "correct": false,
          "feedback": "C'est subjectif et souvent l'inverse. Les\nalgorithmes de type « diviser pour régner »\nsont en général plus simples à concevoir.\n"
        },
        {
          "text": "En programmation dynamique, les sous-problèmes se recouvrent ; en « diviser pour régner », ils sont disjoints",
          "correct": true,
          "feedback": "C'est la distinction fondamentale. Sans\nrecouvrement, la mémoïsation n'apporte\nrien. Avec recouvrement, elle apporte des\ngains majeurs.\n"
        }
      ],
      "explanation": "Pour identifier si un problème relève de la\nprogrammation dynamique, on peut tracer\nl'arbre des appels récursifs et chercher les\nnœuds dupliqués. S'il y en a beaucoup, la\nprogrammation dynamique est probablement le\nbon outil."
    },
    {
      "id": "q19",
      "difficulty": 2,
      "skills": [
        "memoire-temps"
      ],
      "title": "Compromis mémoire et temps",
      "statement": "La programmation dynamique illustre un\ncompromis classique en algorithmique. Lequel ?",
      "options": [
        {
          "text": "Moins de mémoire consommée et moins de temps de calcul",
          "correct": false,
          "feedback": "Il n'y a pas de gain gratuit. Ne stocker\naucune valeur intermédiaire reviendrait au\ncas naïf récursif, qui est précisément\nbeaucoup plus lent.\n"
        },
        {
          "text": "Plus de mémoire consommée, en échange de beaucoup moins de temps de calcul",
          "correct": true,
          "feedback": "On stocke les solutions intermédiaires\n(mémoire en plus) pour ne plus avoir à les\nrecalculer (temps en moins). C'est un\ncompromis central en algorithmique.\n"
        },
        {
          "text": "Aucun changement par rapport à l'algorithme naïf",
          "correct": false,
          "feedback": "La programmation dynamique apporte un gain\nmajeur sur de nombreux problèmes (suite de\nFibonacci, sac à dos, plus longue\nsous-séquence commune, etc.).\n"
        },
        {
          "text": "Plus de temps de calcul mais moins de mémoire consommée",
          "correct": false,
          "feedback": "C'est exactement l'inverse. La\nprogrammation dynamique consomme plus de\nmémoire, mais elle accélère\nconsidérablement le calcul.\n"
        }
      ],
      "explanation": "Pour la suite de Fibonacci, la version naïve a\nune complexité en $O(2^n)$ en temps avec\n$O(n)$ d'espace de pile. La version mémoïsée\npasse à $O(n)$ en temps avec $O(n)$ de mémoire :\non a légèrement augmenté l'espace, mais on a\ndivisé le temps par un facteur exponentiel."
    },
    {
      "id": "q20",
      "difficulty": 2,
      "skills": [
        "code",
        "lecture"
      ],
      "title": "Lecture d'une programmation dynamique ascendante",
      "statement": "Que calcule la fonction Python suivante ?\n\n```\ndef f(n):\n    T = [0] * (n + 1)\n    T[0] = 1\n    for i in range(1, n + 1):\n        T[i] = T[i-1] * i\n    return T[n]\n```",
      "options": [
        {
          "text": "La factorielle $n!$",
          "correct": true,
          "feedback": "On a $T[i] = T[i-1] \\cdot i = i!$, en\npartant de $T[0] = 1 = 0!$. La fonction\nrenvoie donc bien $n!$, par construction\nascendante.\n"
        },
        {
          "text": "La $n$-ième valeur de la suite de Fibonacci",
          "correct": false,
          "feedback": "La suite de Fibonacci utiliserait\nl'instruction `T[i] = T[i-1] + T[i-2]`,\nfondée sur une addition de deux termes\nprécédents, et non sur la multiplication\npar $i$.\n"
        },
        {
          "text": "$2^n$",
          "correct": false,
          "feedback": "Pour obtenir $2^n$, il faudrait écrire\n`T[i] = T[i-1] * 2` plutôt que\n`T[i-1] * i`.\n"
        },
        {
          "text": "La somme $1 + 2 + \\cdots + n$",
          "correct": false,
          "feedback": "Une somme s'obtiendrait par addition. Or\nla fonction effectue une multiplication\n(`T[i-1] * i`), qui correspond à un\nproduit cumulé.\n"
        }
      ],
      "explanation": "Cet exemple illustre une **programmation\ndynamique ascendante** très simple : on\nremplit un tableau en partant du cas de base.\nC'est l'archétype de la tabulation pour une\nrécurrence simple."
    },
    {
      "id": "q21",
      "difficulty": 3,
      "skills": [
        "plus-longue-sous-sequence"
      ],
      "title": "Plus longue sous-séquence commune",
      "statement": "Pour calculer la plus longue sous-séquence\ncommune à deux chaînes de longueurs $m$ et $n$,\nla programmation dynamique a une complexité\nde :",
      "options": [
        {
          "text": "$O(m \\cdot n)$",
          "correct": true,
          "feedback": "On construit un tableau à deux dimensions\nde taille $m \\times n$. Pour chaque case,\nle calcul est en $O(1)$. La complexité\ntotale est donc $O(mn)$.\n"
        },
        {
          "text": "$O(m^2 + n^2)$",
          "correct": false,
          "feedback": "La complexité de ce problème est\nmultiplicative dans les deux longueurs, et\nnon additive sur leurs carrés.\n"
        },
        {
          "text": "$O(m + n)$",
          "correct": false,
          "feedback": "Cette complexité linéaire correspondrait à\nun simple parcours conjoint des deux\nchaînes. Or il faut explorer les\ncombinaisons possibles, ce qui demande un\ncoût supérieur.\n"
        },
        {
          "text": "$O(2^{m+n})$",
          "correct": false,
          "feedback": "Cette complexité exponentielle correspond\nà l'approche par recherche exhaustive\n(essayer toutes les sous-séquences). La\nprogrammation dynamique réduit ce coût à\nun produit polynomial.\n"
        }
      ],
      "explanation": "Cet algorithme est utilisé en bio-informatique\n(alignement de séquences ADN), dans le\nversionnage de fichiers (commande `diff`), en\nreconnaissance vocale, et dans bien d'autres\ndomaines. La complexité quadratique reste\nraisonnable même pour des chaînes de plusieurs\nmilliers de caractères."
    },
    {
      "id": "q22",
      "difficulty": 3,
      "skills": [
        "piege"
      ],
      "title": "Piège du paramètre par défaut mutable",
      "statement": "Dans une mémoïsation manuelle, on écrit :\n\n```\ndef f(n, memo={}):\n    if n in memo: return memo[n]\n    if n <= 1: return n\n    memo[n] = f(n-1, memo) + f(n-2, memo)\n    return memo[n]\n```\n\nQuel problème pose le `memo={}` placé en\nparamètre par défaut ?",
      "options": [
        {
          "text": "Il est partagé entre tous les appels successifs de la fonction, conservant les valeurs entre exécutions différentes",
          "correct": true,
          "feedback": "Python évalue les valeurs par défaut\n**une seule fois**, à la définition de la\nfonction. Le dictionnaire `memo` est donc\npartagé. Cela peut être souhaité (cache\nglobal) ou provoquer des bugs subtils si\nl'on attendait un cache neuf à chaque\nappel.\n"
        },
        {
          "text": "Il rend la fonction trop lente, car le dictionnaire est recréé à chaque appel",
          "correct": false,
          "feedback": "C'est l'inverse. Le dictionnaire par\ndéfaut est créé **une seule fois** (à la\ndéfinition de la fonction), ce qui est\nprécisément le piège.\n"
        },
        {
          "text": "Il ralentit globalement Python",
          "correct": false,
          "feedback": "Aucun ralentissement global. Le seul\nproblème est le partage non intentionnel\ndu cache entre les appels.\n"
        },
        {
          "text": "Il provoque une erreur de récursion",
          "correct": false,
          "feedback": "Aucune erreur de récursion n'est causée par\nun paramètre mutable par défaut. Le piège\nest sémantique, et ne se manifeste pas par\nune erreur d'exécution.\n"
        }
      ],
      "explanation": "La bonne pratique consiste à utiliser\n`memo=None` puis, à l'intérieur de la\nfonction, `if memo is None: memo = {}`. On peut\naussi recourir à `@functools.lru_cache`, qui\ngère ces subtilités automatiquement."
    },
    {
      "id": "q23",
      "difficulty": 3,
      "skills": [
        "reconstruction-solution"
      ],
      "title": "Reconstruire la solution",
      "statement": "En programmation dynamique, on calcule souvent\nune **valeur optimale** (par exemple le coût\nminimal). Comment retrouve-t-on la **solution\neffective**, c'est-à-dire la séquence de\ndécisions qui mène à cet optimum ?",
      "options": [
        {
          "text": "On ne peut pas, la programmation dynamique ne donne que la valeur optimale",
          "correct": false,
          "feedback": "La solution effective peut tout à fait\nêtre reconstruite : c'est ce qui rend la\nprogrammation dynamique utile dans les\napplications réelles.\n"
        },
        {
          "text": "On lance plusieurs programmations dynamiques avec différents paramètres pour identifier les choix",
          "correct": false,
          "feedback": "Cette approche serait inefficace. Une seule\nexécution suffit, à condition de bien lire\nla table à rebours.\n"
        },
        {
          "text": "On la reconstruit en parcourant la table à rebours, à partir de la valeur finale",
          "correct": true,
          "feedback": "On remonte dans la table en suivant les\nchoix qui ont mené à chaque valeur\noptimale. Cela permet de retrouver la\nséquence de décisions sans alourdir le\ncalcul lui-même.\n"
        },
        {
          "text": "On stocke aussi la solution dans la table",
          "correct": false,
          "feedback": "C'est possible mais coûteux en mémoire, et\nsouvent inutile : la solution se\nreconstruit toujours en $O(n)$ après le\ncalcul, par un parcours à rebours.\n"
        }
      ],
      "explanation": "Cette technique de reconstruction à rebours\nest universelle en programmation dynamique :\naprès avoir rempli la table, on retrace les\nchoix faits à chaque étape pour reconstruire\nla solution complète."
    },
    {
      "id": "q24",
      "difficulty": 3,
      "skills": [
        "optimisation-espace"
      ],
      "title": "Optimisation de l'espace",
      "statement": "Pour calculer la suite de Fibonacci en $O(n)$\nen temps avec une mémoire réduite, on peut\nécrire :\n\n```\na, b = 0, 1\nfor _ in range(n):\n    a, b = b, a + b\n```\n\nQuelle est la complexité **en mémoire** de\ncette version ?",
      "options": [
        {
          "text": "$O(n)$",
          "correct": false,
          "feedback": "On ne stocke que deux variables, `a` et\n`b`, durant tout le calcul. Aucun tableau\nde taille $n$ n'est utilisé.\n"
        },
        {
          "text": "$O(n^2)$",
          "correct": false,
          "feedback": "Aucune raison n'amène à une mémoire\nquadratique pour ce calcul.\n"
        },
        {
          "text": "$O(1)$",
          "correct": true,
          "feedback": "Seules deux variables suffisent. C'est\nl'optimisation classique : quand la\nrécurrence ne dépend que des $k$ valeurs\nprécédentes (ici $k = 2$), on peut réduire\nl'espace à $O(k)$, soit une mémoire\nconstante.\n"
        },
        {
          "text": "$O(\\log n)$",
          "correct": false,
          "feedback": "Aucune structure logarithmique n'est\nutilisée ici, simplement deux variables.\n"
        }
      ],
      "explanation": "Cette optimisation est très courante. Pour de\nnombreuses récurrences (suite de Fibonacci,\nsac à dos selon une dimension, etc.), on peut\néviter de stocker toute la table en ne\nconservant que les valeurs strictement\nnécessaires."
    },
    {
      "id": "q25",
      "difficulty": 3,
      "skills": [
        "synthese",
        "identification"
      ],
      "title": "Reconnaître un problème de programmation dynamique",
      "statement": "Pour qu'un problème se résolve efficacement par\nprogrammation dynamique, il doit posséder :",
      "options": [
        {
          "text": "Une décomposition logarithmique du problème",
          "correct": false,
          "feedback": "Cette caractéristique correspond plutôt au\nparadigme « diviser pour régner »\nclassique. Ce n'est pas un prérequis de la\nprogrammation dynamique.\n"
        },
        {
          "text": "Un seul cas de base et une seule récurrence",
          "correct": false,
          "feedback": "Le nombre de cas de base ou de récurrences\nn'est pas un critère. Ce sont les\npropriétés structurelles du problème qui\ncomptent.\n"
        },
        {
          "text": "Une fonction objectif uniquement minimale",
          "correct": false,
          "feedback": "La programmation dynamique s'applique aussi\nà des problèmes de maximisation (sac à dos,\nplus longue sous-séquence) ou de comptage\n(nombre de chemins).\n"
        },
        {
          "text": "La sous-structure optimale et des sous-problèmes recouvrants",
          "correct": true,
          "feedback": "Ces deux propriétés sont nécessaires. La\nsous-structure optimale permet de combiner\nles solutions, et les sous-problèmes\nrecouvrants rendent la mémoïsation utile.\n"
        }
      ],
      "explanation": "Ces deux propriétés sont l'identité de la\nprogrammation dynamique. Sans la sous-structure\noptimale, il est impossible de combiner les\nsolutions. Sans le recouvrement des\nsous-problèmes, la mémoïsation est inutile, et\nl'on retombe sur le paradigme « diviser pour\nrégner » classique."
    },
    {
      "id": "q26",
      "difficulty": 3,
      "skills": [
        "chemins-grille"
      ],
      "title": "Chemins dans une grille",
      "statement": "On veut compter le nombre de chemins\nmonotones (n'allant qu'à droite ou en bas)\nd'un coin d'une grille rectangulaire de $m$\nlignes et $n$ colonnes au coin opposé. Quelle\nest la récurrence appropriée et la complexité\npar programmation dynamique ?",
      "options": [
        {
          "text": "$C(i, j) = C(i-1, j) + C(i, j-1)$ avec\n$C(0, j) = C(i, 0) = 1$, complexité\n$O(m \\cdot n)$\n",
          "correct": true,
          "feedback": "Bonne réponse : pour atteindre la case\n$(i, j)$, on vient soit du dessus\n$(i-1, j)$ soit de la gauche $(i, j-1)$.\nLes cas de base sont les bords, où il\nn'existe qu'un seul chemin (descente ou\ndéplacement à droite uniquement). On\nremplit un tableau à deux dimensions de\ntaille $m \\times n$, chaque case en\n$O(1)$, donc complexité totale\n$O(m \\cdot n)$.\n"
        },
        {
          "text": "$C(i, j) = C(i-1, j-1) + 1$, complexité\n$O(m \\cdot n)$\n",
          "correct": false,
          "feedback": "Erreur : la récurrence proposée ne\ncorrespond pas au problème. On ne peut se\ndéplacer qu'à droite **ou** vers le bas,\npas en diagonale. La récurrence correcte\nadditionne les chemins venant de la case\ndu dessus et de la case de gauche.\n"
        },
        {
          "text": "$C(i, j) = C(i-1, j) \\cdot C(i, j-1)$,\ncomplexité $O(m + n)$\n",
          "correct": false,
          "feedback": "Erreur : pour compter le nombre de\nchemins, on **additionne** les\npossibilités, on ne les multiplie pas. La\ncomplexité est aussi sous-estimée :\nremplir un tableau à deux dimensions\ncoûte $O(m \\cdot n)$, pas $O(m + n)$.\n"
        },
        {
          "text": "$C(i, j) = i + j$, calcul direct en\n$O(1)$\n",
          "correct": false,
          "feedback": "Erreur : le nombre de chemins n'est pas la\nsomme des coordonnées mais bien le\ncoefficient binomial $\\binom{i+j}{i}$. Une\nformule fermée existe, mais elle se\ncalcule en $O(\\min(i, j))$ multiplications,\npas en temps constant.\n"
        }
      ],
      "explanation": "Ce problème est l'un des plus simples\nillustrant la programmation dynamique sur une\ntable à deux dimensions. La même technique\ns'étend aux variantes (cases bloquées,\nsommes minimales, etc.)."
    },
    {
      "id": "q27",
      "difficulty": 3,
      "skills": [
        "distance-edition"
      ],
      "title": "Distance d'édition",
      "statement": "La **distance d'édition** (ou distance de\nLevenshtein) entre deux chaînes mesure le\nnombre minimal d'opérations\n(insertion, suppression, substitution) pour\ntransformer l'une en l'autre. Quel est le\nschéma de programmation dynamique pour la\ncalculer ?",
      "options": [
        {
          "text": "$D(i, j) = \\max(i, j)$, calcul en $O(1)$\n",
          "correct": false,
          "feedback": "Erreur grossière : ce serait dire que la\ndistance d'édition est toujours la\nlongueur de la chaîne la plus longue. Or\ndeux chaînes identiques ont une distance\nde zéro, et la formule donnée n'en tient\npas compte.\n"
        },
        {
          "text": "$D(i, j) = D(i-1, j-1)$ si les deux\nderniers caractères sont égaux ; sinon,\n$D(i, j) = 1 + \\min(D(i-1, j),\nD(i, j-1), D(i-1, j-1))$. Complexité\n$O(m \\cdot n)$\n",
          "correct": true,
          "feedback": "Bonne réponse : trois opérations possibles\nse traduisent par trois récurrences\n(suppression, insertion, substitution),\nplus le cas où les caractères coïncident.\nCas de base : $D(0, j) = j$ et\n$D(i, 0) = i$. La complexité est\nquadratique, ce qui reste praticable pour\ndes chaînes de quelques milliers de\ncaractères.\n"
        },
        {
          "text": "On compare uniquement les premiers\ncaractères des deux chaînes. Complexité\n$O(1)$\n",
          "correct": false,
          "feedback": "Erreur : la distance d'édition prend en\ncompte **toutes** les positions, pas\nseulement la première. Une approche\n$O(1)$ ne donnerait évidemment pas le\nrésultat correct.\n"
        },
        {
          "text": "$D(i, j) = D(i-1, j-1) + 1$ dans tous les\ncas. Complexité $O(\\min(m, n))$\n",
          "correct": false,
          "feedback": "Erreur : cette récurrence ne traite pas\nséparément le cas où les caractères\ncoïncident (gratuit). Elle ignore aussi\nles opérations d'insertion et de\nsuppression, qui modifient la longueur\ndes chaînes. Le résultat serait\nsystématiquement faux.\n"
        }
      ],
      "explanation": "La distance d'édition est l'algorithme central\nde la commande `diff`, des correcteurs\northographiques, et de la bio-informatique\n(alignement de séquences). C'est un exemple\ntype de programmation dynamique sur une\ntable à deux dimensions."
    },
    {
      "id": "q28",
      "difficulty": 2,
      "skills": [
        "trace-monnaie"
      ],
      "title": "Trace sur le rendu de monnaie",
      "statement": "Avec les pièces $\\{1, 3, 4\\}$ et la somme à\nrendre $s = 6$, quel est le **nombre minimal**\nde pièces calculé par programmation dynamique\n(et l'écart avec la méthode gloutonne qui\nprend toujours la plus grosse pièce\npossible) ?",
      "options": [
        {
          "text": "La programmation dynamique trouve $1$\npièce, l'algorithme glouton en trouve $2$\n",
          "correct": false,
          "feedback": "Erreur : aucune pièce de valeur $6$\nn'existe dans le système monétaire\n$\\{1, 3, 4\\}$, donc une pièce ne peut pas\nsuffire. Le minimum atteignable est de\ndeux pièces (combinaison $3 + 3$).\n"
        },
        {
          "text": "La programmation dynamique et l'algorithme\nglouton trouvent tous deux $3$ pièces\n",
          "correct": false,
          "feedback": "Erreur : si c'était le cas, la\nprogrammation dynamique n'aurait aucun\nintérêt sur ce problème. Refaire le\ncalcul : avec $3 + 3$, deux pièces\nsuffisent, ce que l'algorithme glouton\nrate.\n"
        },
        {
          "text": "Les deux échouent car aucune combinaison\nne donne $6$\n",
          "correct": false,
          "feedback": "Erreur : la combinaison $3 + 3 = 6$ est\nparfaitement valide. De même\n$1 + 1 + 4 = 6$ ou $1 + 1 + 1 + 3 = 6$\nfonctionnent. Le problème est résoluble.\n"
        },
        {
          "text": "La programmation dynamique trouve $2$\npièces (la combinaison $3 + 3$), tandis\nque l'algorithme glouton en utilise $3$\n(la combinaison $4 + 1 + 1$)\n",
          "correct": true,
          "feedback": "Bonne réponse : c'est l'exemple\npédagogique classique où la stratégie\ngloutonne échoue à atteindre l'optimum.\nGlouton : prendre $4$, reste $2$, prendre\n$1$, prendre $1$, total $3$ pièces.\nProgrammation dynamique : explore toutes\nles combinaisons et trouve $3 + 3 = 6$,\nsoit seulement $2$ pièces. Cas\nparadigmatique de l'intérêt de la\nprogrammation dynamique.\n"
        }
      ],
      "explanation": "Cet exemple est emblématique : il prouve\nqu'avec un système monétaire arbitraire,\nl'algorithme glouton n'est plus optimal. Avec\nles systèmes monétaires usuels (euros,\ndollars), l'algorithme glouton fonctionne\ngrâce à des propriétés structurelles\nparticulières des dénominations choisies."
    },
    {
      "id": "q29",
      "difficulty": 3,
      "skills": [
        "sac-a-dos-trace"
      ],
      "title": "Trace du sac à dos",
      "statement": "On dispose d'un sac de capacité $W = 5$ et de\ntrois objets : $O_1$ (poids $2$, valeur $3$),\n$O_2$ (poids $3$, valeur $4$), $O_3$ (poids\n$4$, valeur $5$). Quelle est la valeur\noptimale obtenue par programmation\ndynamique ?",
      "options": [
        {
          "text": "$12$ (en prenant les trois objets)\n",
          "correct": false,
          "feedback": "Erreur : prendre les trois objets pèse\n$2 + 3 + 4 = 9 \\mathrm{kg}$, bien\nau-delà de la capacité du sac. C'est la\ndifficulté du sac à dos : on doit\nrenoncer à des objets précieux à cause\nde la contrainte de poids.\n"
        },
        {
          "text": "$7$ (en prenant $O_1$ et $O_2$, poids\ntotal $5$, valeur totale $7$)\n",
          "correct": true,
          "feedback": "Bonne réponse : on construit la table\n$V[i][w]$ donnant la valeur optimale en\nconsidérant les $i$ premiers objets pour\nune capacité $w$. Quelques valeurs clés :\n$V[2][5] = \\max(V[1][5], V[1][2] + 4)$\n$= \\max(3, 3 + 4) = 7$. À l'arrivée\n$V[3][5] = \\max(V[2][5], V[2][1] + 5)$\n$= \\max(7, 0 + 5) = 7$. La solution\noptimale prend $O_1$ ($2 \\mathrm{kg}$,\n$3$) et $O_2$ ($3 \\mathrm{kg}$, $4$),\npoids total $5$, valeur totale $7$.\n"
        },
        {
          "text": "$5$ (en prenant uniquement $O_3$)\n",
          "correct": false,
          "feedback": "Erreur : prendre seulement $O_3$ ne donne\nque $5$ de valeur, alors que la\ncombinaison $O_1 + O_2$ donne $7$ pour\nun poids identique au sac (capacité\natteinte exactement). Il vaut donc mieux\nprendre les deux petits objets que le\ngros.\n"
        },
        {
          "text": "$9$ (en prenant $O_2$ et $O_3$)\n",
          "correct": false,
          "feedback": "Erreur : la combinaison $O_2$ + $O_3$\npèse $3 + 4 = 7 \\mathrm{kg}$, ce qui\ndépasse la capacité $W = 5$ du sac.\nCette solution n'est donc pas réalisable.\n"
        }
      ],
      "explanation": "Construire le tableau $V$ à la main est un\nexcellent exercice. Il révèle que la\nprogrammation dynamique « teste » toutes les\ncombinaisons utiles sans en énumérer\nexplicitement aucune. Pour reconstruire la\nsolution (les objets choisis), on remonte\ndans le tableau à rebours en regardant\npour chaque case quelle décision a mené à\nl'optimum."
    },
    {
      "id": "q30",
      "difficulty": 3,
      "skills": [
        "code-sac-a-dos"
      ],
      "title": "Code du sac à dos en programmation dynamique",
      "statement": "Quel code Python implémente correctement la\nprogrammation dynamique ascendante (par\ntabulation) pour le sac à dos $0/1$ avec $n$\nobjets de poids `poids[i]` et valeurs\n`valeurs[i]` et un sac de capacité `W` ?",
      "options": [
        {
          "text": "```\ndef sac_a_dos(poids, valeurs, W):\n    total = 0\n    for i in range(len(poids)):\n        total += valeurs[i]\n    return total\n```\n",
          "correct": false,
          "feedback": "Erreur : ce code retourne la somme de\n**toutes** les valeurs sans tenir compte\nde la contrainte de poids. Il\nrenverrait par exemple $12$ pour le cas\nde la question précédente, alors que\nla vraie réponse est $7$.\n"
        },
        {
          "text": "```\ndef sac_a_dos(poids, valeurs, W):\n    n = len(poids)\n    V = [[0] * (W + 1) for _ in range(n + 1)]\n    for i in range(1, n + 1):\n        for w in range(W + 1):\n            if poids[i-1] <= w:\n                V[i][w] = max(V[i-1][w],\n                              V[i-1][w-poids[i-1]] + valeurs[i-1])\n            else:\n                V[i][w] = V[i-1][w]\n    return V[n][W]\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : on construit un tableau à\ndeux dimensions de taille\n$(n+1) \\times (W+1)$. Pour chaque case\n$(i, w)$ : si l'objet $i$ rentre dans le\nsac (`poids[i-1] <= w`), on prend le\nmaximum entre « ne pas le prendre »\n(`V[i-1][w]`) et « le prendre »\n(`V[i-1][w-poids[i-1]] + valeurs[i-1]`) ;\nsinon, on ne peut pas le prendre. Indices\n$i-1$ car les listes Python commencent\nà $0$.\n"
        },
        {
          "text": "```\ndef sac_a_dos(poids, valeurs, W):\n    # Tri par ratio valeur/poids décroissant\n    ordre = sorted(range(len(poids)),\n                   key=lambda i: -valeurs[i]/poids[i])\n    total = 0\n    capa = W\n    for i in ordre:\n        if poids[i] <= capa:\n            total += valeurs[i]\n            capa -= poids[i]\n    return total\n```\n",
          "correct": false,
          "feedback": "Erreur : c'est l'algorithme **glouton** par\nratio valeur/poids. Il est optimal pour\nle sac à dos **fractionnaire** (où l'on\npeut couper des objets), mais pas pour\nla version $0/1$. Il peut renvoyer une\nvaleur sous-optimale, comme on l'a vu\ndans la question précédente.\n"
        },
        {
          "text": "```\ndef sac_a_dos(poids, valeurs, W):\n    return max(valeurs)\n```\n",
          "correct": false,
          "feedback": "Erreur : ce code renvoie la valeur de\nl'objet le plus précieux, sans même\nvérifier qu'il rentre dans le sac. Et\nsurtout, il ignore les combinaisons\nd'objets, qui sont souvent meilleures.\n"
        }
      ],
      "explanation": "Complexité : $O(n \\cdot W)$ en temps et en\nespace. Optimisation possible : ne conserver\nque la ligne courante et la précédente,\nramenant l'espace à $O(W)$. Pour\nreconstruire la solution effective, on\nremonte le tableau de la case $V[n][W]$ vers\nla case $V[0][0]$, en notant à chaque\nétape si l'objet $i$ a été pris."
    },
    {
      "id": "q31",
      "difficulty": 3,
      "skills": [
        "memoisation-vs-tabulation"
      ],
      "title": "Mesurer le gain de la mémoïsation",
      "statement": "On compare le calcul de `fibonacci(40)` avec\nla version récursive naïve et avec la\nversion mémoïsée. Quel est l'ordre de\ngrandeur du rapport entre les deux\ncomplexités ?",
      "options": [
        {
          "text": "Les deux versions ont la même complexité\nasymptotique\n",
          "correct": false,
          "feedback": "Erreur : la mémoïsation change\nfondamentalement la complexité. La\nversion naïve calcule plusieurs fois\nles mêmes valeurs (par exemple,\n$\\mathrm{fib}(35)$ est calculée\nplusieurs millions de fois) ; la\nmémoïsée ne calcule chaque valeur qu'une\nseule fois.\n"
        },
        {
          "text": "La version mémoïsée est environ deux fois\nplus rapide\n",
          "correct": false,
          "feedback": "Erreur : un facteur deux serait une\namélioration mineure. Or le passage de\n$O(2^n)$ à $O(n)$ représente un gain\n**exponentiel**, pas multiplicatif. Pour\n$n = 40$, c'est un facteur de plus de\ndix milliards.\n"
        },
        {
          "text": "La version mémoïsée est plus lente à\ncause du surcoût du cache\n",
          "correct": false,
          "feedback": "Erreur : pour $n$ aussi petit que $20$,\nle surcoût du cache est négligeable\ndevant le gain en évitant les calculs\nredondants. Pour $n = 40$, le gain est\ncolossal.\n"
        },
        {
          "text": "La version mémoïsée est environ\n$10^{10}$ fois plus rapide\n",
          "correct": true,
          "feedback": "Bonne réponse : version naïve en\n$O(2^n)$, soit environ\n$2^{40} \\approx 10^{12}$ appels\nrécursifs. Version mémoïsée en $O(n)$,\nsoit environ $40$ appels effectifs. Le\nrapport est donc d'environ\n$10^{12} / 40 \\approx 2{,}5 \\cdot 10^{10}$.\nEn pratique, on passe de plusieurs\nminutes à une fraction de milliseconde.\n"
        }
      ],
      "explanation": "Cet exemple illustre la puissance de la\nprogrammation dynamique. Bien sûr, pour\nFibonacci en pratique, on utilise plutôt la\nversion itérative à deux variables, qui a\nla même complexité $O(n)$ mais avec une\nconsommation mémoire $O(1)$ au lieu de\n$O(n)$."
    },
    {
      "id": "q32",
      "difficulty": 3,
      "skills": [
        "optimisation-espace"
      ],
      "title": "Optimisation de l'espace pour le sac à dos",
      "statement": "Le sac à dos en programmation dynamique\nutilise par défaut un tableau $V$ de taille\n$(n+1) \\times (W+1)$, soit un espace\n$O(n \\cdot W)$. Comment réduire cet\nespace à $O(W)$ tout en gardant la même\ncomplexité en temps ?",
      "options": [
        {
          "text": "On observe que pour calculer la ligne\n$V[i]$, on n'a besoin que de la ligne\n$V[i-1]$. On peut donc se contenter de\ndeux lignes (la précédente et la\ncourante), voire d'une seule en\nparcourant les capacités de droite à\ngauche\n",
          "correct": true,
          "feedback": "Bonne réponse : c'est une optimisation\nclassique. La récurrence ne dépend que\nde la ligne précédente, donc deux\ntableaux de taille $W+1$ suffisent.\nMieux : avec un seul tableau parcouru à\nrebours (`for w in range(W, poids[i]-1,\n-1)`), on garantit qu'on utilise bien\nl'ancienne valeur de $V[w-poids[i]]$\navant qu'elle ne soit écrasée par la\nligne en cours. Espace final : $O(W)$.\n"
        },
        {
          "text": "On utilise un dictionnaire au lieu d'un\ntableau\n",
          "correct": false,
          "feedback": "Erreur : un dictionnaire peut éviter de\nstocker des cases inutilisées (avec une\nmémoïsation paresseuse), mais dans le\npire cas, on stocke toujours $O(n\n\\cdot W)$ entrées. Le gain n'est pas\nsystématique.\n"
        },
        {
          "text": "On ne peut pas réduire l'espace en\ndessous de $O(n \\cdot W)$\n",
          "correct": false,
          "feedback": "Erreur : la réduction à $O(W)$ est\npossible et constitue une optimisation\nstandard. Elle est cependant payée par\nla perte de la trace permettant de\n**reconstruire** la solution effective :\nsi l'on n'a plus que la ligne courante,\non ne peut plus remonter dans le\ntableau à rebours.\n"
        },
        {
          "text": "On compresse les valeurs avec un\nalgorithme de compression\n",
          "correct": false,
          "feedback": "Erreur : la compression peut réduire la\ntaille en mémoire d'un facteur constant,\nmais ne change pas la complexité\nasymptotique. L'optimisation correcte\nest structurelle : ne stocker que ce\nqui est utile pour le calcul courant.\n"
        }
      ],
      "explanation": "Compromis : la réduction à $O(W)$ permet de\ntraiter des sacs à dos avec des capacités\n$W$ très grandes (typiquement plusieurs\nmillions), au prix de la perte d'information\npour reconstruire la solution. Si on a\nbesoin des deux (faible mémoire **et**\nreconstruction), des techniques plus\navancées existent, comme le « partage par le\nmilieu » qui combine deux passes sur des\nmoitiés du tableau."
    }
  ]
}