Files
1ereNSI/gloutons/draft/correction.ipynb
T

550 lines
22 KiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# TP — Draft d'Arène : **corrigé**\n",
"\n",
"Dans le mode **Arène** d'un jeu de cartes, on ne joue pas avec sa collection : on **drafte** son deck.\n",
"On t'ouvre un booster de 3 cartes, tu en gardes **une seule**, et on passe au booster suivant.\n",
"\n",
"**Une carte écartée est perdue pour toujours. On ne revient jamais en arrière.**\n",
"\n",
"Cette règle du jeu est *exactement* la définition d'un algorithme glouton :\n",
"\n",
"- on procède par étapes (un booster à la fois) ;\n",
"- à chaque étape on fait le meilleur choix possible ;\n",
"- on ne revient jamais sur un choix déjà fait.\n",
"\n",
"Ce TP a un seul but : voir de nos propres yeux **ce que le glouton fait bien, et ce qu'il rate**.\n",
"\n",
"---\n",
"\n",
"## Les données\n",
"\n",
"Une carte est un tuple `(nom, puissance, cout)` :\n",
"\n",
"- la **puissance** mesure la force de la carte (plus c'est haut, mieux c'est) ;\n",
"- le **coût** est le nombre de manas nécessaires pour la poser sur le plateau.\n",
"\n",
"> **Règle de jouabilité.** Une carte est dite **chère** si elle coûte **5 manas ou plus**.\n",
"> Un deck n'est **jouable** que s'il contient **au plus 3 cartes chères**.\n",
"> Sinon, on n'a rien à jouer pendant les premiers tours : on perd la partie avant même\n",
"> d'avoir pu poser ses grosses cartes.\n",
"\n",
"**Objectif : obtenir le deck jouable de puissance totale maximale.**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ---------------------------------------------------------------\n",
"# Données du TP — ne pas modifier\n",
"# ---------------------------------------------------------------\n",
"\n",
"# Une carte = (nom, puissance, cout)\n",
"BOOSTERS = [\n",
" [(\"Gnome bricoleur\", 3, 1), (\"Yéti des roches\", 5, 4), (\"Golem de siège\", 6, 5)],\n",
" [(\"Écuyer fidèle\", 2, 1), (\"Loup argenté\", 4, 3), (\"Chevalier du néant\", 5, 5)],\n",
" [(\"Apprenti mage\", 3, 2), (\"Sorcière du marais\", 4, 4), (\"Dragon de bronze\", 5, 5)],\n",
" [(\"Recrue de la garde\", 2, 1), (\"Berserker orc\", 4, 3), (\"Titan de pierre\", 9, 7)],\n",
" [(\"Novice de l'ombre\", 1, 1), (\"Archer elfe\", 3, 2), (\"Léviathan des abysses\", 10, 8)],\n",
" [(\"Sanglier tenace\", 2, 2), (\"Garde royal\", 5, 4), (\"Ogre lourdaud\", 4, 6)],\n",
" [(\"Voleur agile\", 3, 2), (\"Prêtresse de la lune\", 4, 3), (\"Colosse runique\", 7, 6)],\n",
" [(\"Gobelin ferrailleur\", 2, 1), (\"Chaman du totem\", 4, 4), (\"Hydre des marais\", 6, 6)],\n",
" [(\"Éclaireur nain\", 3, 2), (\"Paladin d'argent\", 5, 4), (\"Behemoth ancien\", 6, 7)],\n",
" [(\"Rat des égouts\", 1, 1), (\"Duelliste vétéran\", 5, 3), (\"Ver des sables\", 6, 5)],\n",
"]\n",
"\n",
"COUT_CHER = 5 # une carte est \"chère\" à partir de 5 manas\n",
"MAX_CHERES = 3 # au plus 3 cartes chères dans un deck jouable\n",
"\n",
"\n",
"def afficher_deck(deck):\n",
" \"\"\"Affiche un deck sous forme de tableau.\"\"\"\n",
" for nom, puissance, cout in deck:\n",
" marque = \" (chère)\" if cout >= COUT_CHER else \"\"\n",
" print(f\" {nom:<24} puissance {puissance:>2} coût {cout}{marque}\")\n",
"\n",
"\n",
"print(f\"{len(BOOSTERS)} boosters de 3 cartes.\")\n",
"print(\"Booster 1 :\")\n",
"afficher_deck(BOOSTERS[0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Partie 1 — Savoir juger un deck\n",
"\n",
"Avant de drafter, il faut savoir répondre à deux questions sur un deck :\n",
"**combien vaut-il ?** et **est-il seulement jouable ?**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def puissance_totale(deck):\n",
" \"\"\"Renvoie la somme des puissances des cartes du deck.\"\"\"\n",
" total = 0\n",
" for nom, puissance, cout in deck:\n",
" total = total + puissance\n",
" return total\n",
"\n",
"\n",
"# Vérification\n",
"test = [(\"A\", 3, 1), (\"B\", 5, 6), (\"C\", 2, 2)]\n",
"assert puissance_totale(test) == 10\n",
"assert puissance_totale([]) == 0\n",
"print(\"Exercice 1 : OK\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def nb_cartes_cheres(deck):\n",
" \"\"\"Renvoie le nombre de cartes coûtant COUT_CHER manas ou plus.\"\"\"\n",
" n = 0\n",
" for nom, puissance, cout in deck:\n",
" if cout >= COUT_CHER:\n",
" n = n + 1\n",
" return n\n",
"\n",
"\n",
"def deck_jouable(deck):\n",
" \"\"\"Renvoie True si le deck respecte la règle de jouabilité.\"\"\"\n",
" return nb_cartes_cheres(deck) <= MAX_CHERES\n",
"\n",
"\n",
"# Vérification\n",
"test = [(\"A\", 3, 1), (\"B\", 5, 6), (\"C\", 2, 7), (\"D\", 4, 5), (\"E\", 1, 8)]\n",
"assert nb_cartes_cheres(test) == 4\n",
"assert deck_jouable(test) is False\n",
"assert deck_jouable(test[:3]) is True\n",
"print(\"Exercice 2 : OK\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Partie 2 — Le glouton naïf\n",
"\n",
"La stratégie qui vient spontanément à l'esprit :\n",
"\n",
"> **À chaque booster, je prends la carte la plus puissante.**\n",
"\n",
"C'est bien un algorithme glouton : un choix par étape, le meilleur sur le moment,\n",
"jamais de retour en arrière. Codons-la."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def carte_la_plus_puissante(booster):\n",
" \"\"\"Renvoie la carte de plus grande puissance du booster.\"\"\"\n",
" meilleure = booster[0]\n",
" for carte in booster:\n",
" if carte[1] > meilleure[1]:\n",
" meilleure = carte\n",
" return meilleure\n",
"\n",
"\n",
"# Vérification\n",
"assert carte_la_plus_puissante(BOOSTERS[0]) == (\"Golem de siège\", 6, 5)\n",
"assert carte_la_plus_puissante(BOOSTERS[4]) == (\"Léviathan des abysses\", 10, 8)\n",
"print(\"Exercice 3 : OK\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def draft_naif(boosters):\n",
" \"\"\"Renvoie le deck obtenu en prenant à chaque fois la carte la plus puissante.\"\"\"\n",
" deck = []\n",
" for booster in boosters:\n",
" deck.append(carte_la_plus_puissante(booster))\n",
" return deck\n",
"\n",
"\n",
"deck_naif = draft_naif(BOOSTERS)\n",
"afficher_deck(deck_naif)\n",
"print()\n",
"print(\"Puissance totale :\", puissance_totale(deck_naif))\n",
"print(\"Cartes chères :\", nb_cartes_cheres(deck_naif), \"( maximum autorisé :\", MAX_CHERES, \")\")\n",
"print(\"Deck jouable ? :\", deck_jouable(deck_naif))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Question 1 — correction\n",
"\n",
"L'algorithme n'a regardé qu'**un seul critère, la puissance**, et il l'a optimisé\n",
"booster par booster. Or les cartes les plus puissantes sont justement les plus chères :\n",
"en maximisant la puissance à chaque étape, il accumule mécaniquement des cartes chères.\n",
"\n",
"La contrainte de jouabilité, elle, est **globale** : elle porte sur le deck entier, pas sur\n",
"un choix isolé. Un algorithme qui décide en regardant uniquement le booster courant n'a\n",
"aucun moyen de la voir venir.\n",
"\n",
"C'est le premier enseignement du TP : un glouton optimise le critère qu'on lui donne,\n",
"**rien d'autre**."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Partie 3 — Le glouton contraint\n",
"\n",
"Réparons l'algorithme sans changer sa nature. Il reste glouton — un choix par étape,\n",
"pas de retour en arrière — mais il compte au fur et à mesure les cartes chères déjà prises :\n",
"\n",
"> **À chaque booster, je prends la carte la plus puissante _parmi celles que j'ai encore le droit de prendre_.**\n",
"\n",
"Tant que le quota de cartes chères n'est pas atteint, toutes les cartes sont autorisées.\n",
"Une fois le quota plein, seules les cartes à moins de `COUT_CHER` manas le sont.\n",
"\n",
"*(Les boosters sont construits pour qu'il y ait toujours au moins une carte à moins de 5 manas :\n",
"l'algorithme ne peut donc jamais se retrouver bloqué.)*"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def draft_contraint(boosters):\n",
" \"\"\"Renvoie le deck obtenu par un glouton qui respecte le quota de cartes chères.\"\"\"\n",
" deck = []\n",
" cheres_prises = 0\n",
" for booster in boosters:\n",
" # Cartes que l'on a encore le droit de prendre\n",
" autorisees = []\n",
" for carte in booster:\n",
" if carte[2] < COUT_CHER or cheres_prises < MAX_CHERES:\n",
" autorisees.append(carte)\n",
" # Meilleur choix parmi elles\n",
" choisie = carte_la_plus_puissante(autorisees)\n",
" if choisie[2] >= COUT_CHER:\n",
" cheres_prises = cheres_prises + 1\n",
" deck.append(choisie)\n",
" return deck\n",
"\n",
"\n",
"deck_glouton = draft_contraint(BOOSTERS)\n",
"afficher_deck(deck_glouton)\n",
"print()\n",
"print(\"Puissance totale :\", puissance_totale(deck_glouton))\n",
"print(\"Deck jouable ? :\", deck_jouable(deck_glouton))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Question 2\n",
"\n",
"Cette fois le deck est jouable, pour une puissance de **46**.\n",
"\n",
"Notre algorithme fonctionne. Mais une question reste ouverte, et c'est *la* question\n",
"du chapitre : **46, est-ce le maximum ?**\n",
"\n",
"Le glouton, lui, en est incapable de le dire — il n'a jamais comparé son deck à un autre."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Partie 4 — La force brute : quel était le vrai maximum ?\n",
"\n",
"Pour le savoir, il n'y a qu'une méthode certaine : **essayer tous les decks possibles**\n",
"et garder le meilleur deck jouable.\n",
"\n",
"À chaque booster on a 3 choix, et il y a 10 boosters : cela fait\n",
"\n",
"$$3^{10} = 59\\,049 \\text{ decks possibles.}$$\n",
"\n",
"C'est beaucoup pour un humain, très peu pour une machine. Le code ci-dessous est **fourni** :\n",
"`product(*boosters)` fabrique toutes les combinaisons possibles, une par une.\n",
"\n",
"Lis-le, exécute-le, mais ne cherche pas à le réécrire : ce n'est pas l'objet du TP."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ---------------------------------------------------------------\n",
"# Force brute — code fourni\n",
"# ---------------------------------------------------------------\n",
"from itertools import product\n",
"from time import perf_counter\n",
"\n",
"\n",
"def meilleur_deck_force_brute(boosters):\n",
" \"\"\"Essaie TOUS les decks possibles et renvoie le meilleur deck jouable.\"\"\"\n",
" meilleur = None\n",
" meilleure_puissance = -1\n",
" for deck in product(*boosters):\n",
" if deck_jouable(deck) and puissance_totale(deck) > meilleure_puissance:\n",
" meilleur = list(deck)\n",
" meilleure_puissance = puissance_totale(deck)\n",
" return meilleur, meilleure_puissance\n",
"\n",
"\n",
"debut = perf_counter()\n",
"deck_optimal, puissance_optimale = meilleur_deck_force_brute(BOOSTERS)\n",
"duree = perf_counter() - debut\n",
"\n",
"afficher_deck(deck_optimal)\n",
"print()\n",
"print(\"Puissance optimale :\", puissance_optimale)\n",
"print(f\"Calculé en {duree:.2f} s après avoir testé {3 ** len(BOOSTERS)} decks.\")\n",
"print()\n",
"print(\"Glouton :\", puissance_totale(deck_glouton), \" | Optimal :\", puissance_optimale)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Question 3\n",
"\n",
"Le glouton obtient **46**, l'optimum est **58**. Il s'est fait piéger — reste à comprendre où.\n",
"\n",
"Regarde les 3 cartes chères que chacun a retenues :\n",
"\n",
"- **glouton** : Golem de siège, Chevalier du néant, Dragon de bronze (les boosters 1, 2 et 3) ;\n",
"- **optimum** : Titan de pierre, Léviathan des abysses, Colosse runique (les boosters 4, 5 et 7).\n",
"\n",
"Le tableau ci-dessous calcule, pour chaque booster, ce que rapporte le fait d'y « dépenser »\n",
"une place de carte chère plutôt que de prendre la meilleure carte bon marché.\n",
"\n",
"Exécute-le, puis explique en quelques lignes l'erreur du glouton."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Ce que rapporte réellement chaque carte chère, comparée à la meilleure carte pas chère du même booster\n",
"print(f\"{'Booster':<9}{'meilleure pas chère':<27}{'meilleure chère':<29}{'gain'}\")\n",
"for i, booster in enumerate(BOOSTERS):\n",
" pas_cheres = [c for c in booster if c[2] < COUT_CHER]\n",
" cheres = [c for c in booster if c[2] >= COUT_CHER]\n",
" if cheres == []:\n",
" continue\n",
" meilleure_pas_chere = carte_la_plus_puissante(pas_cheres)\n",
" meilleure_chere = carte_la_plus_puissante(cheres)\n",
" gain = meilleure_chere[1] - meilleure_pas_chere[1]\n",
" print(f\"{i + 1:<9}{meilleure_pas_chere[0] + f' ({meilleure_pas_chere[1]})':<27}\"\n",
" f\"{meilleure_chere[0] + f' ({meilleure_chere[1]})':<29}{gain:+d}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Question 3 — correction\n",
"\n",
"Les gains sont : **+1, +1, +1**, +5, +7, 1, +3, +2, +1, +1.\n",
"\n",
"Le glouton a dépensé ses trois places de cartes chères dans les boosters 1, 2 et 3, pour\n",
"un gain de **+1 à chaque fois**. Quand il arrive au booster 4 (Titan de pierre, **+5**) puis\n",
"au booster 5 (Léviathan des abysses, **+7**), son quota est plein : il doit se rabattre sur\n",
"le Berserker orc et l'Archer elfe.\n",
"\n",
"L'optimum fait exactement l'inverse : il **renonce** aux trois petits gains du début pour\n",
"garder ses places aux boosters 4, 5 et 7 (+7, +5, +3).\n",
"\n",
"$$46 + 7 + 5 + 3 - 1 - 1 - 1 = 58$$\n",
"\n",
"Et c'est là tout le problème : au booster 1, l'algorithme **n'a aucun moyen de savoir** qu'un\n",
"Léviathan des abysses l'attend au booster 5. Il décide avec une vision purement locale, et\n",
"la règle du draft lui interdit de revenir corriger son choix quand l'information arrive.\n",
"\n",
"Le glouton n'a pas fait d'erreur de calcul. Il a fait, trois fois de suite, le meilleur choix\n",
"**du moment** — et c'est précisément ce qui l'a ruiné."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Partie 5 — Alors pourquoi ne pas toujours faire de la force brute ?\n",
"\n",
"Puisque la force brute donne toujours le bon résultat, autant l'utiliser partout, non ?\n",
"\n",
"La cellule suivante mesure le temps de calcul de la force brute pour un nombre croissant\n",
"de boosters. Observe la colonne « temps »."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(f\"{'boosters':<11}{'decks testés':<16}{'temps (s)'}\")\n",
"for n in range(4, len(BOOSTERS) + 1):\n",
" debut = perf_counter()\n",
" meilleur_deck_force_brute(BOOSTERS[:n])\n",
" duree_n = perf_counter() - debut\n",
" print(f\"{n:<11}{3 ** n:<16}{duree_n:.3f}\")\n",
"\n",
"# Extrapolation à un vrai draft d'Arène (30 boosters), à partir du temps mesuré ci-dessus\n",
"vitesse = 3 ** len(BOOSTERS) / duree_n # decks testés par seconde\n",
"secondes = 3 ** 30 / vitesse\n",
"print()\n",
"print(f\"Vitesse mesurée : environ {vitesse:.0f} decks testés par seconde.\")\n",
"print(f\"Pour 30 boosters : {3 ** 30} decks à tester,\")\n",
"print(f\"soit environ {secondes / (365 * 24 * 3600):.0f} années de calcul.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Question 4 — correction\n",
"\n",
"**1.** Chaque booster ajouté multiplie le nombre de decks par **3** — et le temps de calcul\n",
"également. On parle de coût **exponentiel**.\n",
"\n",
"**2.** Pour 30 boosters : $3^{30} \\approx 2 \\times 10^{14}$ decks, soit environ\n",
"**200 000 milliards**.\n",
"\n",
"Le calcul se fait sans connaître la machine : passer de 10 à 30 boosters, c'est multiplier\n",
"le travail par $3^{20} \\approx 3,5$ milliards. Le temps mesuré pour 10 boosters (quelques\n",
"centièmes de seconde) devient donc **plusieurs années** de calcul — la cellule précédente\n",
"affiche l'estimation exacte pour ta machine.\n",
"\n",
"Et gagner un facteur 100 sur la vitesse de la machine ne ferait pas gagner grand-chose :\n",
"il suffirait d'ajouter 5 boosters pour tout reperdre. La force brute n'est pas « lente »,\n",
"elle est **impraticable** : aucune optimisation de code ne rattrape une croissance\n",
"exponentielle.\n",
"\n",
"**3.** Le glouton est le bon choix dès que le nombre de possibilités explose, ou qu'il faut\n",
"une réponse immédiate. Il donne une solution correcte en un temps proportionnel au nombre\n",
"d'étapes (ici : 10 boosters → 10 tours de boucle) sans jamais garantir l'optimum.\n",
"\n",
"Un algorithme de ce type — rapide, raisonnable, sans garantie d'optimalité — s'appelle une\n",
"**heuristique**."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Ce qu'il faut retenir\n",
"\n",
"| | Glouton | Force brute |\n",
"|---|---|---|\n",
"| Résultat | 46 (pas l'optimum) | 58 (l'optimum) |\n",
"| Nombre d'opérations | 10 (une par booster) | 59 049 |\n",
"| Avec 30 boosters | 30 opérations | plusieurs années |\n",
"| Garantie | aucune | optimum certain |\n",
"\n",
"Un **algorithme glouton** construit une solution par étapes, en faisant à chaque étape\n",
"le meilleur choix selon un critère local, et **sans jamais revenir en arrière**.\n",
"\n",
"Il est rapide. Il n'est pas toujours optimal. Quand ce n'est pas le cas, on parle\n",
"d'**heuristique**.\n",
"\n",
"---"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Pour aller plus loin — correction\n",
"\n",
"**1.** Les gains sont +1, +1, +1, +5, +7, 1, +3, +2, +1, +1. Les trois plus gros sont\n",
"+7 (booster 5), +5 (booster 4) et +3 (booster 7) : exactement les cartes chères du deck\n",
"optimal. Cette stratégie donne bien **58**.\n",
"\n",
"Ce n'est pas un hasard : le choix fait dans un booster n'influence les autres que par le\n",
"quota de cartes chères. Le problème revient donc à choisir les 3 boosters où dépenser une\n",
"place, et prendre les 3 meilleurs gains est ici optimal.\n",
"\n",
"**Un glouton peut donc être optimal — encore faut-il l'appliquer au bon critère.** Le mauvais\n",
"glouton ne triait pas sur la puissance de la carte, mais sur ce qu'elle rapporte *vraiment*\n",
"par rapport à l'alternative.\n",
"\n",
"**2.** Parce que ce tableau exige de connaître **les 10 boosters à l'avance**. Or dans un vrai\n",
"draft, on découvre les boosters un par un : au moment de choisir dans le premier, les neuf\n",
"autres n'existent pas encore.\n",
"\n",
"C'est la distinction entre un problème **hors ligne** (toutes les données sont connues au\n",
"départ — rendu de monnaie, sac à dos) et un problème **en ligne** (les données arrivent au\n",
"fur et à mesure — le draft). Dans un problème en ligne, même le meilleur critère du monde\n",
"ne peut pas compenser une information qu'on n'a pas encore."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"Auteur : Florian Mathieu\n",
"\n",
"Licence CC BY-SA\n",
"\n",
"<a rel=\"license\" href=\"http://creativecommons.org/licenses/by-sa/4.0/\"><img alt=\"Licence Creative Commons\" style=\"border-width:0\" src=\"https://i.creativecommons.org/l/by-sa/4.0/88x31.png\" /></a> <br />Ce cours est mis à disposition selon les termes de la <a rel=\"license\" href=\"http://creativecommons.org/licenses/by-sa/4.0/\">Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International</a>"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}