Informatique – TD7 : Complexité et structures de données

publicité
Informatique – TD7 : Complexité et structures de données
CPP
septembre - novembre 2016
1
Complexité
1.1
Introduction (5mn)
Un algorithme est dit de complexité polynomiale si sa complexité est une fonction polynomiale de la taille
du problème n, par exemple f (n) = 3n3 − n2 + 10. Dans le cas d’une complexité polynomiale, les coefficients
des monômes n’importent pas puisqu’on s’intéresse à la complexité asymptotique (quand la taille des données
devient grande), et seul le degré du polynôme compte (n3 dans l’exemple ci-dessus).
Donc, en général, quand on « compte » les opération pour évaluer la complexité, on compte 1 pour chaque
opération (affectation, comparaison, arithmétique sur des entiers ou des flottants), même si la réalité (le temps
mis par chaque opération à s’exécuter) est différente.
Attention quand même attention aux « opérations » qui coûtent O(n) (comme les comparaisons ou copies
de liste). L’amalgame « 1 ligne de code = O(1) » est délicat avec un langage de haut niveau comme Python.
Cette observation s’applique à tous les calculs de complexité asymptotique, et même au cas des complexités
non-polynomiales : on s’intéresse uniquement au terme dominant.
1.2
Multiplication de deux matrices (10mn)
Soient A = (aij ) une matrice n × m et B = (bij ) une matrice m × P
p.
m
Le produit de A et B est une matrice n × p, C = (cij ), avec cij = k=1 aik bkj
— Quelle est la complexité du calcul d’un élément de la matrice C ?
— Combien y a-t-il d’éléments dans la matrice C ?
— Écrire la fonction python matmult(A,B), qui renvoie le produit de deux tableaux numpy A et B, sans
utiliser numpy.dot (rappel : A.shape[0] est le nombre de lignes et A.shape[1] le nombre de colonnes
de A) – attention aux indices !
— Calculer la complexité dans le cas meilleur, le cas pire, en moyenne, en fonction de la taille des données,
ici les dimensions (n, m, p) des matrices A et B.
2
Structures de données
Une structure de données est un assemblage de types simples (dans notre cas listes, booléens, entiers,
flottants, chaînes) permettant de représenter un objet plus complexe (exemples : un tuple d’une relation, un
arbre généalogique, un graphe de villes avec leurs distances)
Nous allons créer une structure de données pour représenter un vecteur creux, c’est-à-dire un vecteur contenant beaucoup de fois la valeur 0.
Un vecteur creux se comporte de la même manière qu’un vecteur classique (np.array à 1 dimension), mais
est optimisé pour le cas où le vecteur contient une majorité de valeurs nulles 1 .
2.1
Préliminaire : opérations sur les listes et complexité
Dans la suite, nous allons utiliser des listes Python pour représenter une structure de données. Voici un
rappel des opérations et de leur complexité.
Soient x une liste de longueur n et i un entier. e est une valeur quelconque.
— x[i] : accès à l’élément i, Θ(1)
1. Par exemple, l’instruction np.zeros(10**10) (créer un vecteur de taille 1010 ) provoque une MemoryError sur la machine de
l’auteur de ce document, alors que créer un vecteur creux (en utilisant coo_matrix de la bibliothèque scipy ou la version faite
maison) de taille 101000 ne pose aucun problème.
1
— x.append(e) : insertion en fin de liste, Θ(1)
— x = x + [e] : construction d’une nouvelle liste contenant les éléments de x suivis de e, Θ(n)
— x.insert(i, e) : insertion de e en position i, Θ(nombre d’éléménts déplacés) = Θ(n − i), ou Θ(1) si
aucun élément n’est déplacé.
— e = x.pop(i) : suppression de l’élément à la position i, Θ(nombre d’éléments déplacés) = Θ(n − i), ou
Θ(1) si aucun élément n’est déplacé.
Hypothèse simplificatrice : on suppose que l’espace mémoire alloué par Python pour stocker les éléments de
la liste est suffisant pour ajouter un élément en fin. L’insertion en fin est donc en Θ(1) et l’insertion en milieu
de liste en Θ(nombre d’éléments déplacés) 2 .
2.2
Structure de données = implantation + constructeurs + testeurs + sélecteurs
+ modifieurs (10mn)
L’utilisateur de notre structure de données ne devrait pas avoir à se soucier de la représentation interne de
notre vecteur creux (on parle de structure de données "opaque" ou "abstraite").
Lorsqu’on doit choisir une structure de données, avant de se poser la question « Comment représenter un
vecteur creux en Python ? » (c’est-à-dire « Quelle structure de données utiliser ? »), il faut se poser la question
« Pourquoi représenter un vecteur creux ? », c’est à dire « Quel jeu de fonctions proposer à l’utilisateur ? ».
Nous allons fournir à l’utilisateur des fonctions permettant de modifier ou de lire cette structure de données.
Pour notre vecteur creux, ces opérations sont les mêmes que pour np.array, mais leur implémentation et
leur complexité sera différente.
Ces fonctions sont de plusieurs types :
— les constructeurs permettent de créer une structure de données vide ou de créer une structure de données
à partir d’un fichier ou d’une autre structure de données (exemple : np.array(...)) ;
— les testeurs et sélecteurs permettent d’interroger la structure de données sans la modifier (est-ce qu’un
élément est présent dans l’ensemble ? combien d’éléments contient l’ensemble ?) (exemples : x.shape(),
x[...]) ;
— les modifieurs permettent de modifier la structure de données (ajouter un élément, en supprimer un,
extraire un élément quelconque) (exemple : x[...] = ...) ;
— des opérateurs qui prennent en entrée plusieurs instances de la structure et renvoient une valeur fonction
de ces entrées ;
— éventuellement, les destructeurs permettent de détruire la structure de données (en Python, dans beaucoup de cas ils ne sont pas nécessaires).
Elles constituent l’interface de la structure de données.
2.3
Spécification de l’interface (10mn)
Spécifier l’interface, c’est donner la réponse à la question « Pourquoi ? » ci-dessus, sous la forme de liste de
fonctions avec leurs paramètres et leurs valeurs de retour, sans en donner le contenu.
Note : la bibliothèque NumPy utilise les opérateurs (+, [], ...) pour l’interface de programmation des vecteurs,
mais vous n’avez pas les notions de Python nécessaires pour le faire : toute l’interface utilisera donc des fonctions
(def ...).
Donner une liste des fonctions de l’interface d’une structure de données « vecteur creux », par catégorie,
avec pour chaque fonction ce qu’elle fait et la liste des paramètres.
2.4
Représentation et implantation (30mn)
Implanter une fonction ou un algorithme, c’est la réponse à la question « Comment ? » ci-dessus.
Il faut choisir quel(s) type(s) Python utiliser, mais aussi comment les valeurs y sont stockées.
Nous proposons d’étudier la représentation d’un vecteur creux par une liste de doublets [indice, valeur]
(valeur est une valeur numérique quelconque. Pour simplifier on travaille avec des entiers, mais le code marchera
aussi avec des flottants par exemple). Pour mémoriser la taille du vecteur, on force la dernière valeur à être
représentée par un doublet même si elle est nulle. Les autres valeurs nulles ne sont pas représentées (on interdit
la présence d’un doublet [indice, 0] si indice n’est pas le dernier élément du vecteur).
2. En réalité, la complexité de ces opérations fait appel à la notion de complexité amortie qui n’est pas au programme, cf.
https://wiki.python.org/moin/TimeComplexity
2
Par exemple, le vecteur [1, 0, 0, 0, 12, 5, 0, 42, 0, 0] est représenté par : [[0, 1], [4, 12], [5,
5], [7, 42], [9, 0]], qui se lit « la valeur 1 à l’indice 0 puis la valeur 12 à l’indice 4, puis . . ., et toutes les
autres valeurs sont nulles ».
On donne la fonction afficher_vecteur(v) qui affiche le vecteur v mathématique (c’est à dire, valeurs
nulles comprises) :
def a f f i c h e r _ ve ct e u r ( v ):
print ( ' [ ' , end = ' ' )
first_zero = 0
for pair in v :
pos = pair [0]
val = pair [1]
# Note : on peut aussi é crire
# pos , val = pair
# On affiche les é v e n t u e l s z é ros depuis la derni è re valeur :
for i in range ( pos - first_zero ):
print (0 , end = ' ' )
# Puis la valeur non - nulle elle - m ê me :
print ( val , end = ' ' )
first_zero = pos + 1
print ( ' ] ' )
On note S la taille du vecteur et N le nombre d’éléments non-nuls. Quelle est la complexité pire cas et
meilleur cas de afficher_vecteur(v) en fonction de S et N ?
— Donner l’implantation en Python des fonctions suivantes :
— creer_vecteur(n) (indice : 1 ligne suffit)
— taille_vecteur(v) (indice : 1 ligne suffit)
— Évaluer la complexité de ces deux fonctions.
— Quelle serait la complexité de ces deux fonctions avec des vecteurs classiques (non creux) ?
— Donnez l’implémentation de la fonction changer_valeur(vect, i, val) (indice : c’est un peu plus
compliqué qu’on aurait pu le croire, cette fois-ci il faudra une dizaine de ligne).
— Quelle est la complexité (pire et meilleur cas) de cette fonction ?
— Donnez l’implémentation de la fonction comparer_vecteur(v1, v2). Quelle est sa complexité ?
— Quelle serait la complexité de cette fonction si on n’avait pas interdit les doublets [indice, 0] ?
2.4.1
Note sur l’examen
Un des exercices de l’examen portera sur les vecteurs creux. Les opérations rappelées section 2.1 seront
rappelées dans le sujet d’examen.
3
Téléchargement