NSI Terminale • Programme français, lycées de Montréal

Exercices corrigés de NSI : algorithmique, récursivité et complexité (Terminale)

Voici une série d'exercices corrigés d'algorithmique pour la spécialité NSI de Terminale. Elle s'adresse aux élèves des lycées français de Montréal, le Lycée Marie de France et le Collège Stanislas. La Première a désormais sa propre série sur son programme d'algorithmique ; celle-ci reprend les tris et la dichotomie pour y ajouter ce que la Terminale demande, la récursivité, la recherche d'un motif dans un texte et les k plus proches voisins.

L'algorithmique se juge sur trois questions, et l'épreuve les pose toutes les trois : l'algorithme donne-t-il le bon résultat (correction), s'arrête-t-il toujours (terminaison), et combien d'opérations coûte-t-il (complexité) ? Écrire un programme qui marche sur l'exemple du cours ne répond à aucune des trois. Chaque exercice ci-dessous vous fait compter, prouver ou comparer, jamais seulement coder.

Série autocorrigée Tape tes réponses sous chaque question : la page te dit juste ou faux avant d'ouvrir la correction. Avec un compte, chaque bonne réponse du premier coup rapporte des points.

Ce chapitre fait partie de NSI en Terminale
Avant de commencer Fiche de révision : les pièges et la méthode de ce chapitre

Avant ce chapitre

Ces notions sont supposées acquises ici. Si le premier exercice résiste, le blocage vient presque toujours de l'une d'elles, pas du chapitre lui-même.

Remonter plus loin : la chaîne complète (4 chapitres) ↓

Le chemin de remédiation, du plus ancien au plus proche. Un élève qui reprend ce chapitre de zéro le reprend dans cet ordre.

  1. 1Algorithmique et PythonSeconde, Mathématiques
  2. 2Python : types, contrôle, fonctions et tableauxPremière
  3. 3Algorithmique : preuve, terminaison et coûtPremière
  4. 4Algorithmique et ScratchTroisième, Mathématiques

Rappel de cours

  • Recherche séquentielle dans un tableau de nn éléments : au pire nn comparaisons, coût linéaire. Recherche dichotomique dans un tableau TRIÉ : environ log2n\log_{2}n comparaisons.
  • Tri par sélection : à chaque tour on cherche le minimum du reste et on l'échange. Nombre de comparaisons toujours égal à n(n1)2\dfrac{n(n-1)}{2}, quel que soit le tableau.
  • Tri par insertion : on insère chaque élément à sa place dans la partie déjà triée. Au pire n(n1)2\dfrac{n(n-1)}{2} comparaisons, mais seulement n1n-1 si le tableau est déjà trié.
  • Invariant de boucle : propriété vraie avant la boucle, préservée par chaque tour, et qui donne le résultat voulu à la sortie. C'est l'outil de preuve de correction.
  • Terminaison : exhiber un variant, entier positif qui décroît strictement à chaque tour.
  • Complexité : on garde l'ordre de grandeur et on ignore les constantes. O(1)O(1), O(logn)O(\log n), O(n)O(n), O(nlogn)O(n\log n), O(n2)O(n^{2}), O(2n)O(2^{n}).
  • Algorithme glouton : à chaque étape on prend le meilleur choix local, sans revenir en arrière. Rapide, mais pas toujours optimal.

Partie A : Les bases (/50)

Exercice 1 : Recherche séquentielle et coût au pire

On travaille sur le tableau t = [7, 2, 9, 4, 1, 8, 3], qui n'est pas trié.

Python
def recherche(t, x):
    for i in range(len(t)):
        if t[i] == x:
            return i
    return -1
  • a) Que renvoie recherche(t, 4) ? Et recherche(t, 5) ? Justifiez le choix de -1 comme valeur de retour.
  • b) Combien de comparaisons effectue la fonction pour chercher 7 ? Pour chercher 3 ? Pour chercher une valeur absente ?
  • c) Donnez le nombre de comparaisons dans le meilleur cas, dans le pire cas, et en moyenne pour un tableau de nn éléments contenant la valeur cherchée.
  • d) Modifiez la fonction pour qu'elle renvoie la LISTE de tous les indices où x apparaît. Que devient alors le coût au pire ?
  • e) Pourquoi ne peut-on pas utiliser la recherche dichotomique sur ce tableau ?

Tape tes réponses, la page te dit juste ou faux 0/10

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Indice 3, ou 1-1 si absent
  • b) 1, 7 et 7 comparaisons
  • c) Meilleur 1, pire nn, moyenne n+12\frac{n+1}{2}
  • d) Tous les indices : nn comparaisons toujours
  • e) Dichotomie impossible sans tri

a) recherche(t, 4) renvoie 3, car t[3] vaut 4. recherche(t, 5) renvoie -1 : la valeur est absente. On choisit -1 parce que c'est un indice IMPOSSIBLE en parcours normal, ce qui permet à l'appelant de distinguer sans ambiguïté l'échec du succès. Renvoyer 0 serait une faute grave, puisque 0 est un indice valide.

b) Pour 7 : une seule comparaison, il est en tête. Pour 3 : sept comparaisons, il est en dernière position. Pour une valeur absente : sept comparaisons également, la boucle va jusqu'au bout.

c) Meilleur cas : 1 comparaison, l'élément est en première position. Pire cas : nn comparaisons, l'élément est en dernier ou absent. En moyenne, si la valeur est présente et que toutes les positions sont équiprobables, on effectue 1+2++nn=n+12\dfrac{1+2+\cdots+n}{n}=\dfrac{n+1}{2} comparaisons, soit environ la moitié du tableau. Dans les trois cas le coût est proportionnel à nn : la recherche séquentielle est de complexité linéaire, O(n)O(n).

d) Il faut supprimer le return anticipé et accumuler les indices, donc parcourir tout le tableau. Le coût devient exactement nn comparaisons dans TOUS les cas, y compris le meilleur : on perd la possibilité de s'arrêter tôt. C'est le prix à payer pour une réponse exhaustive.

e) Parce que le tableau n'est pas trié. La dichotomie repose entièrement sur le fait que comparer la valeur cherchée à l'élément du milieu permet d'éliminer une moitié entière ; si le tableau est en désordre, cette comparaison n'apprend rien sur la position des autres éléments. Trier d'abord coûte plus cher qu'une seule recherche séquentielle : cela ne vaut la peine que si l'on prévoit de nombreuses recherches sur le même tableau.

Python
def tous_les_indices(t, x):
    resultat = []
    for i in range(len(t)):
        if t[i] == x:
            resultat.append(i)
    return resultat        # cout : n comparaisons, toujours

Exercice 2 : Le tri par sélection et son compte exact

Le tri par sélection cherche le minimum de la partie non triée et l'échange avec le premier élément de cette partie. On trie t = [7, 2, 9, 4, 1, 8, 3].

Python
def tri_selection(t):
    for i in range(len(t)):
        m = i
        for j in range(i + 1, len(t)):
            if t[j] < t[m]:
                m = j
        t[i], t[m] = t[m], t[i]
    return t
  • a) Donnez l'état du tableau après chacun des trois premiers tours de la boucle externe.
  • b) Donnez le tableau final.
  • c) Combien de comparaisons la boucle interne effectue-t-elle au tour ii ? Déduisez-en le nombre TOTAL de comparaisons pour n=7n=7, puis la formule générale.
  • d) Ce nombre dépend-il du contenu du tableau ? Comparez le cas d'un tableau déjà trié et celui d'un tableau trié à l'envers.
  • e) Combien d'échanges effectue l'algorithme ? En quoi est-ce un avantage si les éléments sont coûteux à déplacer ?

Tape tes réponses, la page te dit juste ou faux 0/7

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Trois tours : 1, puis 2, puis 3 placés
  • b) [1, 2, 3, 4, 7, 8, 9]
  • c) n(n1)2\frac{n(n-1)}{2}, soit 21 pour n=7n = 7
  • d) Coût indépendant du contenu
  • e) nn échanges seulement

a) Tour i = 0 : le minimum du tableau entier est 1, en position 4 ; on l'échange avec 7, ce qui donne [1, 2, 9, 4, 7, 8, 3]. Tour i = 1 : le minimum de la fin est 2, déjà en place, le tableau est inchangé, [1, 2, 9, 4, 7, 8, 3]. Tour i = 2 : le minimum de la fin est 3, en position 6 ; on l'échange avec 9, ce qui donne [1, 2, 3, 4, 7, 8, 9].

b) Le tableau final est [1, 2, 3, 4, 7, 8, 9]. Il se trouve qu'ici il est déjà entièrement trié après trois tours, mais l'algorithme continue quand même : il ne le sait pas.

c) Au tour ii, la boucle interne parcourt les indices de i+1i+1 à n1n-1, soit n1in-1-i comparaisons. Le total est i=0n1(n1i)\sum_{i=0}^{n-1}(n-1-i), c'est-à-dire (n1)+(n2)++1+0(n-1)+(n-2)+\cdots+1+0, dont la valeur est n(n1)2\frac{n(n-1)}{2}. Pour n=7n=7 cela fait 7×62=21\dfrac{7\times 6}{2}=21 comparaisons.

d) Non, il n'en dépend pas du tout. Les deux boucles sont des for dont les bornes ne dépendent que de nn, et aucun test ne peut les interrompre. Un tableau déjà trié coûte exactement les mêmes 21 comparaisons qu'un tableau en désordre complet. C'est la signature du tri par sélection : son coût est le même dans le meilleur et dans le pire cas, ce qui le rend prévisible mais l'empêche de profiter d'un tableau presque trié. La complexité est O(n2)O(n^{2}) dans tous les cas.

e) L'algorithme effectue exactement nn échanges, un par tour de boucle externe (dont certains sont l'échange d'un élément avec lui-même). C'est très peu : là où le tri par insertion peut effectuer de l'ordre de n2n^{2} déplacements, le tri par sélection en fait nn. Si un élément est coûteux à déplacer, par exemple un gros enregistrement, minimiser les écritures devient plus important que minimiser les comparaisons, et le tri par sélection redevient intéressant.

Exercice 3 : Le tri par insertion et la comparaison des deux tris

Le tri par insertion prend les éléments un par un et les glisse à leur place dans la partie déjà triée, comme on range une main de cartes.

Python
def tri_insertion(t):
    for i in range(1, len(t)):
        valeur = t[i]
        j = i - 1
        while j >= 0 and t[j] > valeur:
            t[j + 1] = t[j]
            j = j - 1
        t[j + 1] = valeur
    return t
  • a) Déroulez l'algorithme sur [5, 3, 8, 1] : donnez l'état du tableau après chaque tour de la boucle for.
  • b) Combien de comparaisons la boucle while effectue-t-elle si le tableau est DÉJÀ TRIÉ ? Déduisez-en le coût total dans le meilleur cas.
  • c) Même question si le tableau est trié à l'envers. Donnez le coût total dans le pire cas.
  • d) Comparez tri par sélection et tri par insertion sur trois critères : coût au mieux, coût au pire, nombre de déplacements.
  • e) Un tableau de 10 000 éléments est presque trié : seuls 5 éléments sont mal placés. Lequel des deux tris choisissez-vous, et pourquoi ?

Tape tes réponses, la page te dit juste ou faux 0/7

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) [3, 5, 8, 1], puis [1, 3, 5, 8]
  • b) Déjà trié : n1n - 1 comparaisons
  • c) Trié à l'envers : n(n1)2\frac{n(n-1)}{2}
  • d) Insertion au mieux, sélection en déplacements
  • e) Presque trié : insertion

a) Départ [5, 3, 8, 1]. Tour i = 1 : on insère 3, qui remonte devant 5, ce qui donne [3, 5, 8, 1]. Tour i = 2 : on insère 8, qui est déjà plus grand que 5, une seule comparaison et rien ne bouge, [3, 5, 8, 1]. Tour i = 3 : on insère 1, qui traverse 8, 5 et 3, ce qui donne [1, 3, 5, 8].

b) Si le tableau est déjà trié, la condition t[j] > valeur est fausse dès la première comparaison à chaque tour : la boucle while s'arrête immédiatement. Cela fait exactement une comparaison par tour, soit n1n-1 comparaisons au total, et aucun déplacement. Le coût au mieux est donc LINÉAIRE, O(n)O(n).

c) Si le tableau est trié à l'envers, chaque nouvel élément doit traverser toute la partie déjà triée : ii comparaisons au tour ii. Le total est 1+2++(n1)=n(n1)21+2+\cdots+(n-1)=\dfrac{n(n-1)}{2}, soit O(n2)O(n^{2}), exactement comme le tri par sélection.

d) Coût au mieux : insertion n1n-1, sélection n(n1)2\frac{n(n-1)}{2}, l'insertion gagne largement. Coût au pire : les deux valent n(n1)2\frac{n(n-1)}{2}, égalité. Déplacements : sélection nn échanges seulement, insertion jusqu'à n(n1)2\frac{n(n-1)}{2} décalages, la sélection gagne. Aucun des deux ne domine l'autre : le choix dépend des données et du coût d'un déplacement.

e) Le tri par insertion, sans hésiter. Un tableau presque trié est précisément son meilleur terrain : chaque élément déjà bien placé coûte une seule comparaison, et seuls les 5 intrus provoquent de vrais décalages. Le coût sera de l'ordre de 10 000 opérations. Le tri par sélection, lui, effectuerait ses 10000×9999250\frac{10000\times 9999}{2}\approx 50 millions de comparaisons, indifférent au fait que le travail soit presque fait. C'est le seul des deux capable de profiter de l'ordre déjà présent.

Exercice 4 : La recherche dichotomique

La dichotomie ne fonctionne que sur un tableau TRIÉ. On travaille sur tri = [1, 2, 3, 4, 7, 8, 9], qui compte 7 éléments d'indices 0 à 6.

Python
def dichotomie(t, x):
    g, d = 0, len(t) - 1
    while g <= d:
        m = (g + d) // 2
        if t[m] == x:
            return m
        elif t[m] < x:
            g = m + 1
        else:
            d = m - 1
    return -1
  • a) Déroulez la recherche de 8 : donnez à chaque tour les valeurs de g, d et m. Combien de tours ?
  • b) Même travail pour la recherche de 5, qui est absente.
  • c) Quel est le nombre maximal de tours pour un tableau de 7 éléments ? Et pour nn éléments ?
  • d) Comparez le nombre d'opérations de la recherche séquentielle et de la dichotomie pour n=1 000n=1\ 000 puis n=1 000 000n=1\ 000\ 000.
  • e) Donnez le variant qui prouve la terminaison de la boucle while.

Tape tes réponses, la page te dit juste ou faux 0/9

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Recherche de 8 : 2 tours
  • b) Recherche de 5 : 3 tours, renvoie 1-1
  • c) log2n+1\lfloor \log_{2} n \rfloor + 1 tours au pire
  • d) 10 et 20 tours contre 10310^{3} et 10610^{6}
  • e) Variant dg+1d - g + 1

a) Départ g = 0, d = 6, donc m = 3 et t[3] = 4, inférieur à 8 : on va à droite, g devient 4. Tour 2 : g = 4, d = 6, m = 5 et t[5] = 8 : trouvé, on renvoie 5. Il aura fallu 2 tours.

b) Départ g = 0, d = 6, m = 3, t[3] = 4 < 5 donc g = 4. Tour 2 : g = 4, d = 6, m = 5, t[5] = 8 > 5 donc d = 4. Tour 3 : g = 4, d = 4, m = 4, t[4] = 7 > 5 donc d = 3. Maintenant g = 4 > d = 3, la boucle s'arrête et la fonction renvoie -1. Trois tours ont suffi pour éliminer les 7 cases.

c) Chaque tour divise par deux le nombre de cases restantes : 7, puis 3, puis 1, puis 0. Il faut donc au plus 3 tours pour 7 éléments. En général le nombre de tours est de l'ordre de log2n\log_{2}n, plus précisément log2n+1\lfloor\log_{2}n\rfloor+1 au pire.

d) Pour n=1 000n=1\ 000 : la recherche séquentielle fait jusqu'à 1 000 comparaisons, la dichotomie environ 10, puisque 210=10242^{10}=1024. Pour n=1 000 000n=1\ 000\ 000 : séquentielle 1 000 000, dichotomie environ 20, puisque 2201062^{20}\approx 10^{6}. Multiplier la taille des données par mille n'ajoute que dix tours à la dichotomie. C'est la différence entre un coût linéaire et un coût logarithmique, et c'est ce qui rend la dichotomie irremplaçable sur de grandes données.

e) Le variant est dg+1d-g+1, le nombre de cases encore candidates. C'est un entier, il reste positif tant que la boucle tourne (la condition d'entrée est gdg\leq d), et chaque tour le fait décroître strictement : dans les deux branches on déplace gg ou dd strictement au-delà de mm, ce qui retire au moins la case du milieu. Un entier positif qui décroît strictement ne peut pas décroître indéfiniment : la boucle se termine.

Exercice 5 : Récursivité : factorielle et Fibonacci

Une fonction récursive s'appelle elle-même sur un cas plus petit, jusqu'à atteindre un cas de base qui, lui, ne relance pas d'appel.

Python
def factorielle(n):
    if n <= 1:
        return 1
    return n * factorielle(n - 1)

def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)
  • a) Identifiez le cas de base et le cas récursif de chaque fonction.
  • b) Déroulez factorielle(5) : écrivez la chaîne d'appels puis la remontée des résultats.
  • c) Que se passerait-il si l'on écrivait le cas de base de factorielle comme if n == 1 et qu'on appelait factorielle(0) ?
  • d) Donnez les dix premiers termes de la suite de Fibonacci calculés par cette fonction, en commençant à fibonacci(0).
  • e) Écrivez une version ITÉRATIVE de la factorielle, avec une boucle.

Tape tes réponses, la page te dit juste ou faux 0/4

a)
b)
c)
d)
Voir la correction

Réponses

  • a) Cas de base et cas récursif
  • b) factorielle(5) = 120 à la remontée
  • c) Cas de base n1n \leq 1
  • d) 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
  • e) Version itérative par accumulation

a) Pour factorielle : cas de base n1n\leq 1, qui renvoie 1 sans nouvel appel ; cas récursif n×n\times factorielle(n1)(n-1). Pour fibonacci : cas de base n<2n<2, qui renvoie nn ; cas récursif, la somme des deux appels précédents. Sans cas de base, la récursion ne s'arrêterait jamais et provoquerait une RecursionError.

b) La descente empile factorielle(5), qui appelle factorielle(4), puis (3), (2) et enfin (1). Ce dernier renvoie 1 sans rappel. La remontée donne alors 2×1=22\times 1=2, puis 3×2=63\times 2=6, puis 4×6=244\times 6=24, puis 5×24=1205\times 24=120. Le résultat est 120. Rien n'est calculé pendant la descente : tout le travail se fait à la remontée.

c) Avec if n == 1, l'appel factorielle(0) ne rencontrerait jamais son cas de base : il appellerait factorielle(-1), puis (-2), et ainsi de suite indéfiniment, jusqu'à l'erreur de dépassement de pile. C'est pourquoi on écrit n1n\leq 1 et non n=1n=1 : le cas de base doit couvrir TOUTES les entrées qui ne relancent pas la récursion. Mathématiquement 0!=10!=1, la version correcte donne donc aussi la bonne valeur.

d) fibonacci(0) à fibonacci(9) valent 0, 1, 1, 2, 3, 5, 8, 13, 21, 34. Et fibonacci(10) vaut 55.

e) La version itérative accumule le produit dans une variable.

Python
def factorielle_iterative(n):
    resultat = 1
    for i in range(2, n + 1):
        resultat = resultat * i
    return resultat

# factorielle_iterative(5) vaut 120, comme la version recursive

Partie B : Niveau examen (/50)

Exercice 6 : Classer des complexités et les justifier

Pour chaque fragment, donnez la complexité en fonction de nn et justifiez en comptant les tours de boucle. On ne demande pas le nombre exact d'opérations mais l'ordre de grandeur.

Python
# Fragment A
for i in range(n):
    print(i)

# Fragment B
for i in range(n):
    for j in range(n):
        print(i, j)

# Fragment C
for i in range(n):
    for j in range(i):
        print(i, j)

# Fragment D
i = n
while i > 1:
    i = i // 2

# Fragment E
for i in range(n):
    j = n
    while j > 1:
        j = j // 2
  • a) Donnez la complexité de chacun des cinq fragments A à E.
  • b) Pour le fragment C, donnez le nombre EXACT de tours de la boucle interne, puis expliquez pourquoi sa complexité est la même que celle de B alors qu'il fait deux fois moins de travail.
  • c) Rangez O(1)O(1), O(n2)O(n^{2}), O(logn)O(\log n), O(n)O(n), O(2n)O(2^{n}) et O(nlogn)O(n\log n) du moins coûteux au plus coûteux.
  • d) Un algorithme en O(n2)O(n^{2}) traite 1 000 éléments en 1 seconde. Estimez le temps pour 10 000 éléments, puis pour 100 000.
  • e) Un second algorithme, en O(nlogn)O(n\log n), traite les mêmes 1 000 éléments en 2 secondes, donc deux fois plus lentement. À partir de quelle taille devient-il préférable ? Répondez qualitativement puis vérifiez sur n=100 000n=100\ 000.

Tape tes réponses, la page te dit juste ou faux 0/11

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) A O(n)O(n), B et C O(n2)O(n^{2}), D O(logn)O(\log n), E O(nlogn)O(n \log n)
  • b) n(n1)2\frac{n(n-1)}{2} tours : même ordre que n2n^{2}
  • c) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)O(1) < O(\log n) < O(n) < O(n \log n) < O(n^{2}) < O(2^{n})
  • d) 100 s puis 10 00010\ 000 s
  • e) 333 s : trente fois plus rapide

a) A : O(n)O(n), une boucle simple. B : O(n2)O(n^{2}), deux boucles imbriquées de nn tours. C : O(n2)O(n^{2}) également. D : O(logn)O(\log n), la variable est divisée par deux à chaque tour. E : O(nlogn)O(n\log n), une boucle de nn tours contenant chacune une boucle logarithmique.

b) La boucle interne fait 0 tour pour i=0i=0, 1 tour pour i=1i=1, et ainsi de suite, soit au total 0+1++(n1)=n(n1)20+1+\cdots+(n-1)=\dfrac{n(n-1)}{2} tours. C'est bien environ la moitié des n2n^{2} tours du fragment B. Mais la notation OO ignore les facteurs constants : n2n2\dfrac{n^{2}-n}{2} et n2n^{2} ont le même ordre de grandeur, car leur rapport tend vers la constante 12\frac{1}{2}. Ce qu'on retient, c'est la façon dont le coût GRANDIT quand nn double, et dans les deux cas il est multiplié par quatre.

c) Du moins au plus coûteux : O(1)O(1), O(logn)O(\log n), O(n)O(n), O(nlogn)O(n\log n), O(n2)O(n^{2}), O(2n)O(2^{n}).

d) En O(n2)O(n^{2}), multiplier nn par 10 multiplie le temps par 102=10010^{2}=100. Pour 10 000 éléments : environ 100 secondes, soit près de 2 minutes. Pour 100 000 : environ 10 000 secondes, soit près de 3 heures. C'est le passage à l'échelle qui tue les algorithmes quadratiques, pas leur vitesse sur les petits jeux de données.

e) Le second est plus lent au départ mais grandit beaucoup moins vite : il finit forcément par gagner, et le point de bascule se situe pour un nn assez petit. Vérification à n=100 000n=100\ 000 : le quadratique met environ 10 000 s. Pour le second, le coût passe de 1000×log210001041000\times\log_{2}1000\approx 10^{4} à 100 000×log2100 0001,66×106100\ 000\times\log_{2}100\ 000\approx 1{,}66\times 10^{6}, soit environ 167 fois plus, donc 2×1673332\times 167\approx 333 secondes. Il est alors exactement trente fois plus rapide que le premier. Un mauvais algorithme rapide perd toujours contre un bon algorithme lent, dès que les données grossissent.

Exercice 7 : Invariant de boucle et preuve de correction

La fonction ci-dessous calcule la somme des éléments d'un tableau. On veut PROUVER qu'elle est correcte, pas seulement la tester.

Python
def somme(t):
    s = 0
    i = 0
    while i < len(t):
        s = s + t[i]
        i = i + 1
    return s
  • a) Proposez un invariant de boucle reliant s et i.
  • b) Vérifiez les trois points : l'invariant est vrai avant d'entrer dans la boucle, il est préservé par un tour, et il donne le résultat voulu à la sortie.
  • c) Donnez un variant et prouvez la terminaison.
  • d) On modifie le code en écrivant s = s + t[i] APRÈS i = i + 1. Que se passe-t-il ? Sur quelle entrée l'erreur se manifeste-t-elle en premier ?
  • e) Proposez un invariant pour la fonction de recherche du maximum de l'exercice 7 de la série sur les bases de Python.

Tape tes réponses, la page te dit juste ou faux 0/5

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Invariant : s=k<it[k]s = \sum_{k<i} t[k]
  • b) Initialisation, conservation, sortie
  • c) Variant len(t)i\text{len}(t) - i
  • d) Incrément avant lecture : IndexError
  • e) imax : premier indice du maximum

a) L'invariant est : au début de chaque tour, s contient la somme des i premiers éléments du tableau, c'est-à-dire s=k=0i1t[k]s=\sum_{k=0}^{i-1}t[k], avec 0ilen(t)0\leq i\leq \text{len}(t).

b) Initialisation : avant la boucle, i=0i=0 et s=0s=0 ; la somme des 0 premiers éléments est bien la somme vide, qui vaut 0. L'invariant est vrai. Conservation : supposons-le vrai en début de tour, donc s=k=0i1t[k]s=\sum_{k=0}^{i-1}t[k]. Le tour ajoute t[i]t[i] à s puis incrémente i. En fin de tour, s vaut k=0it[k]\sum_{k=0}^{i}t[k] et l'indice vaut i+1i+1 : c'est exactement l'invariant écrit pour la nouvelle valeur de i. Terminaison de la preuve : à la sortie, la condition de boucle est fausse, donc i=len(t)i=\text{len}(t), et l'invariant donne s=k=0len(t)1t[k]s=\sum_{k=0}^{\text{len}(t)-1}t[k], la somme de tous les éléments. C'est bien ce qu'on voulait démontrer.

c) Le variant est len(t)i\text{len}(t)-i. C'est un entier, positif tant que la boucle tourne puisque la condition est i<len(t)i<\text{len}(t), et chaque tour le diminue exactement de 1 car i augmente de 1 et len(t) ne change pas. Une suite d'entiers positifs strictement décroissante est finie : la boucle se termine.

d) En incrémentant i avant de l'utiliser, on saute t[0] et on tente de lire t[len(t)], qui n'existe pas : le programme lève une IndexError. L'erreur se manifeste dès le premier tableau non vide, y compris un tableau à un seul élément : i passe à 1 puis on lit t[1] hors bornes. C'est le décalage d'indice classique, et c'est précisément l'invariant qui permet de le repérer sans exécuter le code, car il n'est plus préservé par le tour.

e) L'invariant est : au début de chaque tour, imax est l'indice du plus grand élément parmi les i premiers, et c'est le plus petit indice où ce maximum est atteint. Initialisation avec imax = 0 et i = 1 : le maximum du seul élément t[0] est bien atteint en 0. Conservation : le tour remplace imax seulement si t[i] est STRICTEMENT plus grand, ce qui préserve à la fois la maximalité et le choix de la première occurrence. À la sortie, i vaut len(t) et l'invariant porte alors sur le tableau entier.

Exercice 8 : Le coût caché de la récursivité naïve

La version récursive de Fibonacci vue à l'exercice 5 est élégante et correcte. Elle est aussi inutilisable au-delà de quelques dizaines de termes, et cet exercice explique pourquoi.

  • a) Dessinez l'arbre des appels de fibonacci(5). Combien de fois fibonacci(2) est-il appelé ?
  • b) Pour fibonacci(10), le nombre total d'appels est 177. Comparez-le à la valeur calculée, qui n'est que 55. Que remarquez-vous ?
  • c) Expliquez pourquoi le coût de cette version croît de façon exponentielle.
  • d) Écrivez une version ITÉRATIVE qui calcule fibonacci(n) en une seule boucle. Quelle est sa complexité ?
  • e) Une autre solution consiste à mémoriser les résultats déjà calculés. Expliquez le principe et donnez la complexité obtenue.

Tape tes réponses, la page te dit juste ou faux 0/7

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) fibonacci(2) appelé 3 fois
  • b) 177 appels pour 55
  • c) Coût en φn\varphi^{n} : exponentiel
  • d) Itératif en O(n)O(n)
  • e) Mémoïsation : O(n)O(n)

a) L'appel fibonacci(5) déclenche fibonacci(4) et fibonacci(3) ; fibonacci(4) déclenche à son tour fibonacci(3) et fibonacci(2), et ainsi de suite. En développant tout l'arbre, fibonacci(2) est appelé 3 fois. Le point important est qu'il est recalculé intégralement à chaque fois, sans que rien ne soit conservé d'un appel à l'autre.

b) Il faut 177 appels de fonction pour produire le nombre 55. Autrement dit, on effectue plus de trois fois plus de travail que la valeur du résultat lui-même. Presque tout ce travail est du recalcul pur : dans cet arbre, fibonacci(3) est évalué 21 fois, toujours pour obtenir 2.

c) Chaque appel non terminal en engendre deux, donc le nombre d'appels double approximativement quand nn augmente de 1. On obtient un arbre binaire de profondeur nn, dont le nombre de nœuds croît comme 2n2^{n} (plus précisément comme φn\varphi^{n}, avec φ\varphi le nombre d'or). Passer de n=40n=40 à n=50n=50 multiplie le temps par environ φ10123\varphi^{10} \approx 123 : c'est ce qui rend cette version inutilisable, alors même qu'elle est parfaitement correcte. Correction et efficacité sont deux propriétés indépendantes.

d) Il suffit de garder les deux derniers termes et d'avancer. La boucle fait nn tours et chaque tour coûte une addition : la complexité est LINÉAIRE, O(n)O(n). On passe de 2n2^{n} à nn, ce qui rend fibonacci(1000) instantané.

e) La mémoïsation consiste à ranger chaque résultat dans un dictionnaire dès qu'il est calculé, et à consulter ce dictionnaire avant de relancer un appel. Chaque valeur fibonacci(k) n'est alors calculée qu'une seule fois, pour kk de 0 à nn : la complexité tombe à O(n)O(n) également, au prix d'un espace mémoire O(n)O(n). C'est la même idée que la version itérative, obtenue sans renoncer à l'écriture récursive.

Python
def fibonacci_iteratif(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a                      # O(n)

def fibonacci_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n < 2:
        return n
    if n not in memo:
        memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
    return memo[n]                # O(n) aussi

Exercice 9 : Algorithmes gloutons et le contre-exemple qui les met en défaut

Un algorithme glouton construit sa solution par choix successifs, en prenant à chaque étape ce qui paraît le meilleur sur le moment, sans jamais remettre en cause un choix déjà fait. On étudie le rendu de monnaie.

Python
def rendu(somme, pieces):
    resultat = []
    for p in pieces:          # pieces triees du plus grand au plus petit
        while somme >= p:
            somme = somme - p
            resultat.append(p)
    return resultat
  • a) Avec le système européen [200, 100, 50, 20, 10, 5, 2, 1] en centimes, déroulez rendu(178, pieces). Combien de pièces sont rendues ?
  • b) Pourquoi est-il indispensable que la liste des pièces soit triée par ordre décroissant ?
  • c) On invente maintenant un système à trois valeurs : [4, 3, 1]. Que rend l'algorithme glouton pour la somme 6 ? Combien de pièces ?
  • d) Trouvez la solution optimale pour 6 avec ce système. Conclusion sur l'algorithme glouton ?
  • e) Donnez un avantage réel des algorithmes gloutons qui justifie qu'on les utilise malgré ce défaut.

Tape tes réponses, la page te dit juste ou faux 0/6

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) 178 : 6 pièces
  • b) Tri décroissant indispensable
  • c) [4, 1, 1] : 3 pièces
  • d) [3, 3] : le glouton n'est pas optimal
  • e) Glouton rapide et simple

a) On prend d'abord la plus grosse pièce possible : 100 (reste 78), puis 50 (reste 28), puis 20 (reste 8), puis 5 (reste 3), puis 2 (reste 1), puis 1 (reste 0). La liste rendue est [100, 50, 20, 5, 2, 1], soit 6 pièces. Aucune pièce de 200 n'est prise puisque 200 dépasse 178.

b) Parce que le principe même du glouton est de prendre le meilleur choix local, et « meilleur » signifie ici « la plus grosse pièce qui rentre ». Si la liste n'était pas triée, la boucle épuiserait d'abord des petites pièces et rendrait une solution médiocre, par exemple 178 pièces de 1 centime en commençant par 1. Le tri fait partie de l'algorithme, pas de la préparation des données.

c) Avec [4, 3, 1] et la somme 6 : le glouton prend 4 (reste 2), puis ne peut pas prendre 3, prend 1 (reste 1), puis 1 (reste 0). Il rend [4, 1, 1], soit 3 pièces.

d) La solution optimale est [3, 3], soit 2 pièces seulement. L'algorithme glouton n'est donc PAS optimal en général : en prenant la pièce de 4 il s'est interdit la bonne combinaison, et comme il ne revient jamais en arrière, il ne peut plus se rattraper. Il se trouve que le système européen est, lui, canonique, ce qui rend le glouton optimal dans ce cas particulier, mais c'est une propriété du système de pièces, pas de l'algorithme. Pour un système quelconque, il faut la programmation dynamique.

e) Sa rapidité et sa simplicité. Le glouton ne teste qu'une seule combinaison, celle qu'il construit, là où une recherche exhaustive en examinerait un nombre exponentiel. Quand la solution optimale n'est pas indispensable, ou quand on sait démontrer que le glouton est optimal sur le problème traité, c'est de loin la meilleure approche. Beaucoup d'algorithmes classiques sont gloutons et prouvés optimaux, comme celui de Dijkstra pour les plus courts chemins.

Exercice 10 : Problème : choisir le bon algorithme selon le contexte

Une bibliothèque gère un catalogue de 500 000 ouvrages. Pour chaque situation ci-dessous, indiquez l'algorithme ou la structure à utiliser, donnez sa complexité, et justifiez. Il n'y a pas de bonne réponse unique : c'est la justification qui est notée.

  • a) Le catalogue est trié par numéro ISBN. Un lecteur cherche un ouvrage précis par son ISBN. Que faites-vous et à quel coût ?
  • b) Le catalogue n'est PAS trié et l'on doit y chercher un seul ouvrage, une seule fois. Vaut-il mieux trier d'abord puis faire une dichotomie, ou faire directement une recherche séquentielle ? Justifiez par les coûts.
  • c) Même catalogue non trié, mais on prévoit 10 000 recherches. La réponse change-t-elle ? Posez l'inégalité qui décide.
  • d) On veut afficher les 10 ouvrages les plus empruntés. Faut-il trier tout le catalogue ? Proposez une solution moins coûteuse et donnez sa complexité.
  • e) On veut savoir si deux ouvrages du catalogue ont exactement le même titre. Proposez une méthode naïve, donnez son coût, puis une méthode plus efficace.

Tape tes réponses, la page te dit juste ou faux 0/7

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Dichotomie : 19 comparaisons
  • b) Une recherche : séquentielle
  • c) 10 00010\ 000 recherches : trier d'abord
  • d) Les 10 meilleurs en un parcours
  • e) Doublons : trier, ou un ensemble

a) Recherche dichotomique, puisque le tableau est trié : environ log2(500 000)19\log_{2}(500\ 000)\approx 19 comparaisons. C'est immédiat, même sur un demi-million d'entrées. Faire une recherche séquentielle ici serait gaspiller l'information précieuse que constitue le tri.

b) Pour UNE seule recherche, la recherche séquentielle gagne largement. Elle coûte au pire n=500 000n=500\ 000 comparaisons. Trier d'abord coûterait de l'ordre de nlog2n500 000×199,5n\log_{2}n\approx 500\ 000\times 19\approx 9{,}5 millions d'opérations, soit près de vingt fois plus, avant même d'avoir commencé à chercher. On ne trie jamais pour une seule requête.

c) Oui, la réponse s'inverse. Il faut comparer kk recherches séquentielles, soit k×nk\times n, au coût du tri suivi de kk dichotomies, soit nlog2n+klog2nn\log_{2}n+k\log_{2}n. Avec k=10 000k=10\ 000 : à gauche 10 000×500 000=510\ 000\times 500\ 000=5 milliards d'opérations ; à droite environ 9,59{,}5 millions pour le tri plus 10 000×19190 00010\ 000\times 19\approx 190\ 000, soit moins de 10 millions. Le tri est amorti dès quelques dizaines de recherches : c'est tout l'intérêt d'organiser les données une fois pour les interroger souvent.

d) Non, trier les 500 000 ouvrages pour n'en garder que 10 est un gaspillage. Il suffit de parcourir le catalogue une seule fois en maintenant les 10 meilleurs vus jusqu'ici : pour chaque ouvrage, on le compare au plus faible des 10 retenus et on l'insère si nécessaire. Le coût est O(n×10)O(n\times 10), donc linéaire en nn, contre O(nlogn)O(n\log n) pour un tri complet. Sur un demi-million d'entrées, cela fait environ 10n=510n=5 millions d'opérations contre nlog2n9,5n\log_{2}n\approx 9{,}5 millions, soit deux fois moins dans le pire des cas. En pratique l'écart est bien plus grand : la quasi-totalité des ouvrages est éliminée dès la première comparaison avec le dixième meilleur, si bien que le coût réel avoisine nn et non 10n10n.

e) Méthode naïve : comparer chaque titre à tous les autres, soit n(n1)2\dfrac{n(n-1)}{2} comparaisons, environ 1,25×10111{,}25\times 10^{11} ici, ce qui est hors de portée. Méthode efficace : trier les titres, puis parcourir la liste triée une fois en comparant chaque titre à son voisin immédiat, car deux titres identiques sont forcément côte à côte après tri. Le coût tombe à O(nlogn)O(n\log n) pour le tri plus O(n)O(n) pour le parcours. Encore mieux, on peut utiliser un dictionnaire ou un ensemble et détecter un doublon en un seul parcours, en O(n)O(n) en moyenne.

Partie C : les classiques (/50)

Exercice 11 : Rechercher un motif dans un texte

La recherche d'un mot dans un document, d'un gène dans un génome ou d'une signature dans un fichier se ramène au même problème : trouver toutes les positions où un MOTIF de longueur mm apparaît dans un TEXTE de longueur nn. L'algorithme naïf essaie chaque position l'une après l'autre.

Python
def recherche_naive(texte, motif):
    n, m = len(texte), len(motif)
    positions = []
    for i in range(n - m + 1):
        j = 0
        while j < m and texte[i + j] == motif[j]:
            j = j + 1
        if j == m:
            positions.append(i)
    return positions
  • a) Que renvoie recherche_naive('abracadabra', 'abra') ? Justifiez.
  • b) Combien de positions i la boucle for teste-t-elle pour cet appel ? Pourquoi la borne est-elle nm+1n - m + 1 et non nn ?
  • c) Comptez le nombre total de comparaisons de caractères texte[i + j] == motif[j] effectuées par cet appel.
  • d) On cherche le motif formé de neuf a suivis d'un b dans un texte de 1 0001\ 000 lettres a. Combien de comparaisons sont effectuées ? Donnez la complexité dans le pire cas, en fonction de nn et mm.
  • e) Combien de comparaisons faut-il pour chercher 'xyz' dans 'abracadabra' ? Pourquoi l'algorithme naïf est-il souvent bien plus rapide en pratique que son pire cas ?

Tape tes réponses, la page te dit juste ou faux 0/6

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Occurrences en 0 et 7
  • b) nm+1=8n - m + 1 = 8 positions
  • c) 16 comparaisons
  • d) 9 9109\ 910 comparaisons : O(nm)O(n\,m) au pire
  • e) En pratique, proche de nn

a) Le motif apparaît en position 0, abra au début, et en position 7, abra à la fin : la fonction renvoie [0, 7]. Les deux occurrences ne se chevauchent pas ici, mais l'algorithme trouverait aussi des occurrences qui se chevauchent, puisqu'il teste chaque position indépendamment des autres.

b) n=11n = 11 et m=4m = 4, donc i prend les valeurs 0 à 7 : 8 positions. Au-delà de nmn - m, le motif dépasserait la fin du texte et la lecture de texte[i + j] sortirait des bornes ; la borne nm+1n - m + 1 du range garantit que la dernière position testée place la dernière lettre du motif sur la dernière lettre du texte.

c) Position 0 : abra est trouvé, 4 comparaisons. Position 1 : b contre a, 1 comparaison, de même aux positions 2, 4 et 6, où les lettres r, c et d diffèrent de a. Position 3 : a correspond, puis c contre b échoue, 2 comparaisons ; même chose en position 5 avec d. Position 7 : abra est trouvé, 4 comparaisons. Total : 4+1+1+2+1+2+1+4=164 + 1 + 1 + 2 + 1 + 2 + 1 + 4 = 16 comparaisons.

d) À chaque position, les neuf a du motif correspondent et seul le b échoue : 10 comparaisons par position, pour 1 00010+1=9911\ 000 - 10 + 1 = 991 positions, soit 9 9109\ 910 comparaisons. Dans le pire cas, l'algorithme fait mm comparaisons à chacune des nm+1n - m + 1 positions : sa complexité est O(n×m)O(n \times m).

e) Pour 'xyz', il y a 113+1=911 - 3 + 1 = 9 positions, et la première comparaison échoue à chacune : 9 comparaisons. Dans un texte ordinaire, la première lettre du motif diffère presque toujours de la lettre du texte, si bien que l'on fait environ une comparaison par position et un coût proche de nn. Le pire cas exige un texte et un motif très répétitifs, fréquents dans un génome, rares dans un roman. L'exercice 14 montre comment faire mieux que nn comparaisons.

Exercice 12 : Les k plus proches voisins

On veut prédire l'espèce d'une fleur à partir de la longueur et de la largeur de ses pétales. L'algorithme des kk plus proches voisins cherche, dans une table d'exemples dont on connaît l'espèce, les kk fleurs les plus proches de la nouvelle, et prédit l'espèce majoritaire parmi elles.

La nouvelle fleur Q a des pétales de 4,84{,}8 cm sur 1,61{,}6 cm. On utilise la distance euclidienne (xx)2+(yy)2\sqrt{(x - x')^{2} + (y - y')^{2}}.

FleurLongueur du pétale (cm)Largeur du pétale (cm)Espèce
A1,40,2setosa
B1,50,3setosa
C4,51,5versicolor
D4,01,2versicolor
E5,52,1virginica
F5,01,8virginica
  • a) Calculez la distance de Q à chacune des six fleurs, au centième.
  • b) Classez les fleurs par distance croissante. Quelle espèce l'algorithme prédit-il pour k=1k = 1 ? Pour k=3k = 3 ?
  • c) Quelle prédiction obtient-on pour k=5k = 5 ? Quel problème apparaît, et comment le traiter ?
  • d) Quel est le coût d'une prédiction pour une table de nn exemples ? Pourquoi dit-on que cet algorithme n'a pas de phase d'apprentissage, et quel en est le prix ?
  • e) On ajoute une troisième caractéristique, la masse de la fleur en milligrammes, de l'ordre de plusieurs centaines. Quel défaut cela introduit-il dans le calcul des distances, et quel remède appliquer ?

Tape tes réponses, la page te dit juste ou faux 0/10

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Distances 3,683{,}68, 3,553{,}55, 0,320{,}32, 0,890{,}89, 0,860{,}86, 0,280{,}28
  • b) k=1k = 1 et k=3k = 3 : virginica
  • c) k=5k = 5 : égalité, pondérer par la distance
  • d) Pas d'apprentissage, prédiction en O(n)O(n) au moins
  • e) Normaliser les caractéristiques

a) Distance à A : 3,42+1,42=13,523,68\sqrt{3{,}4^{2} + 1{,}4^{2}} = \sqrt{13{,}52} \approx 3{,}68. À B : 3,32+1,32=12,583,55\sqrt{3{,}3^{2} + 1{,}3^{2}} = \sqrt{12{,}58} \approx 3{,}55. À C : 0,32+0,12=0,100,32\sqrt{0{,}3^{2} + 0{,}1^{2}} = \sqrt{0{,}10} \approx 0{,}32. À D : 0,82+0,42=0,800,89\sqrt{0{,}8^{2} + 0{,}4^{2}} = \sqrt{0{,}80} \approx 0{,}89. À E : 0,72+0,52=0,740,86\sqrt{0{,}7^{2} + 0{,}5^{2}} = \sqrt{0{,}74} \approx 0{,}86. À F : 0,22+0,22=0,080,28\sqrt{0{,}2^{2} + 0{,}2^{2}} = \sqrt{0{,}08} \approx 0{,}28.

b) Ordre croissant : F 0,280{,}28, C 0,320{,}32, E 0,860{,}86, D 0,890{,}89, B 3,553{,}55, A 3,683{,}68. Pour k=1k = 1, le plus proche voisin est F : prédiction virginica. Pour k=3k = 3, les voisins sont F, C et E, soit deux virginica contre une versicolor : prédiction virginica.

c) Pour k=5k = 5, les voisins sont F, C, E, D et B : deux virginica, deux versicolor et une setosa. Il y a ÉGALITÉ, et l'algorithme ne sait pas conclure. Choisir kk impair ne suffit pas quand il y a plus de deux classes. Remèdes : pondérer chaque voisin par l'inverse de sa distance, ce qui donne 10,28+10,864,7\frac{1}{0{,}28} + \frac{1}{0{,}86} \approx 4{,}7 pour virginica contre 10,32+10,894,3\frac{1}{0{,}32} + \frac{1}{0{,}89} \approx 4{,}3 pour versicolor, donc virginica ; ou départager par le plus proche des voisins à égalité ; ou revenir à une valeur de kk plus petite.

d) Il faut calculer les nn distances, puis extraire les kk plus petites : O(n)O(n) pour les distances, et O(nlogn)O(n \log n) si l'on trie tout, O(nk)O(n\,k) si l'on maintient seulement les kk meilleures. L'algorithme n'a pas de phase d'apprentissage parce qu'il ne construit aucun modèle : il conserve la table entière et fait tout le travail au moment de la prédiction. Le prix est double : la table doit rester en mémoire, et chaque prédiction coûte un parcours complet, ce qui devient lourd pour des millions d'exemples et des milliers de prédictions.

e) Les écarts de masse, de l'ordre de dizaines ou de centaines, écraseraient les écarts de pétales, de l'ordre du centimètre : la distance ne dépendrait presque plus que de la masse, et la forme des pétales serait ignorée, sans que rien ne le signale. Le remède est de NORMALISER chaque caractéristique avant de calculer les distances, par exemple en la ramenant entre 0 et 1 à l'aide de son minimum et de son maximum dans la table, afin que chacune pèse d'un poids comparable.

Exercice 13 : L'exponentiation rapide

Calculer xnx^{n} en multipliant nn fois par xx coûte nn multiplications. En remarquant que xn=(xn/2)2x^{n} = (x^{n/2})^{2} quand nn est pair, on fait beaucoup mieux. Cette idée sert en cryptographie, où l'on élève des nombres de centaines de chiffres à des puissances de centaines de chiffres.

Python
def puissance(x, n):
    if n == 0:
        return 1
    if n % 2 == 0:
        y = puissance(x, n // 2)
        return y * y
    return x * puissance(x, n - 1)
  • a) Déroulez puissance(2, 10) : donnez la suite des valeurs de n dans les appels successifs, puis le résultat.
  • b) Comptez les multiplications effectuées par cet appel. Combien en ferait la méthode naïve, qui part de 1 et multiplie nn fois par xx ?
  • c) Donnez la suite des valeurs de n pour puissance(x, 1000) et le nombre de multiplications. Montrez que ce nombre est de l'ordre de log2n\log_{2} n.
  • d) Un élève remplace les deux lignes du cas pair par return puissance(x, n // 2) * puissance(x, n // 2). Le résultat est-il correct ? Comptez les multiplications pour n=8n = 8 avec les deux versions.
  • e) Combien de chiffres le nombre 210002^{1000} possède-t-il ? On utilisera log1020,30103\log_{10} 2 \approx 0{,}30103.

Tape tes réponses, la page te dit juste ou faux 0/9

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Appels 10, 5, 4, 2, 1, 0 : 1 0241\ 024
  • b) 5 multiplications contre 10
  • c) 15 multiplications pour n=1 000n = 1\ 000
  • d) Appel dédoublé : 15 au lieu de 4
  • e) 210002^{1000} a 302 chiffres

a) Les appels successifs portent sur n=10n = 10, pair, puis 5, impair, puis 4, pair, puis 2, pair, puis 1, impair, puis 0, cas de base. En remontant : 20=12^{0} = 1, 21=2×1=22^{1} = 2 \times 1 = 2, 22=2×2=42^{2} = 2 \times 2 = 4, 24=4×4=162^{4} = 4 \times 4 = 16, 25=2×16=322^{5} = 2 \times 16 = 32, 210=32×32=1 0242^{10} = 32 \times 32 = 1\ 024.

b) Chaque appel autre que le cas de base effectue exactement une multiplication, y * y ou x * puissance(x, n - 1) : pour nn valant 10, 5, 4, 2 et 1, cela fait 5 multiplications. La méthode naïve en ferait 10. Le gain paraît modeste ici ; il devient spectaculaire dès que nn grandit.

c) Suite : 1000, 500, 250, 125, 124, 62, 31, 30, 15, 14, 7, 6, 3, 2, 1, 0, soit 15 multiplications au lieu de 1 0001\ 000. Chaque appel pair divise nn par deux ; un appel impair le rend pair et est donc suivi d'une division. Il y a donc au plus deux appels par division par deux, et le nombre de divisions nécessaires pour ramener nn à 1 vaut log2n\lfloor \log_{2} n \rfloor : au total au plus 2log2n2 \log_{2} n multiplications, soit O(logn)O(\log n). Pour n=1 000n = 1\ 000, log21 000=9\lfloor \log_{2} 1\ 000 \rfloor = 9 divisions et 6 appels impairs donnent bien 15.

d) Le résultat est correct, mais le calcul de xn/2x^{n/2} est fait DEUX fois au lieu d'une. Pour n=8n = 8, la version rapide fait les appels 8, 4, 2, 1, soit 4 multiplications. La version modifiée vérifie T(8)=2T(4)+1T(8) = 2\,T(4) + 1, T(4)=2T(2)+1T(4) = 2\,T(2) + 1, T(2)=2T(1)+1T(2) = 2\,T(1) + 1 et T(1)=1T(1) = 1, d'où T(2)=3T(2) = 3, T(4)=7T(4) = 7 et T(8)=15T(8) = 15 : plus que les 8 multiplications de la méthode naïve. Dédoubler l'appel annule tout le bénéfice : c'est le même piège que la récursivité naïve de Fibonacci, où l'on recalcule ce qu'on vient de calculer.

e) Le nombre de chiffres d'un entier N1N \geq 1 vaut log10N+1\lfloor \log_{10} N \rfloor + 1. Ici log1021000=1 000×0,30103=301,03\log_{10} 2^{1000} = 1\ 000 \times 0{,}30103 = 301{,}03, donc 210002^{1000} possède 302 chiffres. L'exponentiation rapide le calcule en 15 multiplications d'entiers, là où la méthode naïve en demanderait mille.

Exercice 14 : La recherche textuelle de Horspool

L'algorithme de Horspool, version simplifiée de celui de Boyer et Moore, compare le motif au texte en partant de la DERNIÈRE lettre du motif, et, en cas d'échec, décale le motif de plusieurs cases d'un coup. Le décalage dépend seulement de la lettre du texte alignée avec la dernière lettre du motif.

On cherche le motif 'chat' dans le texte 'un chat dort sur le chat noir', de 29 caractères, espaces compris.

Python
def horspool(texte, motif):
    n, m = len(texte), len(motif)
    decalage = {}
    for k in range(m - 1):
        decalage[motif[k]] = m - 1 - k
    positions = []
    i = 0
    while i <= n - m:
        j = m - 1
        while j >= 0 and texte[i + j] == motif[j]:
            j = j - 1
        if j < 0:
            positions.append(i)
        i = i + decalage.get(texte[i + m - 1], m)
    return positions
  • a) Construisez la table des décalages du motif 'chat'. Quel décalage applique-t-on pour une lettre qui n'y figure pas ?
  • b) Faites la trace de horspool : pour chaque position i testée, donnez la lettre du texte alignée avec la fin du motif, le nombre de comparaisons et le décalage appliqué.
  • c) Quelles positions sont renvoyées ? Combien de comparaisons au total ? L'algorithme naïf de l'exercice 11 en fait 32 sur le même exemple : commentez.
  • d) Pourquoi la dernière lettre du motif est-elle exclue de la table ? Justifiez sur l'exemple d'un motif comme 'toit'.
  • e) On cherche un motif de 10 lettres toutes différentes, dont aucune n'apparaît dans un texte de 1 000 0001\ 000\ 000 de caractères. Combien de comparaisons Horspool fait-il, et combien l'algorithme naïf ?

Tape tes réponses, la page te dit juste ou faux 0/13

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Décalages c 3, h 2, a 1, autres 4
  • b) 8 positions testées
  • c) Positions 3 et 20, 14 comparaisons contre 32
  • d) Dernière lettre exclue : pas de décalage nul
  • e) 100 000100\ 000 comparaisons, dix fois moins

a) Pour chaque lettre du motif sauf la dernière, le décalage est la distance entre sa dernière occurrence et la fin du motif : c vaut 410=34 - 1 - 0 = 3, h vaut 411=24 - 1 - 1 = 2, a vaut 412=14 - 1 - 2 = 1. Une lettre absente de la table, t compris, donne un décalage égal à la longueur du motif, 4 : aucune position intermédiaire ne peut aligner cette lettre avec une lettre égale du motif.

b) i = 0 : la lettre alignée est c, qui diffère de t, 1 comparaison, décalage 3. i = 3 : t correspond, puis a, h et c, 4 comparaisons, occurrence trouvée, décalage de t, 4. i = 7 : r, 1 comparaison, décalage 4. i = 11 : u, 1 comparaison, décalage 4. i = 15 : e, 1 comparaison, décalage 4. i = 19 : a, 1 comparaison, décalage 1. i = 20 : t, a, h, c correspondent, 4 comparaisons, occurrence trouvée, décalage 4. i = 24 : i, 1 comparaison, décalage 4, et 28>29428 > 29 - 4 arrête la boucle.

c) Positions renvoyées : 3 et 20. Total : 1+4+1+1+1+1+4+1=141 + 4 + 1 + 1 + 1 + 1 + 4 + 1 = 14 comparaisons, contre 32 pour l'algorithme naïf, qui teste les 26 positions. Horspool n'a examiné que 8 positions : la plupart des lettres du texte ne figurent pas dans le motif et font sauter 4 cases d'un coup. Plus le motif est long, plus les sauts sont grands, ce qui fait de cet algorithme, paradoxalement, un algorithme d'autant plus rapide que le motif cherché est long.

d) Le décalage doit amener sous la lettre lue du texte une occurrence PLUS À GAUCHE de cette lettre dans le motif. Si la dernière lettre était incluse, elle recevrait le décalage 0 et l'algorithme ne progresserait plus. Pour 'toit', la table donne t : 3, o : 2, i : 1 ; le t final est exclu, et le décalage de t vient du t initial, 3. C'est exactement ce qu'il faut : après un échec sur une fenêtre terminée par t, on aligne le t initial du motif sous ce t du texte.

e) À chaque position, la lettre alignée avec la fin du motif ne figure pas dans le motif : une comparaison, puis un décalage de 10. On teste donc environ 1 000 00010=100 000\frac{1\ 000\ 000}{10} = 100\ 000 positions, avec 100 000100\ 000 comparaisons. L'algorithme naïf teste les 999 991999\ 991 positions, avec au moins une comparaison chacune, soit environ un million : dix fois plus. Le gain croît avec la longueur du motif.

Exercice 15 : Un glouton optimal : choisir le plus d'activités

Une salle ne peut accueillir qu'une activité à la fois. Onze demandes de réservation sont données par leurs heures de début et de fin. Deux activités sont compatibles si l'une se termine avant que l'autre commence, une fin à 5 h et un début à 5 h étant compatibles. On veut accepter le plus grand nombre possible d'activités.

ActivitéABCDEFGHIJK
Début (h)130535688212
Fin (h)4567991011121416
  • a) Stratégie 1 : parmi les activités compatibles avec celles déjà choisies, prendre celle qui COMMENCE le plus tôt. Appliquez-la. Combien d'activités obtient-on ?
  • b) Stratégie 2 : prendre celle qui SE TERMINE le plus tôt. Appliquez-la. Combien d'activités obtient-on ?
  • c) Stratégie 3 : prendre la plus COURTE. Montrez qu'elle n'est pas optimale sur l'exemple de trois activités de 0 h à 5 h, de 4 h à 6 h et de 5 h à 10 h.
  • d) On admet que la stratégie 2 est toujours optimale. Expliquez l'argument d'échange qui le prouve : pourquoi peut-on toujours remplacer la première activité d'une solution optimale par celle qui se termine le plus tôt ?
  • e) Quel est le coût de la stratégie 2 pour nn activités ? Comparez au nombre de sous-ensembles qu'une recherche exhaustive devrait examiner pour n=50n = 50.

Tape tes réponses, la page te dit juste ou faux 0/8

a)
b)
c)
d)
e)
Voir la correction

Réponses

  • a) Début au plus tôt : 3 activités
  • b) Fin au plus tôt : A, D, H, K
  • c) Plus courte d'abord : 1 contre 2
  • d) Argument d'échange
  • e) O(nlogn)O(n \log n) contre 2502^{50} sous-ensembles

a) L'activité qui commence le plus tôt est C, de 0 h à 6 h. Parmi celles qui commencent à 6 h ou après, la première est G, de 6 h à 10 h. Parmi celles qui commencent à 10 h ou après, il ne reste que K, de 12 h à 16 h. On obtient C, G et K : 3 activités. Commencer tôt ne dit rien de la place qu'on occupe : C bloque à elle seule six heures.

b) L'activité qui se termine le plus tôt est A, de 1 h à 4 h. Les activités compatibles qui se terminent ensuite le plus tôt sont D, de 5 h à 7 h, puis H, de 8 h à 11 h, puis K, de 12 h à 16 h ; toutes les autres chevauchent une activité déjà choisie. On obtient A, D, H et K : 4 activités, soit une de plus que la stratégie 1.

c) La plus courte est celle de 4 h à 6 h, qui dure 2 heures. Une fois choisie, elle chevauche les deux autres, qui sont éliminées : la stratégie 3 accepte 1 activité. Or les activités de 0 h à 5 h et de 5 h à 10 h sont compatibles entre elles : l'optimum vaut 2. Un seul contre-exemple suffit à réfuter une stratégie gloutonne.

d) Soit une solution optimale, rangée par heures de fin, et aa l'activité qui se termine le plus tôt de toutes. La première activité oo de la solution optimale se termine au plus tôt comme aa, par définition de aa. Remplacer oo par aa ne crée aucun conflit : aa se termine avant ou en même temps que oo, donc avant le début de la deuxième activité de la solution. On obtient une solution de même taille, donc encore optimale, qui commence par le choix glouton. En répétant l'argument sur les activités restantes, on montre que toute la solution gloutonne est optimale.

e) Il faut trier les activités par heure de fin, en O(nlogn)O(n \log n), puis les parcourir une fois en ne retenant que celles qui commencent après la fin de la dernière choisie, en O(n)O(n) : coût total O(nlogn)O(n \log n). Une recherche exhaustive examinerait les 2501,1×10152^{50} \approx 1{,}1 \times 10^{15} sous-ensembles de 50 activités. À un milliard de sous-ensembles par seconde, il faudrait près de deux semaines, là où le glouton répond instantanément.

Chapitre suivant Structures de données : piles, files, arbres et graphes

Ce chapitre resservira dans

Les chapitres qui le réclament en amont, plus tard dans l'année ou dans les années suivantes.

Voir aussi

Vous cherchez un tuteur en NSI à Montréal ?

Contactez-moi pour une première séance. Bachelier en informatique de McGill et maîtrise en informatique appliquée de Concordia, je travaille l'algorithmique sur la preuve et le coût, pas seulement sur le code qui tourne.

Site par Studio Squalli