• Aucun résultat trouvé

Tris en temps linéaire

Dans le document Informatique pour tous deuxième année (Page 28-59)

D'après la section précédente, annoncer des tris en temps linéaire paraît absurde, mais en fait l'idée ici est de ne pas utiliser de comparaisons en protant de propriétés que la liste à trier est supposée avoir.

Le tri par dénombrement ou tri comptage (counting sort) permet de trier une liste dont les éléments proviennent d'un ensemble ni, et si possible de taille très petite devant la taille de la liste.

Par exemple, on voudrait trier un million de caractères de la table ASCII, ce qui s'assimile à trier un million de nombres entre 0et 127.

6. On suppose les éléments diérents deux à deux pour faciliter les choses, mais le théorème reste vrai sans cette hypothèse, car dans l'idée se rendre compte d'une égalité nécessite une comparaison, donc on agit comme si les éléments étaient uniques.

3.6. TRIS EN TEMPS LINÉAIRE 29 Pour ce faire, on crée un tableau de taille 128 dont les éléments sont le nombre d'occurrences de chacun des nombres de 0à127, que l'on va compter en parcourant la liste. Une fois le parcours terminé, on constitue directement la version triée de la liste. La complexité est donc un O(n).

Le tri par base (radix sort) est un tri coûteux en espace mais dont la complexité en temps est unO(kn), oùk est la taille (en tant que nombres ou chaîne de caractères) des éléments de la liste.

Ceci correspond à un O(nlogn) pour les nombres de 1 à n, mais on peut imaginer des cas où le nombre k est plus intéressant. Historiquement, ce tri a été introduit pour classer des cartes perforées.

Pour illustrer le principe, on considère une liste de n nombres entre 0 et 999. Le tri par base fait trois passages dans une boucle principale, consistant à insérer de gauche à droite les nombres dans 10 listes, une par chire des unités au premier passage, dizaines au deuxième, centaines au troisième, avant de fusionner les listes dans l'ordre de leur identiant.

À la n du deuxième passage dans la boucle principale, les nombres sont dans l'ordre croissant des dizaines, et à même dizaine, ils sont dans l'ordre croissant des unités en raison du résultat du premier passage, ce qui permet de déduire la correction de l'algorithme.

Une implémentation sur les entiers du tri par base peut être la suivante : def radix_sort(liste):

"""On suppose la liste composée uniquement d'entiers naturels.

Le tri se fait en séparant la liste en dix listes

suivant chaque chiffre depuis les unités, en fusionnant les dix listes et en recommençant à partir de la liste obtenue

jusqu'au chiffre maximal."""

taille_max = 0

paquets = [[] for i in range(10)] # surtout pas [[]] * 10 for elem in liste:

assert type(elem) == int and elem >= 0 if len(str(elem)) > taille_max:

taille_max = len(str(elem))

paquets[elem %10].append(elem) # On profite de la première boucle puiss10 = 1

l = []

for paquet in range(10):

l.extend(paquets[paquet]) for i in range(1, taille_max+1):

paquets = [[] for i in range(10)]

puiss10 *= 10 for elem in l:

paquets[(elem//puiss10)%10].append(elem) l = []

for paquet in range(10):

l.extend(paquets[paquet]) return l

Deuxième partie Travaux pratiques

31

TP 1 : PILES 33

TP 1 : Piles

Ce TP permet de mettre en pratique le cours sur les piles en écrivant soi-même une implémentation en Python de cette structure de données.

Exercice 1 : Recopier le code des opérations de base sur les piles.

Exercice 2 : Écrire les fonctions avancées sur les piles (est_vide(p), taille(p) et sommet(p)).

Les quatre exercices suivants sont à faire deux fois : naturellement puis à l'aide des opérations déjà dénies.

Exercice 3 : Écrire une fonction qui accède au i-ième élément d'une pile en partant du fond (qui est le premier, donc).

Exercice 4 : Écrire une fonction qui échange les deux éléments au sommet d'une pile.

Exercice 5 : Écrire une fonction qui met l'élément du sommet au fond de la pile.

Exercice 6 : Écrire une fonction qui inverse l'ordre des éléments de la pile.

Nous verrons plus en détail la notion de pile d'appels (en Python) dans la section sur la récursivité. Il peut être intéressant de chercher à provoquer une erreur de dépassement de pile7, aussi appelée stack overow.

Une remarque pratique : dans un éditeur tel que Libreoce, la pile des modications est de capacité relativement courte. Ici, il s'agit d'une pile dont le fond est progres-sivement eacé plutôt que de provoquer une erreur quand on tente d'empiler trop d'éléments.

On peut rééchir à une implémentation de la pile qui utilise toujours une liste mais pour lequel le fond de la pile n'est pas nécessairement au début : le premier élément de la liste mémorise non seulement la taille, mais aussi l'indice de départ, de sorte qu'on écrase le deuxième élément après que le bout de la liste est atteint.

Pour ceux que cela intéresse, cela ressemble grandement à l'implémentation intuitive

7. quand on programme, elles arrivent vite si on fait un oubli bête. . .

d'une le, à l'aide d'une liste que l'on qualierait de circulaire, la diérence réside dans le fait que retirer un élément se fait diéremment et donc l'indice de départ est modié à chaque fois que l'on retire un élément.

Exercice 7 : Réaliser cette nouvelle implémentation d'une pile et réécrire les fonctions de ce TP.

TP 2 : APPLICATIONS DES PILES 35

TP 2 : Applications des piles

Ce TP illustre quelques-unes des applications les plus courantes des piles. Il se com-pose de questions de programmation et de questions plus théoriques. Il est impératif d'utiliser des piles pour chaque exercice de programmation.

1 Analyse d'expressions bien parenthésées

Nous nous intéressons à des expressions mathématiques utilisant des parenthèses, avec la question de déterminer si les parenthèses ne provoquent pas d'erreur de syntaxe. Dans la mesure où tout ce qui n'est pas une parenthèse n'a pas de pertinence dans cette étude, on considèrera que les expressions sont des mots n'utilisant que les caractères '(' et ')'.

Exercice 1 : Donner une condition nécessaire et susante pour qu'un mot soit bien parenthésé.

Exercice de mathématiques : Prouver que le nombre de mots bien parenthésés de taille 2n est n+11 2nn

(c'est le n-ième nombre de Catalan).

Exercice 2 : Écrire un programme qui vérie si un mot est bien parenthésé.8

Exercice 3 : Écrire un programme qui, étant donné un mot bien parenthésé, retourne la liste des couples i,j représentant les indices (en commençant à 0) des couples de parenthèses suivant le parenthésage.

On considère maintenant un nombre arbitraire de parenthèses diérentes, qui auront un identiant entier strictement positif. Les parenthèses ouvrantes seront marquées par l'identiant et les parenthèses fermantes par l'opposé de l'identiant. Une ex-pression sera alors une liste d'entiers non nuls9

Exercice 4 : Écrire un programme qui vérie si une telle liste est bien parenthésée.

8. L'utilisation des piles n'est pas obligatoire ici.

9. On pourrait assimiler le zéro à autre chose qu'une parenthèse.

2 Notation polonaise inversée

Une expression en notation polonaise inversée a le bon goût de ne pas nécessiter de parenthèses.

La notation est postxe, dans la mesure où l'opérateur est situé après ses opérandes, de sorte que par exemple (2 + 3) x 5 s'écrira 2 3 + 5 x.

En fait, rencontrer un opérateur dans la lecture de l'expression fait chercher les valeurs (opérandes ou résultats d'opérations) les plus récentes et applique l'opération.

Le nombre d'opérandes d'un opérateur étant important, il faut alors utiliser deux symboles - diérents : un pour le signe (s'appliquant à un opérande) et un pour la soustraction (s'appliquant à deux opérandes)10.

Exercice 5 : Écrire un programme qui évalue une expression arithmétique qui est donnée en notation polonaise inversée, par exemple sous la forme d'une liste de chaînes de caractères11. L'expression utilisera seulement des entiers, des ottants et les opérateurs usuels sur ces nombres.

3 Parcours de labyrinthe

Pour sortir d'un labyrinthe dit parfait, une méthode simple est la méthode de la main gauche : on suit un chemin en maintenant constamment sa main gauche contre un mur12. Cette méthode consiste simplement à faire un parcours en profondeur.

Les labyrinthes que nous considérons ici sont des matrices dont les cellules contiennent une information sur 4 bits, indiquant la présence ou non d'un mur en haut, en bas, à gauche et à droite. Il est essentiel que les informations soient cohérentes d'une cellule à l'autre et que les bords de la matrice indiquent des murs vers les cellules inexistantes.

Exercice 6 : Écrire un programme qui vérie si une matrice correspond à un labyrinthe valide.

10. De nombreuses calculatrices ont utilisé cette notation, ce qui explique l'emploi historique des deux boutons.

11. ou, pour les plus sportifs, d'une chaîne qu'il faudra éclater suivant les espaces 12. Attention aux éraures !

TP 2 : APPLICATIONS DES PILES 37 Nous allons implémenter un algorithme équivalent, consistant, à chaque position (en pratique à chaque intersection), à :

empiler la position courante et mémoriser la position depuis laquelle on y est arrivé, si on la visite pour la première fois ;

tester successivement les sorties possibles de la position courante dans le sens des aiguilles d'une montre à partir du point cardinal d'où on est arrivé dans la position.

Exercice 7 : Écrire un programme correspondant.

Bonus pour les plus motivés : il est possible de générer un labyrinthe parfait, c'est-à-dire dans lequel il existe un et un seul chemin de n'importe quelle position à n'importe quelle position, à partir de tirages aléatoires et de piles. N'hésitez pas à le tenter.

TP 3 : RÉCURSIVITÉ 39

TP 3 : Récursivité

Dans ce TP, toutes les fonctions demandées devront être récursives.

Avant de commencer, comparer des résultats de la fonction racine du cours avec la fonction sqrt du module math.

Déclencher également une erreur de dépassement de la pile d'appels en comptant la profondeur maximale atteinte.

Exercice 1 : Écrire une fonction puissance de complexité logarithmique en temps.

Exercice 2 : Écrire une fonction imprimant les instructions pour résoudre le problème des tours de Hanoï.

Remarque : Ce problème consiste à déplacer une pile de n (en argument) anneaux de taille croissante d'un tas (matérialisé par un piquet) à un autre (parmi trois), les opérations élémentaires étant le déplacement d'un anneau du haut d'une pile sur le haut d'une autre pile, à condition qu'il soit plus petit que l'ancien sommet de la pile d'arrivée.

Le nombre optimal d'opérations élémentaires est 2n−1.

Exercice 3 : Écrire une fonction comptant le nombre de façons de tracer une ligne de longueur n (en argument) avec des segments de longueur 2 ou 3 (l'ordre est important).

Exercice 4 : Adapter le code de l'exercice précédent pour retourner la liste des façons de tracer la ligne en question.

Exercice 5 : Déterminer ce que retourne la fonction de McCarthy.

def mccarthy(n):

if n > 100:

return n-10 else:

return mccarthy(mccarthy(n+11))

Exercice 6 : Recopier le code de la fonction d'Ackermann et l'appeler pour certaines

Exercice 7 : Écrire une fonction retournant le n-ième terme de la suite de Fibonacci.

Déterminer la complexité de la fonction, et chercher à la rendre linéaire.

Exercice 8 : Écrire une fonction qui calcule le PGCD de deux entiers à l'aide de l'algorithme d'Euclide.

Exercice 9 : Écrire une fonction qui détermine si un entier relatif ne comporte que des 0et des 1 dans son écriture en base 3.

Terminons ce TP par quelques compléments. La fonction de Morris est un cas par-ticulier de fonction récursive, dont la terminaison se prouve à l'aide d'un variant. . . et qui pourtant ne termine pas.

En pratique, c'est dû au fait que Python, comme la plupart des langages, évalue d'abord les arguments d'une fonction avant de procéder à son appel.

Ainsi, dans le code suivant : def morris(m, n):

if m == 0:

return 1 else:

return morris(m-1, morris(m, n))

. . . appeler morris(1,0) provoquera un dépassement de la pile d'exécution, car Py-thon calculera morris(0, morris(0, ... morris(0, morris(1, 0))...)), qui vaut bien entendu 1, mais cette valeur ne sera jamais obtenue.

TP 3 : RÉCURSIVITÉ 41 Ceci incite à la prudence lors de l'écriture de preuves de terminaison.

Le dernier exercice met surtout en ÷uvre le premier chapitre, du point de vue al-gorithmique, mais rien n'empêche de chercher une solution en tant que fonction récursive.

Exercice 10 : Écrire des programmes qui résolvent des problèmes classiques de pas-sage de rivière , notamment celui du loup, de la chèvre et du chou.

TP 4 : TRIS 43

TP 4 : Tris

Pour tout ce TP, nous allons évaluer le temps mis par les programmes écrits, grâce à la fonction clock du module time, et générer des listes grâce à la fonction shuffle du module random :

from time import time from random import shuffle

def temps_execution(tri, liste):

avant = time() tri(liste)

return time() - avant l = list(range(20000)) shuffle(l)

temps_execution(mon_tri, l)

Si on souhaite comparer plusieurs tris en place sur une même liste, il est bon de rappeler que l2 = l est une mauvaise idée, car les modications aectant l aectent l2 et vice-versa.

Une façon de copier une liste dans une autre est de faire par exemple l2 = l[:]

(pour une liste de nombres, cela sut, sinon il s'agit de faire une copie profonde).

Exercice 1 : Écrire une version non en place des tris par insertion et par sélection.

La fonction devra alors avoir une valeur de retour, puisque la liste en argument ne sera pas modiée.

Exercice de mathématiques : Montrer que le nombre moyen d'inversions13 dans une permutation est n(n−1)4 .

Le tri à bulles pèche par sa complexité, et seules sa compréhension relativement facile et son originalité font qu'il est enseigné.

Ainsi donc, rentabilisons-le en faisant quelques calculs dessus. Vérier si la liste est

13. Une inversion dans une permutationσest un couple(x, y)tel quex < y etσx> σy.

déjà triée peut se faire de manière très pratique dans le tri à bulles : on utilise un booléen déterminant si un échange a été eectué dans un parcours de la boucle principale. Si le booléen reste à False, c'est que la liste est déjà triée.

L'utilité est de ne pas perdre de temps quand la liste de départ est presque triée, dans la mesure où par exemple quelques éléments parmi les plus grands sont au début de la liste.

Le principe du tri à bulles fait que de tels éléments seront vite envoyés à leur place14, mais le souci est qu'au contraire des éléments parmi les plus petits sont peut-être à la n de la liste15, et ils devront être échangés au cours d'un nombre considérable16 de parcours de la boucle principale.

Le tri cocktail pallie ce souci en faisant des parcours de la liste dans les deux sens, an qu'il n'y ait pas à proprement parler de tortue . Après chaque (double) parcours dans le tri cocktail, un élément supplémentaire est à la bonne place dans les deux extrémités.

Exercice 2 : Implémenter le tri cocktail.

Exercice d'informatique : Faire les preuves de terminaison et correction du tri fusion et du tri rapide.

Exercice d'informatique : Faire tourner à la main un tri au choix du cours sur une entrée de taille 16.

Exercice 3 : Implémenter une fonction de tri qui applique un algorithme le plus ecace possible dans l'hypothèse où la liste en entrée est obtenue à partir d'une liste triée après un nombre très faible d'insertions de nouveaux éléments17

Exercice 4 : Implémenter le tri par dénombrement.

Exercice 5 : Implémenter le tri par base pour des chaînes de caractères.

14. On utilise le terme imagé de lièvres . 15. Et ceux-là sont appelés des tortues . 16. . . . de lièvre ?

17. Le chier http://jdreichert.fr/Enseignement/CPGE/IPTSPE/liste.py contient une liste de la sorte, le but du jeu est de faire le meilleur temps possible sur cet exemple.

TP 4 : TRIS 45 Exercice 6 : Implémenter un algorithme de tri le plus ecace possible pour une liste correspondant à une permutation quelconque dans Sn.

TP 5 : INVERSION DE MATRICES 47

TP 5 : Inversion de matrices 1 Algorithme de Strassen

Le point de départ de notre étude est l'algorithme de Strassen pour la multiplication matricielle.

Le principe est le suivant : on sait que les multiplications matricielles peuvent se faire par blocs.

Or, découper deux matrices (supposées carrées et de taille paire. . . et bien entendu identique, du coup) en quatre blocs chacune, toutes les tailles étant égales, fait se ramener au produit de huit matrices de taille la moitié, ce qui n'apporte rien en termes de complexité.

Des relations particulièrement pertinentes entre les blocs ont permis à Volker Strassen de se ramener à sept multiplications (et beaucoup d'additions), pour un coût nal abaissé à O(nlog2(7)).

Exercice 1 : Rechercher l'algorithme de Strassen sur internet. Ensuite, l'écrire en Python et prouver que les blocs correspondent bien aux produits attendus18.

2 Inverse d'une matrice

Soit M une matrice carrée supposée inversible. D'après les propriétés de la transpo-sition, on a M−1 = (tM M)−1 ×tM et donc on peut ramener le calcul de l'inverse d'une matrice au calcul de l'inverse d'une matrice symétrique19.

Soit donc M symétrique de taille une puissance de 2 (pour simplier). M étant inversible dès qu'elle est dénie positive, on va supposer ceci (c'est le cas pour une matrice de la forme tN N, ce qui tombe bien).

On décompose M en blocs de taille identique, en notant que le bloc en haut à droite et le bloc en bas à gauche sont la transposée l'un de l'autre20, d'où les blocs (dans

18. (exercice de mathématiques)

19. (exercice de mathématiques : prouver l'égalité, et prouver aussi quetM M est symétrique) 20. (exercice de mathématiques : le prouver)

l'ordre de lecture) A, B,tB, C.

On appelle complément de Schur de C la matrice S=C−B×A−1×tB. Il s'avère que l'inverse de M est exactement

A−1+A−1×tB×S−1×B×A−1 −A−1×tB×S−1

−S−1×B×A−1 S−1

Dans ce cas, l'inversion d'une matrice de taille 2n se fait récursivement (les matrices en question sont elles aussi symétriques), et on en déduit l'inverse de M à l'aide de deux inversions de matrices de taillen, ainsi qu'un nombre restreint de multiplications matricielles21, là aussi sur des matrices de taillen.

Exercice 2 : Implémenter cette méthode d'inversion matricielle. Prouver que la com-plexité est dominée par la comcom-plexité du produit (en partant du lemme selon lequel la complexité du produit domine n2).

21. (exercice de mathématiques : combien ?)

TP 6 : DESCENTE DE GRADIENT 49

TP 6 : Descente de gradient

Le but de ce TP est d'illustrer une méthode pour déterminer approximativement (et avec un risque de se tromper totalement) les paramètres d'une équation diéren-tielle dont on connaît la forme, une intuition éventuelle de certains paramètres et surtout des mesures expérimentales. Statistiquement, une poignée de TIPE peuvent bénécier de ce TP chaque année.22

Le principe de la descente de gradient est de partir d'un n-uplet de valeurs présumées pour une équation diérentielle et d'engendrer les valeurs attendues par la théorie pour la fonction solution, pour les comparer aux valeurs expérimentales. Si l'écart est susamment faible, on peut admettre qu'il s'agit de la bonne solution, mais évi-demment on n'arrive pas par magie à un tel écart. Pour des raisons pratiques, l'écart se calcule comme la somme des carrés des diérences entre les valeurs théoriques et expérimentales.

Une fois qu'un écart initial est calculé, il s'agit de voir ce qu'une légère modication d'un des paramètres deviné provoque au niveau de l'écart : s'il est amélioré, on pro-gresse dans cette direction, sinon on annule et on cherche une nouvelle modication, a priori plus légère et éventuellement dans une autre direction (c'est-à-dire de signe inversé ou d'un autre paramètre).

Une façon simple de faire les choses est de tester une modication inme pour com-mencer, an de déterminer la direction à prendre pour le début de l'algorithme (recherche du gradient) avant de passer à des modications raisonnables.

Le risque évoqué en début d'énoncé est de trouver un minimum local vers lequel on va converger sans espoir d'en sortir, alors qu'il ne s'agit pas de la bonne réponse.

La méthode plus ecace mais plus dicile du recuit simulé permet de pallier ce problème, et le lecteur intéressé pourra se renseigner sur le sujet.

Il faut noter que même les mesures expérimentales ne sont pas forcément ables, et il faut tenir compte de la présence éventuelle de bruitage. Ainsi, rien n'empêche de considérer un écart non nul entre la première valeur théorique et la première valeur expérimentale.

22. Merci à mon éminent collègue de physique pour la suggestion et la collaboration pour élaborer ce TP.

Enn, pour limiter au maximum les erreurs d'approximation dues à la résolution théorique de l'équation diérentielle, on utilisera plutôt odeint (même si réécrire

Enn, pour limiter au maximum les erreurs d'approximation dues à la résolution théorique de l'équation diérentielle, on utilisera plutôt odeint (même si réécrire

Dans le document Informatique pour tous deuxième année (Page 28-59)

Documents relatifs