Aller au contenu

Introduction

Il est trÚs souvent possible d'écrire des algorithmes totalement différents pour effectuer une tùche, résoudre un problÚme.

Comment comparer ces diffĂ©rentes solutions si elles donnent le mĂȘme rĂ©sultat ?

Le but de cette partie va ĂȘtre d'aborder la notion de complexitĂ© : ce qu'elle signifie et comment on la dĂ©termine.

Enfin, nous verrons comment s'assurer qu'un algorithme simple donnera toujours une solution correcte.

Partie 1 - ComplexitĂ©âš“ïžŽ

I - Algorithmes de recherche⚓

Cliquez sur Fiche algorithmes01.pdf pour récupérer l'énoncé du TD.

Travail préalable :

crĂ©er une fonction alea_liste(n) qui retourne une liste de n nombres entiers alĂ©atoires xi, avec 0 ≀ xi ≀ n dans le second cas

Affiche l'algorithme

Il est possible d'écrire cette fonction en une seule ligne, mais l'import de la fonction randint(début,fin) présente dans le module random est nécessaire.

Python
import random

def alea_liste(taille):
    return [random.random() for i in range(taille)]

1) Algorithmes de parcours d'un tableau⚓

Pour les exercices suivants, on utilisera la liste :

Python
test=[19, 16, 28, 26, 50, 16, 0, 36, 1, 45, 38, 27, 37, 16, 45, 41, 3, 19, 49, 43, 19, 44, 40, 23, 33, 25, 30, 38, 28, 49, 31, 37, 8, 48, 34, 12, 25, 6, 37, 23, 32, 36, 44, 45, 36, 29, 17, 11, 32, 27]

a. Écrire une fonction indice(liste,valeur) qui retourne l’indice de la premiere occurrence de valeur dans liste.
Exemple : indice(test,36) doit retourner la valeur 7, car test[7]=36.

Affiche l'algorithme
Python
def indice(liste,valeur):
    for i in range(len(liste)):
        if liste[i]==valeur:
            return i
    return -1

Remarque : il est nĂ©cessaire de prĂ©voir le cas oĂč la valeur ne serait pas prĂ©sente.

Il peut ĂȘtre dangereux de retourner False car cela risquerait d'ĂȘtre considĂ©rĂ© comme la valeur 0. D'oĂč le choix de retourner un nombre nĂ©gatif.

b. Écrire une fonction maximum(liste) qui retourne la plus grande valeur contenue dans liste.
Quelle valeur maximum(test) doit-elle retourner ?

Affiche l'algorithme
Python
def maximum(liste):
    maxi=liste[0]
    for i in range(len(liste)):
        if liste[i]>maxi:
            maxi=liste[i]
    return maxi

c. Écrire une fonction moyenne(liste) qui retourne la moyenne des valeurs contenues dans liste.

Affiche l'algorithme
Python
def moyenne(liste):
    somme = 0
    for i in liste:
        somme = somme + i
    moyenne = somme / len(liste)
    return moyenne

III- Evaluation du coĂ»t d'un algorithme⚓

1) Mesure expĂ©rimentale⚓

Le module time contient une fonction time() retournant le temps Ă©coulĂ© en nanosecondes depuis le 01/01/1970. Il est possible grĂące Ă  elle d’évaluer le temps que met une sĂ©rie d’instructions Ă  se rĂ©aliser :

a. En combien de temps moyenne(test) s’est executé ? b. Mesurer maintenant le temps que met moyenne(test) pour des listes plus grandes (gĂ©nĂ©rĂ©es Ă  l’aide alea_liste(n)) c. Que constatez-vous ? Les rĂ©sultats sont-ils toujours les mĂȘmes ?

Affiche l'algorithme
Python
import time
def bench(fonction,liste):
    debut = time.time()
    fonction(liste)
    fin = time.time()
    return fin - debut

Avec la fonction précédente, on se rend compte qu'il est impossible sur une machine récente de mesurer le temps que met la fonction moyenne(liste) à s'executer !

Il est donc nécessaire d'utiliser des listes plus longues pour pouvoir mesurer le temps d'execution :

Affiche l'algorithme
Python
from time import time_ns
from matplotlib import pyplot as plt

def bench(fonction,l_liste):
    liste=alea_liste(l_liste)
    chrono=time_ns()
    fonction(liste)
    return time_ns()-chrono

def bench_fonctions(liste_fonctions,depart,fin,pas,nb_tests=1,labels=False):
    k=0
    plt.ylabel("temps (ns)")
    plt.xlabel("nombre d'éléments")
    for fonction in liste_fonctions:
        x=[]
        y=[]
        increment=depart
        while increment<fin:
            print(increment)
            increment+=pas
            x.append(increment)
            y.append(moyenne([bench(fonction,increment) for i in range(nb_tests)]))
        if labels is False:
            lab=str(k)
        else:
            lab=labels[k]
        plt.plot(x,y,label=lab)
        k+=1
        # recherche relation de proportionnalité par méthode des moindres carrés
        sum_xi2,sum_xi_yi=0,0
        for i in range(len(x)):
            sum_xi2 +=x[i]**2
            sum_xi_yi +=x[i]*y[i]
        a=sum_xi_yi/sum_xi2
        plt.plot(x,[a*x[i] for i in range(len(x))])
        # fin du tracé de la droite
    plt.legend(loc="upper left")
    plt.show()

La fonction bench_fonctions va tester une liste de fonction sur un pour des valeurs à définir, puis tracer le graphe du temps de calcul en fonction de la longueur de la liste.

En utilisant les fonctions précédentes, et en lançant la commande

Python
bench_fonctions([moyenne],100,1e5,1000,10,["temps de calcul"])

on obtient le graphique suivant :

graphique représentant le temps de calcul de la moyenne en fonction du nombre d'éléments d'une liste

On remarque que si les valeurs sont irréguliÚres (en effet un ordinateur effectue beaucoup d'autres tùches pendant qu'il exécute un script en python), elles sont cependant en accord avec une relation de proportionnalité : Le temps d'éxécution semble proportionnel au nombre d'éléments présents dans la liste.

2) VĂ©rification⚓

Il n'est pas possible à notre niveau de déterminer exactement combien de temps va prendre le calcul dans le cas de python et d'un ordinateur de bureau, cependant on peut compter le nombre d'opération que fait l'algorithme dans le cas d'une moyenne.

→ Pour chaque indice de la liste en entrĂ©e, il va ajouter Ă  la variable somme la valeur prĂ©sente Ă  l'indice. Il rĂ©alise cette opĂ©ration n fois et on suppose qu'elle prend un temps t1. Ensuite, il divise somme par le nombre d'Ă©lĂ©ments, cela prend un temp t2.

On a donc un temps total de l'ordre de :

ttotal = n × t1 + t2

Si n est suffisamment grand, on peut négliger le temps pris par la division, on retrouve bien un temps proportionnel au nombre d'éléments.

Définition

On dit alors que la complexité est en O(n) ("grand O (la lettre o) de n").

On ne connait pas forcĂ©ment le coefficient de proportionnalitĂ©, qui dĂ©pend notamment des caractĂ©ristiques de la machine, mais on peut affirmer que si un calcul pour n Ă©lĂ©ments (et n suffisamment grand) a pris un temps t alors pour 2×n Ă©lĂ©ments, le calcul prendra un temps 2×t.

IV- Algorithmes de tri⚓

Il existe plusieurs façons de trier une liste :

Le tri par sélection

Celle qui me semble la plus simple est de chercher le maximum d’une liste, puis une fois celui-ci trouvĂ©, Ă©changer sa position avec l’élĂ©ment en premiĂšre position de la liste (tri dĂ©croissant) ou derniĂšre position (tri croissant).

Affiche l'algorithme

En pseudo-code cela donne :

Text Only
procédure tri_selection(tableau t)
    n ← longueur(t) 
    pour i de 0 Ă  n - 2
        min ← i       
        pour j de i + 1 Ă  n - 1
            si t[j] < t[min], alors min ← j

        si min ≠ i, alors Ă©changer t[i] et t[min]

Le tri par insertion

on peut aussi prendre consécutivement les éléments de la liste et les remplacer correctement un à un parmi les éléments déjà ordonnés de la liste.

Affiche l'algorithme
Text Only
procédure tri_insertion(tableau T)
n ← taille(T)
pour i de 0 Ă  n - 1

    # mémoriser T[i] dans x
    x ← T[i]                            

    # décaler vers la droite les éléments de T[0]..T[i-1] qui sont plus grands que x en partant de T[i-1]
    j ← i                               
    tant que j > 0 et T[j - 1] > x
        T[j] ← T[j - 1]
        j ← j - 1

    # placer x dans le "trou" laissé par le décalage
    T[j] ← x 
Le tri Ă  bulles

On peut enfin balayer progressivement la liste en inversant 1 Ă  1 chaque couple de nombres consĂ©cutivement de façon Ă  ce qu’ils soient dans l’ordre de rangement, et cela jusqu’à ce que la liste soit ordonnĂ©e.

Affiche l'algorithme
Text Only
procédure tri_insertion(tableau T)
n ← taille(T)
pour i de 0 Ă  n - 1
    pour j de 0 Ă  i - 1
        si T[j] > T[j + 1]
            temp = T[j]
            T[j] ← T[j + 1]
            T[j + 1] ← temp

Implémenter ces 3 algorithmes en python

tri par sélection
Python
def indice_mini(liste):
    indice = 0
    minimum = liste[0]
    for i in range(len(liste)):
        if liste[i] < minimum:
            indice = i
            minimum = liste[i]
    return indice

def tri_selection(liste):
    for i in range(len(liste)):
        i_mini = indice_mini(liste[i:])
        liste[i], liste[i + i_mini] = liste[i + i_mini], liste[i]
    return liste
tri par insertion
Python
def tri_insertion(liste):
    for i in range(len(liste)):
        k = 0
        while liste[k] < liste[i]:
            k = k + 1
        temp = liste[i]
        for j in range(i, k, -1):
            liste[j] = liste[j-1]
        liste[k] = temp
    return liste
tri Ă  bulles
Python
def tri_bulle(liste):

    for i in range(len(liste)):
        for j in range(len(liste)-i-1):
            if liste[j+1] < liste[j]:
                liste[j+1], liste[j] = liste[j], liste[j+1]
    return liste

Ces trois algorithmes retournent une liste correctement ordonnée, comment les classer ?

  • Par taille, on constate que le tri Ă  bulle est le plus concis
  • Par nombre de calculs, on constate que le tri Ă  bulles recommence systĂ©matiquement et inverse les positions une Ă  une, lĂ  oĂč les deux autres algorithmes n'inversent qu'un certain nombre de valeurs, voire une seule.
  • Par temps d'exĂ©cution : il est possible d'utiliser la fonction
Python
bench_fonctions([tri_selection,tri_insertion,tri_bulle],125,2.5e3,125,10,["tri par sélection","tri par insertion","tri à bulles"])

On obtient alors le graphique suivant :

graphique représentant le temps de tris pour les 3 algorithmes en fonction du nombre d'éléments

Le tri à bulles est (beaucoup) plus lent que les deux autres méthodes !

On remarque également que le temps de calcul n'est plus proportionnel au nombre d'éléments de la liste mais augmente plus vite que le nombre d'éléments.

Pour voir si ce temps augmente bien en fonction de n2, il suffit de changer l'axe vertical : au lieu de tracer t en fonction de n, on peut tracer √t en fonction de n.

Dans cette représentation, les relations proportionnelle à n2 sont visibles :

graphique représentant le temps de tris pour les 3 algorithmes en fonction du nombre d'éléments

graphique représentant le temps de tris pour les 3 algorithmes en fonction du nombre d'éléments

A savoir

MĂȘme si le tri Ă  bulles est le moins efficace, ces trois tris ont une complexitĂ© en O(n2)

Remarque

Ces 3 tris ne sont pas considérés commes efficaces, il existe des tris bien plus rapides, comme le tri fusion ou le tri rapide dont la complexité est inférieure à O(n2).

graphique reprĂ©sentant le temps de tris pour l'algorithme de tri rapide par rapport Ă  une complexitĂ© en O(nÂČ)

graphique reprĂ©sentant le temps de tris pour l'algorithme de tri rapide par rapport Ă  une complexitĂ© en O(nÂČ)

Partie 2 - Mini-Projets⚓

I- Le morpion⚓

Le morpion est un jeu Ă  2 joueurs sur une grille de 3 × 3 cases.

Chaque joueur place tour à tour une marque dans une case vide, le premier à aligner 3 de ses marques a gagné.

Le fichier suivant contient un script avec l'ossature du programme ainsi que les directives pour le remplir :

Voir le code
Python
# liste de liste définissant le plateau de jeu
# la valeur 0 correspond Ă   une case vide
grille=[[0 for i in range(3)] for j in range(3)]

""" Fonction se chargeant d'afficher le plateau de jeu
"""
def affiche():
    for j in range(3):
        print("{}|{}|{}".format(grille[j][0],grille[j][1],grille[j][2]))
        if j<2 : print("-|-|-")

def jeu():
    fini=False
    while not fini:
        """ tour du joueur 1 """
        # le jeu attend que le joueur indique les coordonnées de la case dans laquelle il compte placer sa marque
        # Supposons que les cases portent chacune un numéro compris entre 0 et 8 ou entre 1 et 9, à  votre convenance
        # ainsi la variable coup doit correspondre au numéro de la case à  marquer
        coup=input("joueur 1")
        # A la place de ce commentaire, vous devez écrire la partie qui se charge de : 
        #- vérifier que la case est libre
        #- modifie le tableau 'grille' pour indiquer que la case est prise par le joueur 1
        #- vérifie que le joueur 1 a gagné, ou non. Dans le cas d'une victoire, on peut interrompre la boucle while
        #  avec la commande break

        """ tour du joueur 2 """
        # faire de mĂȘme que pour le joueur 1
        coup=input("joueur 2")
        # faire de mĂȘme que pour le joueur 1

        pass #commande à  enlever lorsque vous aurez écrit votre programme

    #Afficher le résultat de la partie : joueur vainqueur ou match nul

affiche()

Un tableau est créé : il s'agit d'une liste contenant 3 sous-listes (une par ligne) contenant elles-mĂȘmes 3 valeurs :

Python
grille=[[0 for i in range(3)] for j in range(3)]

La grille sera modifiée en cours de jeu avec les valeurs suivantes : - 0 si vide - 1 si case remplie par le joueur 1 - 2 si case remplie par le joueur 2

Le travail :

  • DĂ©cider de la façon dont les joueurs vont indiquer la case choisie.
  • InterprĂ©ter la rĂ©ponse du joueur en fonction, et vĂ©rifier que la case est jouable, et faire rejouer le joueur si nĂ©cessaire jusqu'Ă  ce qu'il propose une case jouable.
  • Ajouter une fonction gagne() se chargeant de vĂ©rifier que la grille n'est pas dans une situation gagnante.
Voir le corrigé

Détails du programme

La fonction jeu

Python
def jeu():
    while True:
        coup_valide=False
        while not coup_valide:
            coup=int(input("joueur 1 : indiquez la case numérotée de 0 à 8"))
            if grille[coup//3][coup%3]==0:
                coup_valide=True
        grille[coup//3][coup%3]=1
        affiche()
        if jeu_gagnant():
            print("joueur 1 a gagné !")
            break

        if grille_pleine():
            print("Grille complĂšte, pas de gagnant")
            break
  • La fonction contient une boucle "tant que" qui tourne a priori Ă  l'infini. En rĂ©alitĂ©, en cas de victoire ou d'Ă©galitĂ©, les break vont stopper la boucle.
  • Pour faire en sorte que le joueur ayant la main joue un coup valide, on met une autre boucle "tant que" qui se rĂ©pĂ©tera tant que le coup ne sera pas valide. On considĂšre le coup valide lorsque le joueur demande une case vide.
  • La lecture de la case est donnĂ© par grille[coup//3][coup%3], pourquoi cela ?
  • J'ai choisi de numĂ©roter les cases de 0 Ă  8 de gauche Ă  droite puis de haut en bas. Imaginons que je souhaite jouer la case de la 2eme ligne tout Ă  droite : elle porte le numĂ©ro 5. vous pouvez vĂ©rifier que 5//3=1 et 5%3=2, ce qui correspond bien Ă  la case.
  • Une fois le coup jouĂ© et placĂ©, on affiche la grille, puis on vĂ©rifie si l'on est dans une configuration gagnante : Ă  ce point de la fonction, si une configuration gagnante est dĂ©tectĂ©e, c'est nĂ©cessairement celle du joueur 1.
  • On vĂ©rifie enfin que la grille n'est pas vide avant de continuer : en effet, la grille ayant 9 cases, c'est nĂ©cessairement le joueur 1 qui remplira la derniĂšre case.

La fonction jeu_gagnant()

Python
def jeu_gagnant():
    for i in range(3):
        if grille[i][0]==grille[i][1]==grille[i][2] and grille[i][0]!=0:
            return True
        if grille[0][i]==grille[1][i]==grille[2][i] and grille[0][i]!=0:
            return True
    if grille[0][0]==grille[1][1]==grille[2][2] and grille[0][0]!=0:
        return True
    if grille[0][2]==grille[1][1]==grille[2][0] and grille[2][0]!=0:
            return True
    return False
  • Il y a 8 configurations gagnantes : 3 lignes verticales, 3 horizontales et les 2 diagonales.
    On peut tester les 6 lignes avec une boucle et les 2 diagonales Ă  part
  • La fonction verifie que les 3 cases alignĂ©es ont la mĂȘme valeur, mais doit Ă©galement vĂ©rifier que cette valeur n'est pas nulle.
  • Enfin si aucune situation n'est gagnante, la fonction retourne False