Un corrigé pédagogique pour comprendre et justifier vos réponses. Les conseils de rédaction ne constituent pas un barème officiel détaillé.
Exercice 1 · 6 points
Justification de texte et découpage optimal par programmation dynamique
Le sujet part de la phrase de Donald Knuth « An algorithm must be seen to be believed. ». Tous les caractères, espaces compris, sont supposés de même largeur ; les mots ne sont ni coupés ni décorés. Justifier une ligne consiste à atteindre une longueur fixe en ajoutant des espaces entre les mots, ou à droite s’il n’y a qu’un mot.
Pour plusieurs mots, on divise le nombre total d’espaces nécessaires par le nombre d’intervalles entre mots. On donne le quotient à chaque intervalle, puis un espace supplémentaire à chacun des premiers intervalles jusqu’à épuiser le reste. Le schéma de traitement de texte du PDF illustre des lignes alignées sur leurs deux bords ; l’exemple reconstruit ci-dessous rend chaque espace visible par un point médian.
An···algorithm···must··be
|<----25 caractères---->|Question 1
#Montrer que les mots An, algorithm, must, be nécessitent 8 espaces pour une justification de 25 caractères.
Indice
Commencer par compter les lettres seules.
Comprendre la correction
Les mots contiennent 2+9+4+2=17 lettres. La ligne doit mesurer 25 caractères : il faut donc 25−17=8 espaces au total. Ces huit espaces comprennent les séparations ordinaires entre mots ; ce ne sont pas huit espaces ajoutés à trois espaces déjà présents.
1. An--algorithm---must---be
2. An----algorithm--must--be
3. An---algorithm---must--be
4. An---algorithm--must---beQuestion 2
#Choisir la seule répartition correcte parmi les quatre propositions où un tiret représente un espace.
Indice
Le reste de la division est distribué en commençant à gauche.
Comprendre la correction
La troisième proposition convient :An---algorithm---must--be. Il y a trois intervalles et 8=3×2+2 : deux espaces dans chacun, puis un supplémentaire dans les deux premiers. On obtient donc 3,3,2 espaces de gauche à droite.
Le squelette fourni calcule nb_caracteres et nb_mots, puis le nombre total d’espaces. Il répartit ces espaces en deux boucles. Les blancs sont :
def ajout_espace(liste_mots, justification):
nb_caracteres = sum([len(mot) for mot in liste_mots])
nb_mots = len(liste_mots)
assert nb_caracteres + ...
nb_espace_total = justification - nb_caracteres
if nb_mots == 1:
return ... + ' ' * nb_espace_total
q = nb_espace_total // (nb_mots-1)
r = nb_espace_total % (nb_mots-1)
reponse = liste_mots[0]
for i in range(1,r+1):
reponse = reponse + ' ' * ... + liste_mots[i]
for i in range(r+1,nb_mots):
reponse = reponse + ' ' * ... + liste_mots[i]
return reponseQuestion 3
#Compléter la précondition de ajout_espace : les mots doivent tenir sur une ligne.
Indice
Il faut au moins un espace entre deux mots voisins.
Comprendre la correction
assert nb_caracteres + nb_mots - 1 <= justificationEntre nb_mots mots, il faut au moins nb_mots−1 espaces. La somme des lettres seules ne suffit donc pas à décider si la ligne tient. La liste de mots est non vide par contrat. Pour un seul mot, le nombre minimal d’espaces inter-mots vaut 0, ce que cette formule traite correctement.
Question 4
#Compléter les lignes 8,14 et 17 de ajout_espace.
PythonRépartir les espaces pour aligner une ligneÉcrivez votre solution et mettez-la à l’épreuve
Écrivez ajout_espace(liste_mots, justification). La liste est non vide, ses mots sont non vides et leur longueur totale avec un espace entre chaque mot ne dépasse pas justification. Renvoyez une chaîne de cette longueur exacte. Répartissez les espaces aussi également que possible entre les mots ; les intervalles les plus à gauche reçoivent l’espace supplémentaire. Pour un seul mot, complétez à droite.
def ajout_espace(liste_mots, justification):
# À vous de jouer
passLes cas de test proposés :
- Un seul mot : Sans intervalle intérieur, les quatre espaces sont ajoutés à droite.
- La largeur minimale : 9 lettres et deux séparations occupent déjà exactement 11 caractères.
- Un nombre d’espaces divisible : Six espaces sont répartis en deux intervalles de trois.
- Le reste se place à gauche : Cinq espaces donnent 3 puis 2, pas l’inverse.
- Plusieurs intervalles reçoivent un supplément : Huit espaces sur trois intervalles donnent le quotient 2 et le reste 2 : 3, 3, 2.
- Ne pas modifier la liste de mots : La sortie est une chaîne ; les mots d’entrée restent utilisables pour un autre découpage.
Indice
Le groupe des r premiers intervalles reçoit un espace de plus.
Comprendre la correction
def ajout_espace(liste_mots, justification):
nb_caracteres = sum(len(mot) for mot in liste_mots)
nb_mots = len(liste_mots)
assert nb_caracteres + nb_mots - 1 <= justification
nb_espace_total = justification - nb_caracteres
if nb_mots == 1:
return liste_mots[0] + ' ' * nb_espace_total
q = nb_espace_total // (nb_mots - 1)
r = nb_espace_total % (nb_mots - 1)
reponse = liste_mots[0]
for i in range(1, r + 1):
reponse = reponse + ' ' * (q + 1) + liste_mots[i]
for i in range(r + 1, nb_mots):
reponse = reponse + ' ' * q + liste_mots[i]
return reponseLes compléments sont liste_mots[0], q+1 puisq. La première boucle traite exactement r intervalles, de 1 àr inclus. La seconde traite tous les suivants. La concaténation ajoute toujours les espaces avant le mot suivant. La longueur finale vaut la somme des lettres plus tous les espaces distribués, donc la justification demandée.
Le cas d’un seul mot est traité avant la division par nb_mots−1, pour éviter un dénominateur nul. Si r vaut 0, la première boucle est vide et tous les intervalles reçoivent q espaces. Tester une ligne avec un seul mot, un reste nul et un reste non nul couvre les trois mécanismes.
Question 5
#Proposer en langage naturel un algorithme décidant après quel mot revenir à la ligne. Chaque mot tient individuellement dans la justification.
Indice
Décider à partir de la longueur de la ligne courante et du prochain mot.
Comprendre la correction
Parcourir les mots dans l’ordre et construire la ligne courante. Avant d’ajouter un mot, compter sa longueur et l’espace qui le sépare du précédent. Si l’ajout tient dans la justification, le conserver sur cette ligne ; sinon, terminer la ligne avant ce mot et démarrer une nouvelle ligne avec lui.À la fin, conserver la dernière ligne.
Le premier mot d’une ligne ne nécessite pas d’espace préalable. Cet algorithme remplit chaque ligne autant que possible : il est glouton. Il produit un découpage valide mais ne garantit pas de minimiser le coût esthétique qui sera défini ensuite.
Pour les huit mots An, algorithm, must, be, seen, to, be, believed, le découpage[(0,2),(2,5),(5,7),(7,8)] correspond à An algorithm / must be seen / to be / believed. Les blancs sont ceux de la boucle, des bornes de tranche, de la justification passée et de l’affichage.
def affiche_justifie(liste_mots, decoupage, justification):
for ... in decoupage:
ligne_justifiee = ajout_espace(liste_mots[...:...], ...)
...Question 6
#Compléter affiche_justifie à partir d’une liste de mots, d’un découpage en couples et d’une justification.
Indice
Les mêmes bornes exclusives servent aux couples et aux tranchesPython.
Comprendre la correction
def affiche_justifie(liste_mots, decoupage, justification):
for debut, fin in decoupage:
ligne_justifiee = ajout_espace(liste_mots[debut:fin], justification)
print(ligne_justifiee)Chaque tuple désigne une tranche dont la borne finale est exclue. Le déballage fournit directement les deux indices. Un appel à ajout_espace transforme seulement les mots de cette ligne, puis print l’affiche. La fonction n’a pas besoin de renvoyer un texte complet : son contrat demande un affichage.
PartieC : le coût d’une ligne est le carré du nombre d’espaces supplémentaires ajoutés au-delà des séparations ordinaires. Le coût d’un découpage est la somme de ces coûts, dernière ligne comprise.
liste_mots = ['An','algorithm','must','be','seen','to','be','believed']Question 7
#Pour une justification 15 et le découpage[(0,2),(2,4),(4,7),(7,8)], compléter le tableau des coûts.
Indice
Soustraire les lettres et les séparations obligatoires avant de mettre au carré.
Comprendre la correction
| Début | Fin exclue | Mots | Lettres | Espaces supplémentaires | Coût |
|---|---|---|---|---|---|
| 0 | 2 | 2 | 11 | 3 | 9 |
| 2 | 4 | 2 | 6 | 8 | 64 |
| 4 | 7 | 3 | 8 | 5 | 25 |
| 7 | 8 | 1 | 8 | 7 | 49 |
Le total vaut 9+64+25+49=147. Les espaces supplémentaires sont ceux qui s’ajoutent à l’unique séparation ordinaire entre chaque paire de mots. Par exemple must be a 6 lettres et un espace obligatoire, donc 15−7=8 espaces supplémentaires, coût 64. Pour believed seul, les 7 espaces à droite sont tous supplémentaires.
Question 8
#Écrire cout(i, j,liste_mots, justification), pour les mots d’indicei àj−1. Le coût vaut un million si la ligne dépasse la largeur.
Indice
La ligne contient j−i mots et donc j−i−1 séparations minimales.
Comprendre la correction
def cout(i, j, liste_mots, justification):
longueur = sum(len(mot) for mot in liste_mots[i:j]) + (j - i - 1)
if longueur > justification:
return 1000000
return (justification - longueur) ** 2La longueur minimale combine les lettres de la tranche et ses j−i−1 séparations.C’est cette longueur qu’il faut comparer à la justification. Lorsque la ligne tient, la différence est le nombre d’espaces supplémentaires. Le carré pénalise davantage une ligne très étirée que plusieurs lignes peu étirées.
assert cout(0,2,liste_mots,15) == 9
assert cout(0,4,liste_mots,15) == 1000000
assert cout(5,8,liste_mots,15) == 1Le contrat utilisé par les algorithmes porte sur une tranche non vide,0≤i< j≤n. Le nombre 1000000 est la pénalité imposée par le sujet pour une ligne impossible ; une implantation générale utiliserait plutôt l’infini pour que cette pénalité ne puisse jamais être préférable à un coût valide très grand.
Question 9
#Avec n≥50 mots, est-il raisonnable de tester tous les découpages possibles ?
Indice
Compter les choix entre les mots, pas après le dernier.
Comprendre la correction
Non en général. Il y a n−1 positions possibles de retour à la ligne, avec deux choix indépendants à chacune : couper ou ne pas couper. Donc 2ⁿ⁻¹ découpages potentiels. Pour 50 mots, cela donne 2⁴⁹=562949953421312 possibilités avant même leur évaluation. Beaucoup sont invalides, mais les énumérer toutes reste prohibitif.
L’illustration de l’énoncé compare une heuristique gloutonne, sans garantie d’optimum, et la programmation dynamique, qui mémorise le meilleur coût de chaque suffixe de mots.L’IA citée dans l’énoncé fournit ce programme à analyser :
def justifie_dynamique(liste_mots, justification):
n = len(liste_mots)
cout_mini = [0] * n
indice_retour_ligne_mini = [0] * n
for i in range(n - 1, -1, -1):
cout_mini[i] = cout(i, n, liste_mots, justification)
indice_mini = n
for j in range(i + 1, n):
best = cout_mini[j] + cout(i, j, liste_mots, justification)
if best < cout_mini[i]:
cout_mini[i] = best
indice_mini = j
indice_retour_ligne_mini[i] = indice_mini
decoupage = []
k = 0
while k < n:
decoupage.append((k, indice_retour_ligne_mini[k]))
k = indice_retour_ligne_mini[k]
return decoupageQuestion 10
#Donner l’ordre de grandeur du nombre d’appels à cout dans justifie_dynamique pour n mots.
Indice
Compter séparément l’appel initial de chaquei et ceux de sa boucle interne.
Comprendre la correction
Le nombre exact est n+n(n−1)/2=n(n+1)/2, donc Θ(n²) appels. Chaque indicei effectue un appel initial cout(i, n), puis n−i−1 appels dans la boucle surj. La somme forme une série arithmétique.
La question porte sur le nombre d’appels, pas sur le temps total. Si chaque cout parcourt une tranche pour additionner ses longueurs, ces appels ne coûtent pas tous un temps constant. Une somme préfixe des longueurs pourrait accélérer ce calcul, mais ne change pas le décompte quadratique des appels demandés ici.
Question 11
#Établir la relation entre les éléments de cout_mini à partir du programme.
Indice
Décomposer un suffixe en une première ligne et un suffixe plus court.
Comprendre la correction
Notons C[i] le coût minimal du suffixe commençant au moti et posons C[n]=0. Pour chaquei, on choisit l’indicej qui termine la première ligne :C[i] = min(cout(i,j,liste_mots,justification) + C[j] pour i<j≤n). Le code traite j=n dans l’initialisation, et les autres j dans la boucle.
Les indicesi sont parcourus en décroissant : lorsque C[i] est calculé, tous les C[j] avecj> i sont déjà connus. Chaque candidat combine le coût de sa première ligne avec le meilleur coût possible de tout ce qui suit. Le tableau indice_retour_ligne_mini mémorise ensuite le choixj pour reconstruire un découpage, au lieu de recalculer les décisions.
Pour les huit mots et une justification 15, on obtient C=[19,42,10,41,74,1,16,49]. Le découpage optimal[(0,2),(2,5),(5,8)] coûte 9+9+1=19, contre 147 pour le découpage de la question 7.
Question 12
#Modifier justifie_dynamique pour renvoyer également le coût du découpage.
Indice
Le suffixe qui commence à 0 est le texte complet.
Comprendre la correction
return decoupage, (cout_mini[0] if n > 0 else 0)Cette instruction remplace le return final.C[0] représente le coût optimal de tout le texte ; le coût d’un texte vide est 0. Le résultat devient un tuple(découpage, coût), par exemple([(0,2),(2,5),(5,8)],19) pour la phrase avec justification 15. Il n’est pas nécessaire de recalculer les coûts des lignes : ils ont déjà servi à la programmation dynamique.
Comparer le remplissage glouton et le découpage optimalUn atelier pour expérimenter
Changez la largeur pour les huit mots du sujet. Le tableau affiche les lignes et leur coût. Les espaces sont rendus visibles par· ; la dernière ligne est pénalisée comme les autres, selon l’énoncé.
Lire le résultat de l’expérience initiale
Coût total : 19.
Un remplissage maximal peut rendre la ligne suivante très étirée.La programmation dynamique compare le coût de la ligne et celui du suffixe restant.
| Tranche | Ligne justifiée | Coût |
|---|---|---|
| [0,2[ | An····algorithm | 9 |
| [2,5[ | must···be··seen | 9 |
| [5,8[ | to··be·believed | 1 |
Optimiser une ligne isolée ne suffit pas : le meilleur choix dépend du texte restant.
Revoir les notions de cet exercice
Exercice 2 · 6 points
Construire et utiliser un arbre de compression de Huffman
Le codage de Huffman adapte les codes binaires aux fréquences des caractères. Pour « julie fuit la pluie », l’arbre arb_julie porte 19 occurrences au total. Une branche gauche ajoute 0 au code, une branche droite ajoute 1. Les caractères sont placés aux feuilles et le poids d’un nœud interne est la somme des poids de ses descendants.
arb_julie, reconstruit avec les mêmes branches et poids. Le mot espace représente le caractère « ».Le mot julie se code j→0100, u→111, l→100, i→101, e→011, soit 0100111100101011. Le caractère espace se code 00.L’arbre doit être connu pour décoder ces codes de longueurs différentes.
Question 1
#Donner un exemple de feuille et la racine de la figure 1.
Indice
Repérez les caractères isolés au bas des branches et le nœud supérieur.
Comprendre la correction
La feuille (j,1) convient ; toute autre feuille de caractère serait aussi un exemple. La racine regroupe tous les caractères et porte le poids 19 : son nom suit l’ordre espace-j-f-e-l-i-p-t-a-u. Une feuille n’a pas d’enfant ; la racine n’a pas de parent.
Question 2
#Donner la profondeur du nœudp et son code binaire.
Indice
Comptez les arêtes, pas les nœuds traversés.
Comprendre la correction
Sa profondeur vaut 4 arêtes et son code est 1100 : droite, droite, gauche, gauche depuis la racine. La profondeur est donc ici aussi la longueur du code. La racine est à la profondeur 0.
Question 3
#Quel est l’intérêt de donner de faibles profondeurs aux caractères fréquents ?
Indice
Une économie d’un bit se répète à chaque occurrence du caractère.
Comprendre la correction
Chaque occurrence d’un caractère utilise autant de bits que son code. Si un caractère apparaît f fois et a une profondeur d, il contribue f×d bits au texte compressé. Réduire la profondeur d’un caractère fréquent économise donc davantage de bits que réduire celle d’un caractère rare. Le but est de minimiser cette somme pondérée des longueurs, pas de rendre toutes les feuilles également profondes.
La profondeur n’est pas strictement décroissante avec chaque fréquence à cause des contraintes de structure et des égalités. La propriété utile est globale : le code de Huffman minimise la longueur pondérée parmi les codes préfixes construits pour ces fréquences.
class Noeud:
def __init__(self,nom,nb_occu,fils_g,fils_d):
....nom = nom
....nb_occu = nb_occu
....fils_g = fils_g
....fils_d = fils_d
def __str__(self):
return '(' + ... .nom + ',' + str(...) + ')'Question 4
#Compléter le constructeur et __str__ de Noeud.
Indice
Les quatre affectations portent sur l’objet courant.
Comprendre la correction
class Noeud:
def __init__(self, nom, nb_occu, fils_g, fils_d):
self.nom = nom
self.nb_occu = nb_occu
self.fils_g = fils_g
self.fils_d = fils_d
def __str__(self):
return '(' + self.nom + ',' + str(self.nb_occu) + ')'self désigne l’instance dont les attributs sont initialisés. La conversion str de nb_occu permet de le concaténer aux chaînes de caractères.__str__ renvoie le texte, elle ne l’affiche pas directement. Les enfants serontNone pour une feuille et des objetsNoeud pour un nœud interne.
def liste_occurrences(chaine):
dico = ...
for c in chaine:
if c in ...:
dico[c] = dico[c] + 1
else:
...
liste_res = ...
for cle in dico:
liste_res....
return ...Question 5
#Compléter liste_occurrences qui renvoie les tuples(caractère, nombre d’occurrences).
Indice
Séparez le comptage dans le dictionnaire et la construction de la liste finale.
Comprendre la correction
def liste_occurrences(chaine):
dico = {}
for c in chaine:
if c in dico:
dico[c] += 1
else:
dico[c] = 1
liste_res = []
for cle in dico:
liste_res.append((cle, dico[cle]))
return liste_resUne première occurrence crée le compteur à 1 ; les suivantes l’incrémentent. Le dictionnaire conserve l’ordre d’apparition des clés, ce qui donne l’ordre attendu dans l’exemple. La seconde boucle convertit les associations en tuples, un tuple par caractère distinct. La somme des compteurs doit être égale à la longueur du texte, espaces inclus.
liste_occurrences('julie fuit la pluie')
# [('j',1),('u',3),('l',3),('i',3),('e',2),
# (' ',3),('f',1),('t',1),('a',1),('p',1)]def tri_liste(liste_a_trier):
liste_triee = []
for i in range(0,...):
element = liste_a_trier[i]
...
while j < len(liste_triee) and element[1] >= liste_triee[j][1]:
...
liste_triee.insert(...,...)
return liste_trieeQuestion 6
#Compléter tri_liste par insertion selon les nombres d’occurrences croissants.
Indice
Le champ 1 de chaque tuple est le nombre d’occurrences.
Comprendre la correction
def tri_liste(liste_a_trier):
liste_triee = []
for i in range(len(liste_a_trier)):
element = liste_a_trier[i]
j = 0
while j < len(liste_triee) and element[1] >= liste_triee[j][1]:
j += 1
liste_triee.insert(j, element)
return liste_trieej part de 0 pour chaque élément et avance tant que les poids déjà présents sont inférieurs ou égaux. Le nouvel élément est inséré après ces poids et avant le premier poids strictement plus grand. La comparaison>= conserve l’ordre relatif des tuples de même fréquence : ce tri est stable. Il ne modifie pas liste_a_trier, mais construit une autre liste.
La garde j<len(liste_triee) est placée en premier pour éviter un accès hors limites. Quand le nouvel élément est le plus grand, j atteint la longueur et insert l’ajoute à la fin. Un élément de poids minimal est inséré à l’indice 0.
Question 7
#Écrire conversion_en_noeuds qui transforme une liste de tuples en liste de feuillesNoeud.
Indice
Un tuple correspond à une feuille, sans enfant.
Comprendre la correction
def conversion_en_noeuds(liste):
return [Noeud(c, nb, None, None) for c, nb in liste]Chaque tuple fournit le nom du caractère et son poids. Comme ces nœuds représentent encore des caractères seuls, leurs deux enfants valentNone.L’ordre est conservé : une liste de tuples triée devient une liste de nœuds triée selon le même poids.
def insere_noeud(noeud,liste_noeud):
j = 0
while j < len(liste_noeud) and ... > ...:
...
liste_noeud.insert(...,...)Question 8
#Compléter insere_noeud pour modifier une liste de nœuds déjà triée par poids croissant.
Indice
Comparez les attributsnb_occu, pas les noms des groupes.
Comprendre la correction
def insere_noeud(noeud, liste_noeud):
j = 0
while j < len(liste_noeud) and noeud.nb_occu > liste_noeud[j].nb_occu:
j += 1
liste_noeud.insert(j, noeud)La comparaison stricte imposée par le squelette insère le nouveau nœud avant les nœuds de même poids. La liste reste triée, mais cette convention d’égalité diffère de celle du tri précédent. Elle contribue à reproduire précisément l’arbre de la figure lorsque plusieurs combinaisons de mêmes poids sont possibles. La fonction modifie la liste et ne renvoie pas une nouvelle liste.
def construit_arbre(liste):
while ... > 1:
noeud1 = liste.pop(0)
noeud2 = liste.pop(0)
nom_noeud_pere = noeud1.nom + "-" + noeud2.nom
nb_occu_noeud_pere = ...
noeud_pere = Noeud(...)
insere_noeud(...,liste)
return ...Question 9
#Compléter construit_arbre : extraire les deux poids minimaux, créer leur père, puis le réinsérer jusqu’à obtenir la racine.
PythonConstruire un arbre de Huffman en regroupant les deux plus petits poidsÉcrivez votre solution et mettez-la à l’épreuve
Complétez construit_arbre(liste). La liste non vide contient des nœuds triés par poids nb_occu croissant. À chaque étape, extrayez les deux premiers, créez leur père dont le nom joint leurs noms par un tiret et dont le poids est leur somme, puis réinsérez-le avec insere_noeud. Ce helper place un nouveau nœud avant les poids égaux, conformément au sujet. Renvoyez la racine ; la liste d’entrée doit ne contenir plus que cette racine. Les sous-arbres doivent rester les objets extraits.
def construit_arbre(liste):
# À vous de jouer
passLes cas de test proposés :
- Une seule feuille est déjà la racine : Une liste de longueur 1 ne demande aucun regroupement.
- Deux feuilles, ordre des enfants conservé : Les deux nœuds extraits deviennent les enfants du nouveau nœud, dans cet ordre.
- Un poids égal : insérer le père avant l’ancienne feuille : a-b pèse 3 et se place avant c, qui pèse aussi 3. La convention d’égalité détermine cet arbre précis.
- Réinsérer ne signifie pas ajouter à la fin : Après le premier regroupement, le père de poids 2 doit encore participer au prochain choix minimal.
- La somme des poids et les feuilles sont préservées : On regroupe les références existantes sans perdre ni dupliquer une feuille.
Indice
Le poids du père additionne les occurrences, ses enfants conservent les objets.
Comprendre la correction
def construit_arbre(liste):
assert len(liste) > 0
while len(liste) > 1:
noeud1 = liste.pop(0)
noeud2 = liste.pop(0)
nom_noeud_pere = noeud1.nom + '-' + noeud2.nom
nb_occu_noeud_pere = noeud1.nb_occu + noeud2.nb_occu
noeud_pere = Noeud(nom_noeud_pere,nb_occu_noeud_pere,noeud1,noeud2)
insere_noeud(noeud_pere, liste)
return liste[0]Les deux pop(0) extraient successivement les deux premiers éléments de la liste triée. Le poids du père est leur somme et ses enfants sont les deux objets extraits. Le nom concatène leurs noms pour décrire les caractères regroupés. Le père est réinséré à sa place selon son poids, ce qui maintient la précondition de l’itération suivante.
Chaque tour enlève deux nœuds actifs et en remet un : la longueur de la liste diminue de 1. Avec k caractères distincts, on effectue k−1 regroupements. Dans cet exemple, les deux premières feuilles regroupées sont j et f, de poids 1 chacune. La liste d’entrée est modifiée. La garde précise que construire cet arbre attend au moins une feuille ; le texte vide demanderait une convention d’arbre vide séparée.
codage = {
" ": "00",
"j": "0100",
"f": "0101",
"e": "011",
"l": "100",
"i": "101",
"p": "1100",
"t": "11010",
"a": "11011",
"u": "111"
}Question 10
#Identifier la structure renvoyée par codage_arbre dans l’exemple.
Indice
Observez les associationsclé: valeur entre accolades.
Comprendre la correction
C’est un dictionnaire Python. Ses clés sont les caractères et ses valeurs les chaînes de bits de leurs codes. Chaque caractère doit apparaître une seule fois comme clé ; les codes peuvent avoir des longueurs différentes.
Question 11
#Écrire compresse(texte, codage), qui renvoie une chaîne de bits en remplaçant chaque caractère par son code.
Indice
Un code est une chaîne : la concaténation préserve ses zéros initiaux.
Comprendre la correction
def compresse(texte, codage):
resultat = ''
for caractere in texte:
resultat += codage[caractere]
return resultatLe parcours respecte l’ordre du texte et concatène les codes sans séparateur.L’arbre de Huffman produit un code préfixe : aucun code de feuille n’est le préfixe d’un autre, ce qui permet de retrouver les frontières lors du décodage en parcourant l’arbre. La table doit contenir tous les caractères du message ; sinon l’accès au dictionnaire échoue, ce qui signale un codage inadapté.
assert compresse('julie',codage) == '0100111100101011'
assert compresse('',codage) == ''Le mot julie utilise 16 bits avec cette table. La phrase complète utilise 61 bits de données, contre 152 pour 19 caractères à 8 bits. Cette comparaison n’inclut pas le stockage ou la transmission de l’arbre : pour un message aussi court, cet overhead peut peser davantage que l’économie.
Encoder un message avec l’arbre deJulieUn atelier pour expérimenter
Saisissez un texte composé des caractères de la phrase du sujet. La table reste celle d’arb_julie : l’atelier montre la concaténation et le nombre de bits, sans reconstruire un arbre différent pour chaque message.
Lire le résultat de l’expérience initiale
61 bits de données pour19 caractères.
Les caractères fréquents ont souvent des codes courts.La taille indiquée exclut la table de codage et l’arbre nécessaires au décodage.
| Caractère | Code | Bits cumulés |
|---|---|---|
| j | 0100 | 4 |
| u | 111 | 7 |
| l | 100 | 10 |
| i | 101 | 13 |
| e | 011 | 16 |
| espace | 00 | 18 |
| f | 0101 | 22 |
| u | 111 | 25 |
| i | 101 | 28 |
| t | 11010 | 33 |
| espace | 00 | 35 |
| l | 100 | 38 |
| a | 11011 | 43 |
| espace | 00 | 45 |
| p | 1100 | 49 |
| l | 100 | 52 |
| u | 111 | 55 |
| i | 101 | 58 |
| e | 011 | 61 |
La fréquence détermine le coût total ; la propriété de préfixe permet de décoder sans séparateurs.
Exercice 3 · 8 points
Balades dans un parc d’attractions et vente de photos
Le parc est modélisé par un graphe dont les sommets sont des attractions au nom unique. Chaque attraction a une durée, chaque arête un temps de déplacement. Les durées se mesurent en minutes. Les objetsAttraction portent nom, duree et une liste de voisines sous forme de tuples(attraction, durée du trajet).
class Attraction:
def __init__(self, nom, duree):
self.nom = nom
self.duree = duree
self.voisines = []a1 = Attraction('Grand huit',11)
a2 = Attraction('Petits chevaux',6)
a3 = Attraction('Train fantôme',9)
a4 = Attraction('Grande roue',10)
a1.voisines = [(a2,7),(a3,5)]
a2.voisines = [(a1,7),(a3,3),(a4,4)]
a3.voisines = [(a1,5),(a2,3),(a4,6)]
a4.voisines = ...Question 1
#La grande roue dure désormais12 minutes. Écrire la modification de l’objet correspondant.
Indice
La durée d’une attraction est un attribut du sommet.
Comprendre la correction
a4.duree = 12La variablea 4 référence la grande roue. On modifie son attributduree ; les temps de déplacement des arêtes restent inchangés. Aucune nouvelle instance n’est nécessaire.
Question 2
#Expliquer a2.voisines[2][1] et donner sa valeur.
Indice
Lire les deux accès successivement : liste, puis tuple.
Comprendre la correction
La valeur est 4.L’indice 2 sélectionne le troisième tuple des voisines de a2, soit(a4,4).L’indice 1 de ce tuple sélectionne ensuite la durée du trajet,4 minutes. Ce n’est ni la durée de la grande roue ni son numéro.
Question 3
#Expliquer la ligne 7 qui définit les voisines de a3.
Indice
Associer chaque variable à son attraction avant de lire la durée.
Comprendre la correction
Le train fantôme est relié au grand huit en 5 minutes, aux petits chevaux en 3 minutes et à la grande roue en 6 minutes. Les premières composantes sont les objetsAttraction correspondants ; les secondes sont les durées des trajets. Cette liste sert à retrouver à la fois les connexions et leurs poids.
Question 4
#Compléter la liste des voisines de a4.
Indice
Repérer les deux arêtes incidentes à la grande roue.
Comprendre la correction
a4.voisines = [(a2,4),(a3,6)]La grande roue est reliée aux petits chevaux et au train fantôme. Les liaisons sont enregistrées dans les deux sens puisqu’on utilise un graphe non orienté. Il n’existe pas de liaison directe entre a4 et a1.
Question 5
#Pourquoi utiliser un graphe non orienté ?
Indice
Le sens du déplacement change-t-il la liaison décrite ?
Comprendre la correction
Les chemins du parc peuvent être parcourus dans les deux sens avec les mêmes durées dans ce modèle. Une seule arête représente donc la liaison réciproque. Si un trajet était à sens unique ou avait des durées différentes selon le sens, une représentation orientée serait nécessaire ; ce n’est pas l’hypothèse du sujet.
Une balade est un chemin du graphe ; les usagers font les attractions dans cet ordre. Sa durée additionne les durées des attractions et des trajets. Une attraction peut encore être répétée à ce stade, comme dans[a1,a2,a3,a1,a3].
Question 6
#Calculer la durée de la balade[a1,a2,a3] et expliquer le calcul.
Indice
La durée additionne des sommets et des arêtes.
Comprendre la correction
On additionne les attractions 11+6+9=26 minutes et les trajetsa 1-a2=7 minutes, puis a2-a3=3 minutes. La balade dure donc 26+7+3=36 minutes. On compte chaque attraction parcourue, y compris celle de départ et celle d’arrivée.
Question 7
#Pourquoi[a2,a1,a4,a3] n’est-elle pas une balade ?
Indice
Contrôler chaque paire consécutive de la liste.
Comprendre la correction
Les attractionsa 1 et a4 ne sont pas directement voisines. Le passage du grand huit à la grande roue rompt donc la condition de chemin, même si les deux autres transitions existent. Il ne suffit pas que toutes les attractions soient présentes dans le graphe.
Question 8
#Écrire sont_voisines(a, b), qui indique si deux objetsAttraction sont voisins. On admet leur comparaison avec==.
Indice
La liste contient des couples, donc il faut en extraire l’attraction.
Comprendre la correction
def sont_voisines(a,b):
for voisine, duree in a.voisines:
if voisine == b:
return True
return FalseChaque élément de a.voisines est un tuple : on compare sa première composante àb. Un test b in a.voisines comparerait l’objetb à des tuples et ne répondrait pas correctement. La durée est déballée mais n’intervient pas dans le booléen. Un voisin trouvé permet un retourTrue ;False doit attendre la fin de la boucle.
Question 9
#Écrire est_balade(tableau), qui vérifie les voisinages successifs.
Indice
Vérifier la propriété demandée sans lui ajouter une contrainte future.
Comprendre la correction
def est_balade(tableau):
for i in range(len(tableau) - 1):
if not sont_voisines(tableau[i], tableau[i + 1]):
return False
return TrueLa boucle traite les n−1 transitions entre n attractions. Le premier défaut suffit à répondreFalse ;True après la boucle signifie que toutes les transitions ont été validées. Cette version accepte une balade d’une attraction et, par convention de test de toutes les transitions, le tableau vide. Si l’application exige au moins une attraction, il faudrait ajouter cette précondition explicitement.
Les répétitions ne sont pas interdites par cette question : elles le seront dans la création automatique décrite ensuite. Il ne faut donc pas ajouter un test d’unicité qui rejetterait l’exemple[a1,a2,a3,a1,a3].
Pour les balades créées automatiquement, chaque attraction doit apparaître au maximum une fois. On préalloue un tableau de la taille du parc, rempli de None ; nb compte les cases effectivement utilisées. Le programme proposé est :
def parcours(attr, deja_vues, balade, nb):
if attr.nom not in deja_vues:
deja_vues[attr.nom] = True
if nb == 0 or sont_voisines(attr, balade[nb - 1]):
balade[nb] = attr
nb += 1
for voisine in attr.voisines:
nb = parcours(voisine[0], deja_vues, balade, nb)
return nbQuestion 10
#Quel type de parcours réalise la fonction parcours fournie ?
Indice
Observer l’appel récursif à l’intérieur de la boucle des voisines.
Comprendre la correction
Un parcours en profondeur : l’appel récursif explore une voisine et ses descendantes avant de passer à la voisine suivante. Le dictionnairedeja_vues évite de retraiter une attraction déjà rencontrée, ce qui empêche les cycles de provoquer une récursion sans fin.
Question 11
#Après balade=[None for _ in range(4)] puis parcours(a4,{}, balade,0), que contient balade ?
Indice
Suivre l’ordre exact des tuples de chaque listevoisines.
Comprendre la correction
[a4, a2, a1, a3]Attraction découverte | Ajout à la balade | État utile |
|---|---|---|
a4 | Oui, première attraction | [a4] |
a2 | Oui, voisine de a4 | [a4, a2] |
a1 | Oui, voisine de a2 | [a4, a2, a1] |
a3 | Oui, voisine de a1 | [a4, a2, a1, a3] |
Les voisines sont traitées dans l’ordre des listes données. Tous les autres appels concernent alors des noms déjà présents dans deja_vues et n’ajoutent rien. La valeur de retour vaut 4, mais la question porte sur le contenu du tableau modifié.
a2.voisines = [(a1,7),(a3,3)]
a4.voisines = [(a3,6)]
tableau = [None for _ in range(4)]
parcours(a3,{},tableau,0)Question 12
#Après avoir retiré la liaisona 2-a4 dans les deux listes, puis appelé parcours(a3,{}, tableau,0), que contient tableau ?
Indice
Au retour de récursion, la dernière attraction ajoutée peut différer de l’attraction courante.
Comprendre la correction
[a3, a1, a2, None]Depuis a3, on visite d’aborda 1, puis a2, qui sont ajoutées. La découverte suivante de a4 se produit après le retour récursif à a3, mais la dernière attraction de la balade reste a2. Or a4 n’est plus voisine de a2 : elle est marquée vue sans être ajoutée. Le compteur final vaut 3 et la dernière case resteNone.
Cet exemple montre que le parcours des sommets et la balade produite ne sont pas la même liste. Le code garantit des transitions valides entre les attractions ajoutées, mais ne garantit pas d’inclure toutes les attractions ni de produire la balade la plus longue.
Question 13
#Identifier la structuredeja_vues et expliquer son rôle.
Indice
Le code teste les clés puis effectue deja_vues[attr.nom]=True.
Comprendre la correction
C’est un dictionnaire : l’appel fournit {} et le code associe chaque nom d’attraction à True. Son rôle est de mémoriser les attractions déjà traitées pour ne pas les explorer plusieurs fois. Les noms sont uniques par hypothèse, donc ils peuvent servir de clés de visite.
PartieB : les visiteurs qui acceptent reçoivent un bracelet permettant leur identification et la prise de photos, ensuite proposées à la vente.L’énoncé précise que les personnes refusant ne sont pas photographiées et décrit le stockage des données et les droits d’accès. La base utilisée est :
| Relation | Attributs | Références |
|---|---|---|
| visiteur | id INT(primaire), nom TEXT, prenom TEXT, date TEXT | Une ligne correspond à une visite datée |
| photo | id INT(primaire),id_visiteur INT,id_attraction INT, heure TEXT, prix FLOAT | id_visiteur→visiteur.id ;id_attraction→attraction.id |
| attraction | id INT(primaire), nom TEXT, duree INT | Aucune clé étrangère |
La date est au format AAAA-MM-JJ, par exemple 2025-02-01.L’heure est au formatHH: MM, par exemple 05:49. Ces formats fixes permettent une comparaison chronologique par l’ordre des chaînes.
Question 14
#Expliquer clé primaire et clé étrangère.
Indice
Donner la règle et un exemple tiré du schéma.
Comprendre la correction
Une clé primaire identifie une ligne par une valeur unique et non nulle. Une clé étrangère référence une clé d’une autre relation et doit correspondre à une ligne existante, lorsque la référence n’est pas autorisée à être nulle. Ici, photo.id_visiteur relie chaque cliché à une visite et photo.id_attraction à une attraction.
Question 15
#Obtenir sans doublons les noms et prénoms des visiteurs présents le11janvier 2025.
SQLLister les visiteurs d’une journée sans doublonsÉcrivez votre solution et mettez-la à l’épreuve
Renvoyez sans doublons les couples nom-prénom des visiteurs présents le 11 janvier 2025. La date du schéma est écrite AAAA-MM-JJ.
SELECT ...
FROM ...
WHERE ...;Les cas de test proposés :
- Jeu pédagogique sur le schéma officiel : Le sujet fournit le schéma sans extrait de visiteurs. Ces visites sont donc des données pédagogiques explicitement ajoutées pour tester la date et les doublons de projection.
- Cas complémentaire : même jour dans un autre mois : Jeu complémentaire pédagogique, distinct des données officielles. Le11 janvier ne doit pas être confondu avec le 1er novembre ou le 12 janvier.
Indice
Le format utilise le mois et le jour sur deux chiffres.
Comprendre la correction
SELECT DISTINCT nom, prenom FROM visiteur WHERE date = '2025-01-11';Le filtre porte sur la date complète au format du schéma.DISTINCT élimine les paires nom-prénom répétées dans le résultat. Il ne supprime aucune visite enregistrée dans la base.
Question 16
#AlanTURING est venu plusieurs fois en 2024 et a acheté toutes les photos proposées. Obtenir la somme totale payée.
Indice
La date appartient à visiteur ; le prix appartient à photo.
Comprendre la correction
SELECT SUM(p.prix)
FROM photo AS p JOIN visiteur AS v ON v.id = p.id_visiteur
WHERE v.nom = 'TURING' AND v.prenom = 'Alan'
AND v.date >= '2024-01-01' AND v.date < '2025-01-01';La jointure rattache chaque photo à sa visite, ce qui permet de sélectionner le nom, le prénom et les dates de 2024. Le filtre de date couvre toute l’année et exclut 2025.L’hypothèse « toutes les photos achetées » autorise la somme des prix proposés ; sans elle, il manquerait une information d’achat.
Une jointure vers attraction n’est pas nécessaire pour ce total.SUM additionne les prix des lignes retenues. Si aucune ligne ne correspond, SQL renvoie généralementNULL ; COALESCE pourrait donner 0 pour un affichage, mais n’est pas requis par la question.
SELECT visiteur.nom, prenom
FROM visiteur JOIN photo ON visiteur.id = photo.id_visiteur
JOIN attraction ON attraction.id = photo.id_attraction
WHERE attraction.nom = 'Grande roue'
AND heure = '12:34' AND date = '2024-07-26';Question 17
#Expliquer ce que cherche la requête fournie sur Grande roue,12:34 et 2024-07-26.
Indice
Interprétez ensemble les trois filtres et les deux clés étrangères.
Comprendre la correction
Les gérants cherchent les noms et prénoms des visiteurs photographiés sur la grande roue à12h 34 lors de leur visite du26juillet 2024. Les jointures relient chaque cliché à son visiteur et à son attraction. Le résultat ne signifie pas que toutes les personnes présentes dans le parc à cette heure ont été photographiées, ni que ces photos ont été achetées. La requête n’utilise pas DISTINCT : une personne peut réapparaître si plusieurs clichés satisfont les filtres.
Question 18
#Proposer une modification de la base permettant plusieurs formats et supports de vente pour un même cliché(A5, A6, poster, porte-clé…).
Indice
Séparer le cliché, l’objet vendu et le prix de leur association.
Comprendre la correction
Conserver photo pour le cliché et ses informations de prise de vue, puis ajouter une relation support décrivant chaque format et une relation offre_photo reliant un cliché à un support avec son prix. La clé composée(id_photo,id_support) empêche de créer deux offres identiques pour le même couple. Les deux identifiants sont des clés étrangères vers photo et support.
| Relation proposée | Attributs et contraintes |
|---|---|
| support | id : clé primaire ; libelle : texte(A5, A6, poster, porte-clé…) |
offre_photo | id_photo : référencephoto.id ;id_support : référencesupport.id ; prix : nombre ; clé primaire(id_photo,id_support) |
Le prix, qui dépend désormais de l’offre, est déplacé de photo vers offre_photo. Les anciennes valeurs doivent être migrées vers une offre de support standard avant la suppression de l’attributinitial. Il faut éviter une colonne textuelle contenant « A5, A6, poster » : elle mélangerait plusieurs valeurs et ne permettrait pas d’associer proprement chaque prix à son support.
Ce modèle décrit ce qui est proposé à la vente. Si le parc veut aussi enregistrer les quantités effectivement achetées, une relation d’achat sera nécessaire, mais cela représente un besoin supplémentaire à celui de l’offre demandé ici.
Découvrir une attraction sans pouvoir l’ajouterUn atelier pour expérimenter
Choisissez le départ et retirez éventuellement le chemin entre petits chevaux et grande roue.L’atelier reproduit l’ordre récursif du code et distingue les attractions visitées de celles ajoutées à la balade.
Lire le résultat de l’expérience initiale
Balade :[a4, a2, a1, a3].
Le marquage a lieu avant le test d’ajout.La récursion suit le graphe,mais seules les attractions adjacentes à la fin actuelle de la balade sont ajoutées.
| Découverte | Décision | Balade utile |
|---|---|---|
| a4 | Ajoutée | a4 |
| a2 | Ajoutée | a4, a2 |
| a1 | Ajoutée | a4, a2, a1 |
| a3 | Ajoutée | a4, a2, a1, a3 |
Un sommet marqué comme visité n’est pas forcément un sommet ajouté à la solution construite.
Du sujet à la méthode
Votre prochaine séance de révision
- Distinguer les différentes notions de coût : espaces supplémentaires, bits et minutes.
- Pour une récursion, observer les états modifiés avant et après chaque appel.
- En SQL, placer chaque donnée dans la relation dont elle dépend réellement.
Retrouver ces notions dans d’autres sujets
Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027
Énoncé : sujet 25-NSIPE2 (PDF) · Publication d’origine (nouvel onglet). Corrigé et explications pédagogiques proposés par Sofien.
