{
  "chapter": {
    "id": "recursivite",
    "level": "terminale",
    "theme": "Algorithmique",
    "title": "Récursivité",
    "description": "Définition d'une fonction récursive (cas de base, appel récursif),\ndéroulement de la pile d'appels, terminaison, exemples classiques\n(factorielle, Fibonacci, somme, parcours d'arbre, tours de Hanoï),\nrécursivité simple vs double, complexité et mémoïsation.",
    "prerequisites": [],
    "references": []
  },
  "questions": [
    {
      "id": "q01",
      "difficulty": 1,
      "skills": [
        "definition"
      ],
      "title": "Définition d'une fonction récursive",
      "statement": "Quelle propriété caractérise une fonction récursive ?",
      "options": [
        {
          "text": "Elle s'appelle elle-même, avec au moins un cas de base qui ne fait pas d'appel récursif",
          "correct": true,
          "feedback": "Bonne réponse : une fonction récursive est définie par un ou\nplusieurs cas de base (qui arrêtent la récursion) et un ou\nplusieurs appels à elle-même sur un sous-problème plus petit.\n"
        },
        {
          "text": "Elle prend obligatoirement deux paramètres : une valeur et une accumulation",
          "correct": false,
          "feedback": "Erreur : une fonction récursive peut n'avoir qu'un seul\nparamètre (par exemple `factorielle(n)`). L'accumulateur est\nun schéma possible, pas une obligation.\n"
        },
        {
          "text": "Elle renvoie obligatoirement une liste qui se construit pas à pas",
          "correct": false,
          "feedback": "Erreur : la valeur de retour peut être de n'importe quel type\n(entier, booléen, chaîne, liste, etc.).\n"
        },
        {
          "text": "Elle utilise une boucle `while` qui décompte une variable",
          "correct": false,
          "feedback": "Erreur : c'est la définition d'une fonction itérative. La\nrécursivité remplace justement les boucles par des appels\nsuccessifs de la fonction à elle-même.\n"
        }
      ],
      "explanation": "Une fonction récursive comporte deux ingrédients indispensables :\nau moins un **cas de base** (qui renvoie une valeur sans appel\nrécursif) et un ou plusieurs **appels récursifs** sur un\nsous-problème strictement plus petit, pour garantir la\nterminaison."
    },
    {
      "id": "q02",
      "difficulty": 1,
      "skills": [
        "terminaison",
        "cas-de-base"
      ],
      "title": "Nécessité du cas de base",
      "statement": "Que se passe-t-il en Python si une fonction récursive ne possède\naucun cas de base ?",
      "options": [
        {
          "text": "Elle renvoie systématiquement `None`",
          "correct": false,
          "feedback": "Erreur : `None` est la valeur renvoyée par défaut quand une\nfonction n'a pas d'instruction `return`. Ici, le problème\nest la non-terminaison, pas la valeur de retour.\n"
        },
        {
          "text": "Elle renvoie $0$",
          "correct": false,
          "feedback": "Erreur : aucune valeur de retour spéciale n'est associée à\nl'absence de cas de base.\n"
        },
        {
          "text": "Elle s'exécute infiniment et finit par lever une erreur `RecursionError`",
          "correct": true,
          "feedback": "Bonne réponse : Python limite la profondeur de la pile\nd'appels (par défaut autour de $1000$). Sans cas de base,\nla fonction s'auto-appelle indéfiniment et provoque cette\nerreur.\n"
        },
        {
          "text": "Le compilateur la rejette avant exécution",
          "correct": false,
          "feedback": "Erreur : Python n'analyse pas la sémantique de la fonction\navant de l'exécuter. Sans cas de base, l'erreur survient à\nl'exécution, pas à la compilation.\n"
        }
      ],
      "explanation": "Le cas de base est ce qui garantit la terminaison de la\nrécursion. Sans lui, on a une boucle infinie qui empile des\nappels jusqu'à dépassement de la pile."
    },
    {
      "id": "q03",
      "difficulty": 1,
      "skills": [
        "pile-appels"
      ],
      "title": "Rôle de la pile d'appels",
      "statement": "Quand une fonction `f` s'appelle récursivement, que stocke la\npile d'appels pendant l'exécution ?",
      "options": [
        {
          "text": "Une copie complète de la mémoire à chaque appel",
          "correct": false,
          "feedback": "Erreur : ce serait inefficace et inutile. Seules les\ninformations propres à l'appel courant sont empilées.\n"
        },
        {
          "text": "Le contexte de chaque appel : paramètres, variables locales et point de retour",
          "correct": true,
          "feedback": "Bonne réponse : à chaque appel, Python empile un cadre\nd'exécution contenant les paramètres, les variables locales\net l'instruction où reprendre une fois l'appel terminé. Au\nretour, ce cadre est dépilé.\n"
        },
        {
          "text": "Uniquement la valeur de retour de chaque appel",
          "correct": false,
          "feedback": "Erreur : la valeur de retour n'est connue qu'à la **fin** de\nchaque appel. La pile sert justement à conserver l'état\npendant que les appels imbriqués se déroulent.\n"
        },
        {
          "text": "Le code source de la fonction, dupliqué à chaque appel",
          "correct": false,
          "feedback": "Erreur : le code source de la fonction n'est stocké qu'une\nseule fois. La pile contient seulement le **contexte\nd'exécution** de chaque appel.\n"
        }
      ],
      "explanation": "C'est la limite physique de la pile (typiquement quelques\nmilliers d'appels) qui rend les récursions très profondes\nproblématiques en Python : un parcours linéaire d'une liste de\n$10\\ 000$ éléments par récursion plante avec\n`RecursionError`."
    },
    {
      "id": "q04",
      "difficulty": 1,
      "skills": [
        "evaluation"
      ],
      "title": "Évaluation d'une récurrence simple",
      "statement": "On considère la fonction Python suivante :\n\n```\ndef f(n):\n    if n == 0:\n        return 1\n    return 2 * f(n - 1)\n```\n\nQue renvoie l'appel `f(4)` ?",
      "options": [
        {
          "text": "$32$",
          "correct": false,
          "feedback": "Erreur : c'est $f(5) = 2^5$. Une multiplication en trop.\n"
        },
        {
          "text": "$16$",
          "correct": true,
          "feedback": "Bonne réponse : $f(4) = 2 \\cdot f(3) = 2 \\cdot 2 \\cdot f(2) = \\ldots = 2^4 \\cdot f(0) = 2^4 = 16$.\n"
        },
        {
          "text": "$4$",
          "correct": false,
          "feedback": "Erreur : confusion entre l'argument $n$ et la valeur de\nretour. Ici $f(4) = 2^4$, pas $4$.\n"
        },
        {
          "text": "$8$",
          "correct": false,
          "feedback": "Erreur : c'est $f(3) = 2^3$. Le calcul de $f(4)$ ajoute une\nmultiplication par $2$.\n"
        }
      ],
      "explanation": "Cette fonction calcule $2^n$. La récurrence $f(n) = 2 \\cdot f(n-1)$\navec $f(0) = 1$ donne immédiatement $f(n) = 2^n$."
    },
    {
      "id": "q05",
      "difficulty": 1,
      "skills": [
        "factorielle"
      ],
      "title": "Identifier le cas de base de la factorielle",
      "statement": "On définit `factorielle` ainsi :\n\n```\ndef factorielle(n):\n    if n <= 1:\n        return 1\n    return n * factorielle(n - 1)\n```\n\nQuel est le rôle de la condition `if n <= 1: return 1` ?",
      "options": [
        {
          "text": "Elle assure que `n` reste positif pendant l'exécution",
          "correct": false,
          "feedback": "Erreur : la condition n'a pas d'effet sur les autres appels.\nSon rôle est d'arrêter la récursion quand $n$ atteint $1$.\n"
        },
        {
          "text": "Elle initialise une variable de comptage",
          "correct": false,
          "feedback": "Erreur : aucune variable n'est initialisée. C'est une\ncondition de fin qui interrompt la récursion.\n"
        },
        {
          "text": "C'est le cas de base, qui arrête la récursion sans nouvel appel",
          "correct": true,
          "feedback": "Bonne réponse : c'est le cas de base. Pour $n \\leq 1$, la\nfonction renvoie $1$ directement, sans appel récursif. C'est\nce qui garantit la terminaison.\n"
        },
        {
          "text": "Elle gère uniquement les cas d'erreur quand `n` est négatif",
          "correct": false,
          "feedback": "Erreur : la condition couvre $n \\leq 1$ (donc aussi $n = 0$\net $n = 1$), pas seulement les cas négatifs. Elle joue un\nrôle algorithmique, pas seulement défensif.\n"
        }
      ],
      "explanation": "Sans ce cas de base, la fonction continuerait à décrémenter $n$\nindéfiniment, en passant par $0, -1, -2, \\ldots$, et\nprovoquerait une `RecursionError`. Le cas de base est l'élément\ncentral de toute récursion bien définie."
    },
    {
      "id": "q06",
      "difficulty": 1,
      "skills": [
        "identification"
      ],
      "title": "Identifier le cas de base de l'algorithme d'Euclide",
      "statement": "Dans la fonction suivante, qui calcule le PGCD de deux entiers,\nquel est le cas de base ?\n\n```\ndef pgcd(a, b):\n    if b == 0:\n        return a\n    return pgcd(b, a % b)\n```",
      "options": [
        {
          "text": "`a == 0`",
          "correct": false,
          "feedback": "Erreur : la condition d'arrêt teste `b`, pas `a`.\nL'algorithme d'Euclide réduit progressivement `b` vers $0$.\n"
        },
        {
          "text": "`b == 0`",
          "correct": true,
          "feedback": "Bonne réponse : quand `b` vaut $0$, la fonction renvoie `a`\ndirectement, sans nouvel appel. C'est le cas de base.\n"
        },
        {
          "text": "`a == b`",
          "correct": false,
          "feedback": "Erreur : ce cas n'est pas géré explicitement. Dans cet\nalgorithme, l'égalité `a == b` mène à `pgcd(b, 0)` au prochain\nappel, qui déclenche le vrai cas de base.\n"
        },
        {
          "text": "`a % b == 0`",
          "correct": false,
          "feedback": "Erreur : ce n'est pas la condition utilisée. Si `a % b == 0`,\nl'appel récursif suivant sera `pgcd(b, 0)`, qui déclenchera\nle vrai cas de base à l'appel suivant.\n"
        }
      ],
      "explanation": "L'algorithme d'Euclide repose sur la propriété\n$\\text{PGCD}(a, b) = \\text{PGCD}(b, a \\mod b)$, avec le cas de\nbase $\\text{PGCD}(a, 0) = a$. À chaque appel, $b$ diminue\nstrictement (puisque $a \\mod b < b$), garantissant la\nterminaison."
    },
    {
      "id": "q07",
      "difficulty": 1,
      "skills": [
        "recursivite-simple-vs-double"
      ],
      "title": "Récursivité simple ou récursivité double",
      "statement": "Une fonction qui contient **deux appels récursifs** sur des\nsous-problèmes différents est dite :",
      "options": [
        {
          "text": "Une récursivité double (ou multiple)",
          "correct": true,
          "feedback": "Bonne réponse : c'est typique du parcours d'arbres binaires\n(un appel pour le sous-arbre gauche, un pour le droit), de\nFibonacci ($f(n-1) + f(n-2)$) ou de la stratégie « diviser\npour régner ».\n"
        },
        {
          "text": "Une récursivité terminale",
          "correct": false,
          "feedback": "Erreur : la récursivité terminale désigne un cas où l'appel\nrécursif est la **dernière** opération de la fonction\n(optimisable par certains compilateurs).\n"
        },
        {
          "text": "Une récursivité simple",
          "correct": false,
          "feedback": "Erreur : on parle de récursivité simple quand la fonction ne\ncontient **qu'un seul** appel récursif (par exemple\n`factorielle`).\n"
        },
        {
          "text": "Une récursivité croisée",
          "correct": false,
          "feedback": "Erreur : la récursivité croisée concerne deux fonctions qui\ns'appellent mutuellement (par exemple `pair(n)` et\n`impair(n)`), pas deux appels d'une même fonction.\n"
        }
      ],
      "explanation": "La distinction simple/double est importante pour la complexité :\nune récursivité simple a typiquement une complexité linéaire,\ntandis qu'une récursivité double naïve a une complexité\nexponentielle (cas de Fibonacci sans mémoïsation)."
    },
    {
      "id": "q08",
      "difficulty": 1,
      "skills": [
        "terminaison",
        "decroissance"
      ],
      "title": "Garantir la terminaison",
      "statement": "Quelle propriété l'argument d'un appel récursif doit-il vérifier\npour garantir que la fonction termine ?",
      "options": [
        {
          "text": "Il doit être un entier",
          "correct": false,
          "feedback": "Erreur : ce n'est pas une condition. Les arguments peuvent\nêtre des chaînes, des listes, des arbres, etc. Ce qui compte,\nc'est qu'ils diminuent en « taille » à chaque appel.\n"
        },
        {
          "text": "Il doit augmenter strictement à chaque appel",
          "correct": false,
          "feedback": "Erreur : si l'argument augmente, on s'éloigne du cas de\nbase au lieu de s'en rapprocher. La fonction ne termine\njamais.\n"
        },
        {
          "text": "Il doit rester constant tant que le cas de base n'est pas atteint",
          "correct": false,
          "feedback": "Erreur : si l'argument reste constant, la fonction se rappelle\navec les mêmes valeurs et ne termine jamais.\n"
        },
        {
          "text": "Il doit décroître strictement à chaque appel, vers le cas de base",
          "correct": true,
          "feedback": "Bonne réponse : pour qu'une fonction récursive termine, il\nfaut que la suite des arguments des appels successifs\natteigne le cas de base en un nombre fini d'étapes. La\ndécroissance stricte d'une quantité positive (la « variant »)\nassure cette terminaison.\n"
        }
      ],
      "explanation": "En toute rigueur, on associe à chaque appel un entier appelé\n« variant », strictement décroissant et minoré par $0$. Pour\n`factorielle(n)`, le variant est $n$. Pour une fonction sur\n`L[1:]`, c'est `len(L)`."
    },
    {
      "id": "q09",
      "difficulty": 1,
      "skills": [
        "identification",
        "lecture"
      ],
      "title": "Lecture d'une récursion sur les chaînes",
      "statement": "Dans la fonction suivante, quel est le cas de base ?\n\n```\ndef compte(chaine):\n    if chaine == \"\":\n        return 0\n    return 1 + compte(chaine[1:])\n```",
      "options": [
        {
          "text": "`chaine == \"\"`",
          "correct": true,
          "feedback": "Bonne réponse : quand la chaîne est vide, la fonction renvoie\n$0$ sans nouvel appel. C'est le cas de base, qui arrête la\nrécursion.\n"
        },
        {
          "text": "`1 + compte(chaine[1:])`",
          "correct": false,
          "feedback": "Erreur : c'est l'instruction de l'appel récursif. Le cas de\nbase est la branche qui ne fait **pas** d'appel.\n"
        },
        {
          "text": "La fonction n'a pas de cas de base",
          "correct": false,
          "feedback": "Erreur : la branche `if chaine == \"\": return 0` arrête bien\nla récursion sans nouvel appel.\n"
        },
        {
          "text": "`chaine[1:]`",
          "correct": false,
          "feedback": "Erreur : `chaine[1:]` est l'argument de l'appel récursif, pas\nle cas de base. Le cas de base est ce qui **arrête** la\nrécursion.\n"
        }
      ],
      "explanation": "Cette fonction calcule en réalité la longueur de la chaîne. Le\ncas de base est la chaîne vide (longueur $0$) ; chaque appel\nrécursif retire le premier caractère et ajoute $1$."
    },
    {
      "id": "q10",
      "difficulty": 1,
      "skills": [
        "definition",
        "valeurs"
      ],
      "title": "Récursivité et définition par récurrence",
      "statement": "Quelle suite mathématique peut être implémentée le plus\nnaturellement par une fonction récursive simple (un seul appel\nrécursif par cas) ?",
      "options": [
        {
          "text": "Une suite quelconque, tirée au hasard",
          "correct": false,
          "feedback": "Erreur : il faut une définition claire (cas initial et\nrelation de récurrence) pour pouvoir écrire une fonction\nrécursive.\n"
        },
        {
          "text": "Une suite définie par $u_n = f(u_{n-1}, u_{n-2})$",
          "correct": false,
          "feedback": "Erreur : cette définition demande **deux** valeurs\nprécédentes, donc deux appels récursifs (récursivité double).\nLa suite de Fibonacci en est l'exemple classique.\n"
        },
        {
          "text": "Une suite définie par $u_0$ et $u_{n} = f(u_{n-1})$",
          "correct": true,
          "feedback": "Bonne réponse : on a un cas de base ($u_0$) et un appel\nrécursif (calcul de $u_n$ à partir de $u_{n-1}$). La\nstructure correspond exactement à une récursion simple.\n"
        },
        {
          "text": "Une suite définie uniquement par sa valeur initiale",
          "correct": false,
          "feedback": "Erreur : sans relation de récurrence, il n'y a pas d'appel\nrécursif à faire. Une seule valeur ne nécessite pas de\nfonction récursive.\n"
        }
      ],
      "explanation": "La récursion simple correspond aux suites du premier ordre\n($u_n$ dépend uniquement de $u_{n-1}$). Une récurrence d'ordre\n$2$ (Fibonacci) demande une récursion double, plus coûteuse en\ntemps si on n'utilise pas de mémoïsation."
    },
    {
      "id": "q11",
      "difficulty": 2,
      "skills": [
        "pile-appels",
        "deroulement"
      ],
      "title": "Déroulement d'une somme récursive",
      "statement": "On considère :\n\n```\ndef somme(L):\n    if L == []:\n        return 0\n    return L[0] + somme(L[1:])\n```\n\nQue renvoie `somme([3, 5, 2])` ?",
      "options": [
        {
          "text": "$5$",
          "correct": false,
          "feedback": "Erreur : c'est l'élément du milieu de la liste, mais la\nfonction additionne **tous** les éléments.\n"
        },
        {
          "text": "$30$",
          "correct": false,
          "feedback": "Erreur : confusion possible avec un produit ou un calcul\nerroné. La fonction additionne les éléments, elle ne les\nmultiplie pas.\n"
        },
        {
          "text": "$0$",
          "correct": false,
          "feedback": "Erreur : c'est la valeur renvoyée pour la **liste vide**, qui\nintervient au plus profond de la pile, mais qui est ensuite\nadditionnée aux éléments dépilés.\n"
        },
        {
          "text": "$10$",
          "correct": true,
          "feedback": "Bonne réponse : $3 + 5 + 2 = 10$. En déroulant :\n$3 + \\text{somme}([5,2]) = 3 + (5 + \\text{somme}([2])) = 3 + (5 + (2 + 0)) = 10$.\n"
        }
      ],
      "explanation": "Cette fonction est l'archétype du parcours récursif d'une liste :\ncas de base sur la liste vide ($0$), et appel récursif sur le\nreste de la liste après extraction du premier élément."
    },
    {
      "id": "q12",
      "difficulty": 2,
      "skills": [
        "pile-appels",
        "evaluation"
      ],
      "title": "Compter les chiffres d'un entier",
      "statement": "On considère :\n\n```\ndef nb_chiffres(n):\n    if n < 10:\n        return 1\n    return 1 + nb_chiffres(n // 10)\n```\n\nQue renvoie `nb_chiffres(4073)` ?",
      "options": [
        {
          "text": "$3$",
          "correct": false,
          "feedback": "Erreur : oubli d'un chiffre. L'entier $4073$ comporte $4$\nchiffres : $4, 0, 7, 3$.\n"
        },
        {
          "text": "$4$",
          "correct": true,
          "feedback": "Bonne réponse : on déroule $1 + \\text{nb\\_chiffres}(407) = 1 + 1 + \\text{nb\\_chiffres}(40) = 1 + 1 + 1 + \\text{nb\\_chiffres}(4) = 1 + 1 + 1 + 1 = 4$.\n"
        },
        {
          "text": "$10$",
          "correct": false,
          "feedback": "Erreur : confusion possible avec la base $10$ utilisée pour\nextraire les chiffres. La fonction compte le nombre de\ndivisions par $10$ nécessaires pour atteindre un nombre à un\nchiffre.\n"
        },
        {
          "text": "$5$",
          "correct": false,
          "feedback": "Erreur : un chiffre en trop. Quand $n < 10$, la fonction\nrenvoie $1$ et n'effectue pas d'appel supplémentaire.\n"
        }
      ],
      "explanation": "La division entière par $10$ retire le dernier chiffre. Le cas\nde base $n < 10$ correspond à un nombre à un seul chiffre\n(longueur $1$). Cette technique est très utile pour parcourir un\nentier chiffre par chiffre sans le convertir en chaîne."
    },
    {
      "id": "q13",
      "difficulty": 2,
      "skills": [
        "completer",
        "syntaxe"
      ],
      "title": "Compléter une fonction récursive",
      "statement": "Pour que la fonction suivante renvoie le nombre d'occurrences\nd'un caractère `c` dans une chaîne `s`, par quoi faut-il\nremplacer les pointillés ?\n\n```\ndef occurrences(s, c):\n    if s == \"\":\n        return ...\n    if s[0] == c:\n        return 1 + occurrences(s[1:], c)\n    return occurrences(s[1:], c)\n```",
      "options": [
        {
          "text": "$0$",
          "correct": true,
          "feedback": "Bonne réponse : une chaîne vide contient zéro occurrence de\nn'importe quel caractère. C'est la valeur neutre pour la\nsomme finale.\n"
        },
        {
          "text": "`None`",
          "correct": false,
          "feedback": "Erreur : `None` ne pourrait pas s'additionner avec $1$ dans\nla branche récursive, et la fonction est censée renvoyer un\ncomptage.\n"
        },
        {
          "text": "$1$",
          "correct": false,
          "feedback": "Erreur : une chaîne vide ne contient aucune occurrence d'un\ncaractère, le cas de base doit donc renvoyer $0$.\n"
        },
        {
          "text": "`c`",
          "correct": false,
          "feedback": "Erreur : la fonction est censée renvoyer un entier (un\ncomptage), pas un caractère.\n"
        }
      ],
      "explanation": "Le cas de base d'un comptage récursif est presque toujours $0$\n(aucun élément trouvé dans la structure vide). La récursion\najoute ensuite $1$ chaque fois qu'on rencontre un élément\ncherché."
    },
    {
      "id": "q14",
      "difficulty": 2,
      "skills": [
        "implementation",
        "listes"
      ],
      "title": "Maximum d'une liste sans `max`",
      "statement": "On souhaite écrire récursivement le maximum d'une liste non vide.\nQuel est le **cas de base** approprié ?\n\n```\ndef maximum(L):\n    if ...:\n        return L[0]\n    ...\n```",
      "options": [
        {
          "text": "`L[0] > L[-1]`",
          "correct": false,
          "feedback": "Erreur : c'est une comparaison entre deux éléments, pas un\ncritère d'arrêt sur la taille de la liste.\n"
        },
        {
          "text": "`L == []`",
          "correct": false,
          "feedback": "Erreur : sur une liste vide, `L[0]` provoquerait une erreur\n(`IndexError`). Le cas de base doit garantir que `L[0]`\nexiste.\n"
        },
        {
          "text": "`len(L) == 2`",
          "correct": false,
          "feedback": "Erreur : on n'a alors pas couvert le cas $\\text{len}(L) = 1$,\nqui peut survenir comme appel récursif terminal. La fonction\nne terminerait pas correctement.\n"
        },
        {
          "text": "`len(L) == 1`",
          "correct": true,
          "feedback": "Bonne réponse : le maximum d'une liste à un seul élément est\ncet élément lui-même. C'est le cas de base le plus naturel\npour cette fonction.\n"
        }
      ],
      "explanation": "Une fois le cas de base posé (`len(L) == 1`), l'appel récursif\ncompare le premier élément au maximum du reste de la liste :\n`m = maximum(L[1:])` puis renvoie `L[0] if L[0] > m else m`."
    },
    {
      "id": "q15",
      "difficulty": 2,
      "skills": [
        "implementation",
        "miroir"
      ],
      "title": "Inversion récursive d'une liste",
      "statement": "Quelle expression complète correctement la fonction `miroir`\nci-dessous, qui doit renvoyer la liste inversée ?\n\n```\ndef miroir(L):\n    if L == []:\n        return []\n    return ...\n```",
      "options": [
        {
          "text": "`miroir(L[1:]) + [L[0]]`",
          "correct": true,
          "feedback": "Bonne réponse : on inverse récursivement le reste de la\nliste, puis on place `L[0]` à la fin. Exemple :\n$[1, 2, 3] \\to \\text{miroir}([2, 3]) + [1] = [3, 2] + [1] = [3, 2, 1]$.\n"
        },
        {
          "text": "`miroir(L[0]) + miroir(L[1:])`",
          "correct": false,
          "feedback": "Erreur : `L[0]` est un élément simple, pas une liste : on ne\npeut pas l'inverser récursivement.\n"
        },
        {
          "text": "`miroir(L)[1:]`",
          "correct": false,
          "feedback": "Erreur : cette définition est circulaire (la fonction\ns'appelle sur la même valeur), donc ne termine jamais.\n"
        },
        {
          "text": "`[L[0]] + miroir(L[1:])`",
          "correct": false,
          "feedback": "Erreur : cette construction renverrait la liste **inchangée**.\nPour l'inverser, l'élément `L[0]` doit aller à la fin, pas au\ndébut.\n"
        }
      ],
      "explanation": "L'idée centrale : pour inverser une liste, on inverse la queue\net on y accole la tête. Cette construction est élégante mais\ncoûteuse en mémoire (à chaque appel, on crée une nouvelle liste\navec `+`)."
    },
    {
      "id": "q16",
      "difficulty": 2,
      "skills": [
        "evaluation",
        "suite"
      ],
      "title": "Croissance d'une population",
      "statement": "Un biologiste modélise une population de bactéries qui double\nchaque heure, en partant d'une seule bactérie. Si la fonction\nrécursive est :\n\n```\ndef population(n):\n    if n == 0:\n        return 1\n    return 2 * population(n - 1)\n```\n\nCombien de bactéries y a-t-il après $10$ heures ?",
      "options": [
        {
          "text": "$100$",
          "correct": false,
          "feedback": "Erreur : la croissance n'est pas géométrique de raison $10$.\nChaque heure, la population est multipliée par $2$, pas par\n$10$.\n"
        },
        {
          "text": "$1024$",
          "correct": true,
          "feedback": "Bonne réponse : $\\text{population}(10) = 2^{10} = 1024$. Une\nfois la fonction reconnue comme calculant $2^n$, on applique\nla formule directement.\n"
        },
        {
          "text": "$20$",
          "correct": false,
          "feedback": "Erreur : confusion avec une croissance linéaire ($2 \\cdot n$).\nOr la population **double**, donc on a une croissance\nexponentielle.\n"
        },
        {
          "text": "$2048$",
          "correct": false,
          "feedback": "Erreur : c'est $2^{11}$, soit population après $11$ heures,\npas $10$.\n"
        }
      ],
      "explanation": "Cet exemple illustre la croissance exponentielle : la population\ndouble à chaque pas. Après $20$ heures, on aurait\n$2^{20} \\approx 10^6$ bactéries ; après $30$ heures, plus d'un\nmilliard."
    },
    {
      "id": "q17",
      "difficulty": 2,
      "skills": [
        "parcours",
        "arbre"
      ],
      "title": "Récursivité sur un arbre binaire",
      "statement": "On parcourt un arbre binaire avec la fonction :\n\n```\ndef cherche(elt, arbre):\n    if arbre is None:\n        return False\n    if arbre.valeur == elt:\n        return True\n    return cherche(elt, arbre.gauche) or cherche(elt, arbre.droite)\n```\n\nCombien d'appels récursifs cette fonction génère-t-elle au\nmaximum dans une seule exécution ?",
      "options": [
        {
          "text": "Aucun, c'est une fonction itérative déguisée",
          "correct": false,
          "feedback": "Erreur : la fonction contient bien deux appels récursifs\n(`cherche(elt, arbre.gauche)` et\n`cherche(elt, arbre.droite)`). Elle n'est pas itérative.\n"
        },
        {
          "text": "Deux, un sur chaque sous-arbre",
          "correct": true,
          "feedback": "Bonne réponse : c'est de la récursivité double. Chaque appel\npeut générer deux nouveaux appels (gauche et droit), ce qui\nexplore exhaustivement l'arbre dans le pire cas.\n"
        },
        {
          "text": "Autant que d'éléments dans la liste représentant l'arbre",
          "correct": false,
          "feedback": "Erreur : la fonction n'est pas écrite sur une liste mais sur\nune structure d'arbre. Chaque appel travaille sur un sous-arbre.\n"
        },
        {
          "text": "Un seul, sur le sous-arbre gauche",
          "correct": false,
          "feedback": "Erreur : si l'élément n'est pas dans le sous-arbre gauche,\non explore aussi le sous-arbre droit. Cela fait potentiellement\ndeux appels par nœud.\n"
        }
      ],
      "explanation": "Le `or` court-circuite l'appel droit dès que l'appel gauche\nrenvoie `True`, donc dans la pratique on n'explore pas toujours\nles deux sous-arbres. Mais dans le pire cas (élément absent ou\nprésent uniquement à droite), les deux appels sont effectués."
    },
    {
      "id": "q18",
      "difficulty": 2,
      "skills": [
        "pile-debordement"
      ],
      "title": "Limite pratique de la récursion en Python",
      "statement": "Pour quelle raison principale Python lève-t-il une\n`RecursionError` lors d'une récursion trop profonde, même si la\nfonction comporte un cas de base correct ?",
      "options": [
        {
          "text": "La pile d'appels du programme a une taille limite (par défaut autour de $1000$)",
          "correct": true,
          "feedback": "Bonne réponse : Python fixe par défaut une profondeur\nmaximale de récursion d'environ $1000$ pour éviter qu'une\nerreur de programmation ne consomme toute la mémoire de la\npile. On peut augmenter cette limite avec\n`sys.setrecursionlimit()`, mais ce n'est pas une bonne\npratique.\n"
        },
        {
          "text": "Le cas de base met trop de temps à s'évaluer",
          "correct": false,
          "feedback": "Erreur : le temps d'évaluation d'une condition est\nnégligeable. Le problème est la **mémoire** consommée par la\npile d'appels.\n"
        },
        {
          "text": "La fonction utilise trop de variables locales",
          "correct": false,
          "feedback": "Erreur : ce n'est pas le nombre de variables locales qui\ncompte, mais le nombre d'appels imbriqués.\n"
        },
        {
          "text": "Python ne sait pas gérer la récursivité",
          "correct": false,
          "feedback": "Erreur : Python gère parfaitement la récursivité, mais avec\nune limite pratique sur la profondeur de la pile.\n"
        }
      ],
      "explanation": "Cette limite est une protection : sans elle, une fonction\nrécursive mal conçue saturerait la mémoire de la pile et ferait\nplanter le système. Elle implique que certains algorithmes\nfonctionnant sur de grandes structures (parcours d'une liste de\nplusieurs milliers d'éléments) doivent être écrits de manière\nitérative en Python."
    },
    {
      "id": "q19",
      "difficulty": 2,
      "skills": [
        "complexite",
        "fibonacci"
      ],
      "title": "Complexité de Fibonacci récursif naïf",
      "statement": "Quelle est la complexité en temps de la fonction suivante,\nappelée pour calculer `fibonacci(n)` ?\n\n```\ndef fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n - 1) + fibonacci(n - 2)\n```",
      "options": [
        {
          "text": "$O(n^2)$",
          "correct": false,
          "feedback": "Erreur : la complexité n'est pas polynomiale mais\n**exponentielle**. L'arbre d'appels double de taille à chaque\nniveau.\n"
        },
        {
          "text": "$O(n)$",
          "correct": false,
          "feedback": "Erreur : c'est la complexité de la version itérative ou\nmémoïsée. La version naïve recalcule plusieurs fois les\nmêmes valeurs.\n"
        },
        {
          "text": "$O(\\log n)$",
          "correct": false,
          "feedback": "Erreur : aucune division n'est effectuée sur $n$ ; on le\ndécrémente seulement de $1$ ou $2$. La complexité ne peut\ndonc pas être logarithmique.\n"
        },
        {
          "text": "$O(2^n)$",
          "correct": true,
          "feedback": "Bonne réponse : chaque appel génère deux appels imbriqués,\ncréant un arbre binaire de profondeur $n$. Le nombre d'appels\ncroît comme $2^n$ (plus précisément $\\varphi^n$ avec\n$\\varphi$ le nombre d'or).\n"
        }
      ],
      "explanation": "`fibonacci(35)` génère plusieurs milliards d'appels avec cette\nversion naïve. C'est un cas typique où la mémoïsation (stocker\nles résultats déjà calculés dans un dictionnaire) ramène la\ncomplexité à $O(n)$."
    },
    {
      "id": "q20",
      "difficulty": 2,
      "skills": [
        "memoisation"
      ],
      "title": "Principe de la mémoïsation",
      "statement": "Qu'est-ce que la **mémoïsation** d'une fonction récursive ?",
      "options": [
        {
          "text": "Le stockage en cache des résultats déjà calculés, pour les réutiliser sans les recalculer",
          "correct": true,
          "feedback": "Bonne réponse : on conserve dans un dictionnaire (ou un\ntableau) les résultats déjà obtenus. Avant chaque calcul, on\nvérifie si le résultat est déjà connu ; si oui, on le\nrenvoie directement.\n"
        },
        {
          "text": "Une technique pour réduire la profondeur de la pile d'appels",
          "correct": false,
          "feedback": "Erreur : la mémoïsation n'agit pas sur la profondeur de la\npile mais sur le **nombre d'appels effectifs** : elle évite\nde recalculer ce qui a déjà été calculé.\n"
        },
        {
          "text": "La conversion automatique d'une fonction récursive en boucle",
          "correct": false,
          "feedback": "Erreur : ce serait la dérécursivation. La mémoïsation ne\nmodifie pas la structure de la fonction, elle ajoute un\ncache.\n"
        },
        {
          "text": "La compression des appels récursifs en un seul",
          "correct": false,
          "feedback": "Erreur : aucune fusion d'appels n'a lieu. La mémoïsation\nconserve la même structure d'appels, mais évite les\nredondances.\n"
        }
      ],
      "explanation": "Pour Fibonacci, la mémoïsation transforme une complexité\n$O(2^n)$ en $O(n)$. Python propose un décorateur\n`@functools.lru_cache` qui implémente automatiquement la\nmémoïsation sur n'importe quelle fonction."
    },
    {
      "id": "q21",
      "difficulty": 3,
      "skills": [
        "hanoi"
      ],
      "title": "Tours de Hanoï avec deux disques",
      "statement": "Pour résoudre le problème des tours de Hanoï avec $2$ disques\n(déplacer la pile de A vers C en utilisant B), combien de\ndéplacements minimum sont nécessaires ?",
      "options": [
        {
          "text": "$3$",
          "correct": true,
          "feedback": "Bonne réponse : (1) petit de A vers B, (2) grand de A vers C,\n(3) petit de B vers C. Cela correspond à $T(2) = 2^2 - 1 = 3$.\n"
        },
        {
          "text": "$2$",
          "correct": false,
          "feedback": "Erreur : il faut d'abord déplacer le petit disque sur l'étape\nintermédiaire B, puis le grand disque vers C, puis ramener le\npetit sur le grand. Cela fait $3$ déplacements, pas $2$.\n"
        },
        {
          "text": "$4$",
          "correct": false,
          "feedback": "Erreur : un déplacement en trop. Avec $2$ disques, $3$\ndéplacements suffisent.\n"
        },
        {
          "text": "$7$",
          "correct": false,
          "feedback": "Erreur : c'est $T(3) = 2^3 - 1$, soit le nombre de\ndéplacements pour $3$ disques, pas $2$.\n"
        }
      ],
      "explanation": "Le nombre de déplacements pour $n$ disques est\n$T(n) = 2^n - 1$, donné par la récurrence\n$T(n) = 2 \\cdot T(n-1) + 1$ avec $T(1) = 1$. Pour $n = 64$\n(légende des moines de Hanoï), il faudrait plus de\n$1{,}8 \\times 10^{19}$ déplacements."
    },
    {
      "id": "q22",
      "difficulty": 3,
      "skills": [
        "hanoi",
        "recurrence"
      ],
      "title": "Récurrence des tours de Hanoï",
      "statement": "Quelle relation de récurrence vérifie $T(n)$, le nombre de\ndéplacements minimum pour résoudre les tours de Hanoï avec $n$\ndisques ?",
      "options": [
        {
          "text": "$T(n) = 2 \\cdot T(n-1) + 1$",
          "correct": true,
          "feedback": "Bonne réponse : pour déplacer $n$ disques, on déplace les\n$n-1$ disques du dessus vers la tour auxiliaire ($T(n-1)$\ndéplacements), puis le grand disque vers la destination ($1$\ndéplacement), puis on ramène les $n-1$ disques sur la\ndestination ($T(n-1)$ déplacements). D'où $T(n) = 2T(n-1) + 1$.\n"
        },
        {
          "text": "$T(n) = T(n-1) + T(n-2)$",
          "correct": false,
          "feedback": "Erreur : c'est la récurrence de Fibonacci, sans rapport avec\nles tours de Hanoï. Il n'y a pas de raison de combiner deux\ntailles différentes ici.\n"
        },
        {
          "text": "$T(n) = T(n-1) + 1$",
          "correct": false,
          "feedback": "Erreur : cette récurrence donne $T(n) = n$ (croissance\nlinéaire), ce qui est faux. La résolution exige un nombre\nexponentiel de déplacements.\n"
        },
        {
          "text": "$T(n) = n!$",
          "correct": false,
          "feedback": "Erreur : ce serait la factorielle, qui croît plus vite que\n$2^n$. Pour Hanoï, la croissance est exponentielle de base\n$2$, pas factorielle.\n"
        }
      ],
      "explanation": "Cette récurrence se résout en $T(n) = 2^n - 1$. La récursivité\nest ici **double** (deux appels à $T(n-1)$), ce qui explique la\ncroissance exponentielle."
    },
    {
      "id": "q23",
      "difficulty": 3,
      "skills": [
        "implementation",
        "complexite"
      ],
      "title": "Choisir entre récursif et itératif",
      "statement": "Pour calculer la somme des $n$ premiers entiers, on peut écrire\nune boucle ou une fonction récursive. Pour $n = 10\\ 000$ en\nPython, quelle version est préférable ?",
      "options": [
        {
          "text": "La version récursive, plus rapide grâce à la pile d'appels",
          "correct": false,
          "feedback": "Erreur : à compter d'une certaine profondeur, la pile\nd'appels devient un handicap (consommation mémoire,\n`RecursionError`). La récursion n'est pas plus rapide qu'une\nboucle.\n"
        },
        {
          "text": "Aucune des deux, il faut absolument utiliser une formule fermée",
          "correct": false,
          "feedback": "Erreur : la formule $\\frac{n(n+1)}{2}$ existe et est en effet\nla solution la plus efficace, mais cela ne disqualifie pas\nla boucle. La récursion, elle, pose un vrai problème de pile.\n"
        },
        {
          "text": "La version itérative, qui évite le débordement de pile et reste très efficace",
          "correct": true,
          "feedback": "Bonne réponse : pour $n = 10\\ 000$, la version récursive\ndépasserait la limite par défaut de la pile Python ($\\sim\n1000$ appels). Une boucle `for` est plus sûre, plus rapide,\net tout aussi lisible pour ce calcul.\n"
        },
        {
          "text": "Les deux versions sont strictement équivalentes en performance",
          "correct": false,
          "feedback": "Erreur : la version récursive est plus lente (chaque appel\ncoûte plus qu'une itération) et risque de planter par\ndébordement de pile.\n"
        }
      ],
      "explanation": "En Python, la récursivité est élégante mais coûteuse :\nprivilégier l'itératif quand le problème ne se prête pas\nnaturellement à la récursion. La récursion brille pour les\nstructures naturellement récursives (arbres, listes\nhiérarchiques, fractales) ou pour les algorithmes diviser-pour-\nrégner."
    },
    {
      "id": "q24",
      "difficulty": 3,
      "skills": [
        "analyse",
        "lecture-de-code"
      ],
      "title": "Analyse d'une récursion mystère",
      "statement": "On considère :\n\n```\ndef f(n):\n    if n == 0:\n        return 0\n    if n == 1:\n        return 1\n    return f(n - 1) + f(n - 2) + 1\n```\n\nCombien d'**appels récursifs au total** sont effectués pour\ncalculer `f(4)` (en comptant l'appel initial) ?",
      "options": [
        {
          "text": "$4$",
          "correct": false,
          "feedback": "Erreur : on confond avec l'argument. Il faut compter chaque\nappel à `f` qui apparaît dans l'arbre d'appels, pas la\nvaleur de $n$.\n"
        },
        {
          "text": "$9$",
          "correct": true,
          "feedback": "Bonne réponse : `f(4)` appelle `f(3)` et `f(2)`. `f(3)`\nappelle `f(2)` et `f(1)`. `f(2)` appelle `f(1)` et `f(0)`.\nEn tout : $f(4)$, $f(3)$, $f(2)$, $f(1)$, $f(0)$, $f(2)$,\n$f(1)$, $f(1)$, $f(0)$ = $9$ appels.\n"
        },
        {
          "text": "$16$",
          "correct": false,
          "feedback": "Erreur : $16 = 2^4$ surestime largement le nombre d'appels.\nL'arbre n'est pas équilibré jusqu'à la profondeur $n$.\n"
        },
        {
          "text": "$5$",
          "correct": false,
          "feedback": "Erreur : on a oublié les appels redondants. Comme dans\nFibonacci naïf, certains arguments sont recalculés plusieurs\nfois.\n"
        }
      ],
      "explanation": "Cette fonction calcule $f(n) = F_{n+1} - 1$, où $F_n$ est la\nsuite de Fibonacci. Plus important pédagogiquement : elle\nmontre comment la récursivité double engendre une explosion du\nnombre d'appels : c'est ce que la mémoïsation vient résoudre."
    },
    {
      "id": "q25",
      "difficulty": 3,
      "skills": [
        "memoisation",
        "complexite"
      ],
      "title": "Effet de la mémoïsation",
      "statement": "On mémoïse la fonction Fibonacci avec un dictionnaire de cache.\nCombien d'appels effectifs (calculs réels, avant cache) sont\neffectués pour `fibonacci(20)` après mémoïsation ?",
      "options": [
        {
          "text": "Le même nombre qu'avant mémoïsation",
          "correct": false,
          "feedback": "Erreur : la mémoïsation change précisément le nombre d'appels\nréels en évitant les recalculs. Ce nombre passe d'$O(2^n)$ à\n$O(n)$.\n"
        },
        {
          "text": "Exactement $1$",
          "correct": false,
          "feedback": "Erreur : le cache n'élimine pas la nécessité de calculer\nchaque valeur **au moins une fois**. Pour\n$\\text{fibonacci}(20)$, il faut bien évaluer\n$\\text{fibonacci}(0), \\text{fibonacci}(1), \\ldots, \\text{fibonacci}(20)$\nau moins une fois chacun.\n"
        },
        {
          "text": "Environ $20$",
          "correct": true,
          "feedback": "Bonne réponse : avec mémoïsation, chaque valeur\n$\\text{fibonacci}(k)$ pour $k$ allant de $0$ à $20$ n'est\ncalculée qu'une seule fois. Les appels suivants sur la même\nvaleur sont satisfaits par le cache.\n"
        },
        {
          "text": "Environ $2^{20}$ (un million)",
          "correct": false,
          "feedback": "Erreur : c'est l'ordre de grandeur **sans** mémoïsation. Tout\nl'intérêt du cache est précisément de réduire ce nombre à\nquelque chose de bien plus petit.\n"
        }
      ],
      "explanation": "La mémoïsation transforme une complexité exponentielle en\ncomplexité linéaire, au prix d'un espace mémoire supplémentaire\négalement linéaire (le dictionnaire de cache). C'est l'un des\nexemples les plus marquants du compromis temps/espace en\nalgorithmique."
    },
    {
      "id": "q26",
      "difficulty": 2,
      "skills": [
        "recursivite-terminale"
      ],
      "title": "Récursivité terminale",
      "statement": "Une fonction récursive est dite **terminale** quand son\nappel récursif est la **dernière opération** effectuée\ndans la fonction. Parmi les fonctions suivantes,\nlaquelle est récursive terminale ?",
      "options": [
        {
          "text": "```\ndef f(n):\n    if n <= 1:\n        return n\n    return f(n - 1) + f(n - 2)\n```\n",
          "correct": false,
          "feedback": "Erreur : la fonction effectue une addition après les\ndeux appels récursifs. De plus, comme il y a deux\nappels distincts, cette fonction est doublement\nrécursive et ne peut pas être terminale.\n"
        },
        {
          "text": "```\ndef f(n):\n    if n == 0:\n        return None\n    print(n)\n    return f(n - 1) + 1\n```\n",
          "correct": false,
          "feedback": "Erreur : après l'appel récursif `f(n - 1)`, on\najoute `1`. L'opération en attente après le retour\nde l'appel empêche cette fonction d'être récursive\nterminale.\n"
        },
        {
          "text": "```\ndef f(n):\n    if n == 0:\n        return 1\n    return n * f(n - 1)\n```\n",
          "correct": false,
          "feedback": "Erreur : après l'appel récursif `f(n - 1)`, on\neffectue encore une multiplication par `n`. L'appel\nrécursif n'est donc pas la toute dernière opération,\nce qui empêche cette fonction d'être récursive\nterminale.\n"
        },
        {
          "text": "```\ndef f(n, acc):\n    if n == 0:\n        return acc\n    return f(n - 1, n * acc)\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : l'appel récursif `f(n - 1, n * acc)`\nest la dernière opération de la fonction (la\nmultiplication par `n` est faite **avant** l'appel,\ndans le calcul du paramètre). Aucune opération n'est\nen attente après l'appel. Cette forme se transforme\nmécaniquement en boucle.\n"
        }
      ],
      "explanation": "Pourquoi cette propriété est-elle importante ? Une fonction\nrécursive terminale peut, en principe, être convertie en\nboucle sans utiliser de pile : on parle d'**élimination\nde la récursivité terminale**. Certains compilateurs\nle font automatiquement (Scheme par exemple), mais pas\nPython, qui conserve toujours la pile d'appels."
    },
    {
      "id": "q27",
      "difficulty": 3,
      "skills": [
        "derecursivation"
      ],
      "title": "Convertir une récursion en itération",
      "statement": "On veut transformer la fonction `factorielle` récursive\nsimple en une version itérative équivalente. Quel code\neffectue cette conversion correctement ?",
      "options": [
        {
          "text": "```\ndef factorielle(n):\n    if n == 0:\n        return 1\n    return n * factorielle(n - 1)\n```\n",
          "correct": false,
          "feedback": "Erreur : c'est précisément la **version récursive**.\nLa question demandait une version itérative, sans\nappel à la fonction elle-même.\n"
        },
        {
          "text": "```\ndef factorielle(n):\n    return n ** n\n```\n",
          "correct": false,
          "feedback": "Erreur grossière : $n^n$ n'est pas la factorielle de\n$n$. Pour $n = 4$, on aurait $256$ au lieu de $24$.\nLa factorielle est définie par $n \\cdot (n-1) \\cdot\n\\ldots \\cdot 1$.\n"
        },
        {
          "text": "```\ndef factorielle(n):\n    produit = 1\n    for i in range(1, n + 1):\n        produit = produit * i\n    return produit\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : on remplace les appels récursifs par\nune boucle `for` qui multiplie successivement par\n$1, 2, \\ldots, n$. La complexité est la même\n($O(n)$), mais on évite la pile d'appels et le risque\nde `RecursionError` sur des grandes valeurs de `n`.\n"
        },
        {
          "text": "```\ndef factorielle(n):\n    produit = 0\n    for i in range(1, n + 1):\n        produit = produit * i\n    return produit\n```\n",
          "correct": false,
          "feedback": "Erreur : l'élément neutre de la multiplication est\n$1$, pas $0$. Avec `produit = 0`, le résultat reste\ntoujours $0$ quel que soit `n`. Bug classique\nd'initialisation.\n"
        }
      ],
      "explanation": "Schéma général de la dérécursivation d'une récursion\nsimple : un accumulateur initialisé à l'élément neutre,\nune boucle qui parcourt les arguments des appels\nrécursifs successifs, et une mise à jour à chaque\nitération. Pour la récursivité double (Fibonacci), on\nconserve souvent les deux dernières valeurs dans deux\nvariables."
    },
    {
      "id": "q28",
      "difficulty": 3,
      "skills": [
        "recursivite-croisee"
      ],
      "title": "Récursivité croisée",
      "statement": "On définit deux fonctions qui s'appellent mutuellement :\n```\ndef pair(n):\n    if n == 0:\n        return True\n    return impair(n - 1)\n\ndef impair(n):\n    if n == 0:\n        return False\n    return pair(n - 1)\n```\nComment qualifier cette construction et que renvoie\n`pair(7)` ?",
      "options": [
        {
          "text": "C'est de la récursivité croisée (ou mutuelle) :\ndeux fonctions s'appellent l'une l'autre, formant\nensemble un schéma récursif. `pair(7)` renvoie `False`\n",
          "correct": true,
          "feedback": "Bonne réponse : la récursivité croisée est un\nschéma où plusieurs fonctions se définissent\nmutuellement. Ici, en décrémentant `n` à chaque\nappel et en alternant pair/impair, on aboutit à\n`pair(0) = True` ou `impair(0) = False` selon la\nparité de `n`. Pour `n = 7`, on alterne sept fois\net on arrive à `impair(0)`, donc le résultat est\n`False`.\n"
        },
        {
          "text": "Cette construction est invalide en Python ; le\nprogramme plante immédiatement\n",
          "correct": false,
          "feedback": "Erreur : Python autorise parfaitement deux fonctions\nà s'appeler mutuellement, à condition qu'elles soient\ntoutes les deux définies au moment où on les appelle.\nAucune erreur n'est levée à la définition.\n"
        },
        {
          "text": "C'est une récursivité terminale, et `pair(7)`\nrenvoie `True`\n",
          "correct": false,
          "feedback": "Erreur de qualification et de résultat : la\nrécursivité terminale désigne le fait que l'appel\nrécursif soit la dernière opération de la fonction\n(ce qui est vrai ici, mais pas la caractéristique\nprincipale). La caractéristique dominante est qu'il\ny a deux fonctions s'appelant mutuellement. Et\n`pair(7)` renvoie `False`.\n"
        },
        {
          "text": "C'est une simple récursivité double, et `pair(7)`\nrenvoie `True`\n",
          "correct": false,
          "feedback": "Erreur : la récursivité double désigne deux appels\nd'une même fonction, comme dans Fibonacci. Ici, on\na deux fonctions distinctes qui s'appellent\nmutuellement, ce qui s'appelle récursivité\n**croisée**. De plus, $7$ est impair, donc\n`pair(7)` renvoie `False`, pas `True`.\n"
        }
      ],
      "explanation": "La récursivité croisée se prête à des problèmes où\nplusieurs cas se répondent (analyseurs syntaxiques,\nautomates à plusieurs états, alternances). En pratique,\non peut souvent la réduire à une récursivité simple\navec un paramètre supplémentaire (ici, on pourrait\nécrire `parite(n, est_pair)` qui alterne)."
    },
    {
      "id": "q29",
      "difficulty": 3,
      "skills": [
        "accumulateur"
      ],
      "title": "Récursivité avec accumulateur",
      "statement": "Voici deux écritures de la fonction qui calcule\nla somme des entiers de $1$ à $n$ :\n\n```\n# Version A\ndef somme_a(n):\n    if n == 0:\n        return 0\n    return n + somme_a(n - 1)\n\n# Version B\ndef somme_b(n, acc=0):\n    if n == 0:\n        return acc\n    return somme_b(n - 1, acc + n)\n```\n\nQuelle est la **différence essentielle** entre\nles deux versions ?",
      "options": [
        {
          "text": "La version B utilise plus de mémoire\n",
          "correct": false,
          "feedback": "Erreur : c'est précisément l'inverse en\nprincipe. Avec une optimisation de\nrécursivité terminale, la version B\npeut s'exécuter avec une pile de taille\nconstante. Sans cette optimisation (cas\nde Python), les deux versions consomment\nla pile de manière comparable.\n"
        },
        {
          "text": "La version A est plus rapide à l'exécution\n",
          "correct": false,
          "feedback": "Erreur : les deux versions ont la même\ncomplexité asymptotique $O(n)$ en temps.\nLa différence porte sur la **structure**\ndes appels et la **consommation de pile**,\npas sur la rapidité.\n"
        },
        {
          "text": "La version B donne un résultat différent\nde la version A\n",
          "correct": false,
          "feedback": "Erreur : les deux fonctions calculent\nrigoureusement la même valeur,\n$1 + 2 + \\ldots + n = n(n+1)/2$. Vérifier\nsur un exemple concret : pour $n = 3$,\nles deux versions renvoient $6$.\n"
        },
        {
          "text": "La version A laisse une opération en\nattente après chaque appel récursif (le\n`n + ...`), tandis que la version B passe\nle résultat partiel par un accumulateur\net n'a aucune opération en attente. La\nversion B est dite récursive terminale\n",
          "correct": true,
          "feedback": "Bonne réponse : dans la version A, après\nle retour de `somme_a(n - 1)`, il reste\nencore une addition à effectuer. Tous les\ncontextes intermédiaires doivent donc\nrester sur la pile d'appels. Dans la\nversion B, l'accumulateur transporte le\nrésultat partiel comme paramètre de\nl'appel suivant, sans rien laisser en\nattente. Certains compilateurs (Scheme,\nHaskell, OCaml) optimisent\nautomatiquement la version B en boucle,\névitant le débordement de pile pour de\ngrandes valeurs de `n`.\n"
        }
      ],
      "explanation": "Le motif accumulateur est très utilisé en\nprogrammation fonctionnelle. Il consiste à\ntransporter le résultat partiel comme\nparamètre, ce qui évite de laisser des\nopérations en attente. Python ne fait\naucune optimisation de récursivité\nterminale (volonté de Guido van Rossum\npour préserver la lisibilité des traces\nd'erreur), mais l'écriture reste utile\npédagogiquement et pour porter du code\nvers d'autres langages."
    },
    {
      "id": "q30",
      "difficulty": 3,
      "skills": [
        "pile-trace"
      ],
      "title": "Trace de la pile d'appels",
      "statement": "Lors de l'appel `factorielle(4)` (avec la\ndéfinition récursive standard), combien de\ncadres d'exécution sont **simultanément**\nempilés au moment où le cas de base est\natteint ?",
      "options": [
        {
          "text": "$5$ cadres (pour `factorielle(4)`, `factorielle(3)`, `factorielle(2)`, `factorielle(1)`, `factorielle(0)`)",
          "correct": true,
          "feedback": "Bonne réponse : chaque appel récursif\nempile un nouveau cadre **avant** que le\nprécédent ne se termine. Au moment où\n`factorielle(0)` atteint le cas de base,\ntous les cadres précédents sont encore\nen attente, car ils ont laissé\nl'opération `n * factorielle(n - 1)` à\nfinir. Les cadres se dépilent ensuite\nen sens inverse, en multipliant à chaque\nétape. Pour `n = 1000`, on aurait $1001$\ncadres simultanés, ce qui dépasse la\nlimite par défaut de Python.\n"
        },
        {
          "text": "$1$ cadre (Python optimise les appels récursifs)",
          "correct": false,
          "feedback": "Erreur : Python ne fait **pas** d'optimisation\nde récursivité terminale, contrairement à\nd'autres langages. Et même si la version\nétait terminale, la fonction `factorielle`\nstandard ne l'est pas (il y a une\nmultiplication en attente après chaque\nappel récursif).\n"
        },
        {
          "text": "$4$ cadres",
          "correct": false,
          "feedback": "Erreur : on oublie le cadre de\n`factorielle(0)`, le cas de base. Au\nmoment précis où il s'exécute, il est\nbien sur la pile, en plus des cadres\ndes quatre appels précédents.\n"
        },
        {
          "text": "$2$ cadres (le cadre courant et celui de la fonction qui a fait l'appel)",
          "correct": false,
          "feedback": "Erreur : tous les cadres récursifs sont\nempilés simultanément, pas seulement\ndeux. Chaque appel récursif est lui-même\nune « fonction qui a fait un appel »,\nce qui empile un nouveau cadre.\n"
        }
      ],
      "explanation": "C'est précisément cette accumulation de\ncadres qui provoque `RecursionError` pour\nles récursions très profondes. La pile\nd'appels par défaut en Python a une taille\nd'environ $1\\,000$ cadres\n(paramétrable avec\n`sys.setrecursionlimit`, mais déconseillé).\nPour des problèmes nécessitant une\nprofondeur supérieure, il faut soit\nutiliser une version itérative, soit\ntransformer la récursion en récursivité\nterminale puis en boucle."
    },
    {
      "id": "q31",
      "difficulty": 3,
      "skills": [
        "ackermann"
      ],
      "title": "Fonction d'Ackermann",
      "statement": "La fonction d'Ackermann est définie par :\n```\nA(0, n) = n + 1\nA(m, 0) = A(m - 1, 1) si m > 0\nA(m, n) = A(m - 1, A(m, n - 1)) si m > 0 et n > 0\n```\nQuelle est sa caractéristique remarquable ?",
      "options": [
        {
          "text": "Elle croît extrêmement vite : par exemple\n$A(4, 2)$ est un nombre comportant des\nmilliers de chiffres. Elle est calculable\nmais n'est pas primitive récursive,\nc'est-à-dire qu'elle ne peut pas être\nobtenue par les schémas de récurrence\nsimples (sans appels imbriqués)\n",
          "correct": true,
          "feedback": "Bonne réponse : la fonction d'Ackermann\na une importance théorique majeure en\ncalculabilité. Elle illustre que la\nrécursivité générale est strictement\nplus puissante que la récurrence\nprimitive (boucles bornées). Au niveau\npédagogique, elle est un excellent\nexemple de récursivité **doublement\nimbriquée** : l'argument du second appel\ndépend lui-même d'un appel récursif.\nQuelques valeurs : $A(0, 0) = 1$,\n$A(1, 1) = 3$, $A(2, 2) = 7$,\n$A(3, 3) = 61$, $A(4, 0) = 13$,\n$A(4, 1) = 65\\,533$.\n"
        },
        {
          "text": "Elle a une complexité polynomiale\n",
          "correct": false,
          "feedback": "Erreur : sa complexité dépasse toute\nfonction polynomiale, exponentielle, ou\ntour d'exponentielles. C'est précisément\nce qui la rend remarquable.\n"
        },
        {
          "text": "Elle ne se calcule jamais (récursion\ninfinie)\n",
          "correct": false,
          "feedback": "Erreur : la fonction d'Ackermann termine\ntoujours, c'est-à-dire qu'elle est\n**totale** sur ses entiers naturels. Mais\nle temps de calcul peut être\nastronomique pour $m \\geq 4$.\n"
        },
        {
          "text": "Elle calcule simplement la somme $m + n$\n",
          "correct": false,
          "feedback": "Erreur : la fonction d'Ackermann croît\nbeaucoup plus vite que l'addition, et\nplus vite que la multiplication, et\nplus vite que l'exponentiation, et plus\nvite que toute itération finie de ces\nopérations.\n"
        }
      ],
      "explanation": "Ackermann illustre la **hiérarchie des\nfonctions calculables** : addition (rapide),\nmultiplication (=addition itérée),\nexponentiation (=multiplication itérée),\ntétration (=exponentiation itérée), et\nainsi de suite. Ackermann généralise ces\nopérations à un niveau d'imbrication\narbitraire, ce qui la place hors d'atteinte\ndes récursions primitives."
    },
    {
      "id": "q32",
      "difficulty": 2,
      "skills": [
        "comparaison-fibonacci"
      ],
      "title": "Fibonacci itératif vs récursif",
      "statement": "Pour calculer $\\mathrm{fib}(50)$, le temps\nd'exécution sur un PC moderne est :",
      "options": [
        {
          "text": "Les deux versions sont instantanées\n",
          "correct": false,
          "feedback": "Erreur : seule la version itérative est\ninstantanée. La version récursive naïve\ndevient impraticable bien avant $n =\n50$.\n"
        },
        {
          "text": "Les deux versions plantent par\ndépassement de pile\n",
          "correct": false,
          "feedback": "Erreur : la version itérative ne\nconsomme pas la pile (juste deux\nvariables). La version récursive a une\nprofondeur d'appels de $n = 50$, ce qui\nest largement sous la limite Python\n($\\sim 1\\,000$). Le problème de la\nversion récursive est le **temps**, pas\nla pile.\n"
        },
        {
          "text": "Quelques millisecondes pour la version\nitérative ; plusieurs dizaines de\nminutes, voire plus, pour la version\nrécursive naïve\n",
          "correct": true,
          "feedback": "Bonne réponse. La version itérative est\nen $O(n)$ : pour $n = 50$, c'est environ\n$50$ opérations, donc quelques\nmicrosecondes. La version récursive\nnaïve est en $O(\\varphi^n)$ avec\n$\\varphi \\approx 1{,}618$ : pour $n =\n50$, c'est environ\n$\\varphi^{50} \\approx 2{,}9 \\cdot\n10^{10}$ appels. À environ $10^7$ appels\npar seconde en Python, cela représente\nenviron $50$ minutes.\n"
        },
        {
          "text": "Les deux versions sont équivalentes en\ntemps\n",
          "correct": false,
          "feedback": "Erreur : la différence est colossale,\nplusieurs ordres de grandeur. C'est\nl'exemple classique pour motiver la\nmémoïsation ou la version itérative.\n"
        }
      ],
      "explanation": "Cet exemple est l'argument le plus\npédagogique pour expliquer la mémoïsation.\nAvec une mémoïsation simple, on retombe en\n$O(n)$ tout en gardant la lisibilité de la\nversion récursive. C'est le compromis\nqu'offre la programmation dynamique :\ngarder la clarté de la formulation\nrécursive, sans en payer le coût\nexponentiel."
    }
  ]
}