Algorithmique Appliquée

BTS SIO SISR

Résolution de problèmes classiques

Plan

  • Listes chaînées
  • Queue et FIFO
  • Pile et LIFO
  • Comparaison entre FIFO et LIFO
  • Rappels sur la théorie des ensembles
  • Rappels sur le calcul matriciel

Listes chaînées

Introduction

  • En Python, une list permet de rassembler un nombre variable d'éléments.
  • Nous allons voir une manière d'implémenter ce type de liste.

Notion de liste chaînée

  • Une liste chaînée est une structure de données récursive.
  • Une liste chaînée est composée de noeuds.
  • Un noeud comporte 2 variables :
    • Une valeur,
    • Le noeud suivant.

Noeud d'une liste chaînée

2 noeuds

Structure de données

from dataclasses import dataclass

@dataclass
class Noeud:
    """Noeud de la liste chainee.
    
    La valeur peut etre n'importe quel objet 
    Python valide.
    Le suivant doit avoir pour type Noeud
    ou None.
    """
    valeur = None
    suivant = None

Noeud de départ

  • Comment identifier le noeud de départ de la liste ?
  • On souhaite que chaque noeud ait la même représentation.
  • On introduit un nouveau type, Liste, qui référence le noeud de départ.
  • Une Liste n'a pas de valeur.

Noeud de départ identifié par la liste

Structure de données

from dataclasses import dataclass

@dataclass
class Liste:
    """Liste chainee.

    Il s'agit simplement d'un point d'entree
    vers le 1er noeud de la liste chainee, 
    nommé initial.
    La variable initial doit avoir pour type
    Noeud ou None.
    """
    initial = None

Liste chaînée comportant 2 valeurs

Exemple avec 3 valeurs

liste_chainee = creer_liste_chainee(42, 3.14, "bonjour")

⬇️

Dernier noeud

Le suivant du dernier noeud est None.

Parcours d'une liste chaînée

def parcours(liste_chainee, f):
    """Appelle f sur chaque valeur de la liste chaînée.

    liste_chainee - liste chaînée de type Liste.
    f - fonction prenant un argument.
    """
    noeud = liste_chainee.initial
    while noeud != None:
        f(noeud.valeur)       # appelle la fonction f
        noeud = noeud.suivant # passage au noeud suivant

Insertion au début

L'insertion au début est non-destructive.
Il est possible de construire des listes chaînées immutables.

Insertion au milieu

Insertion à la fin

Suppression au début

Suppression au milieu

Suppression à la fin

Liste doublement chaînée

from dataclasses import dataclass

@dataclass
class NoeudBidirectionnel:
    valeur = None
    suivant = None
    precedent = None   # Nouveau

@dataclass
class ListeBidirectionnelle:
    initial = None
    final = None       # Nouveau

Evaluation des listes chaînées

  • Avantages :
    • Insertion rapide au début.
    • Insertion rapide à la fin pour une liste doublement chaînée.
  • Inconvénients :
    • Pas d'indexation : il faut parcourir potentiellement tous les éléments pour en retrouver un.
    • Mémoire éparse : chaque noeud a sa propre adresse en mémoire.

Comparaison avec une list

  • La Liste chaînée est un exercice intéressant pour comprendre comment une list peut être implémentée.
  • La Liste chaînée introduite ici et dans le prochain TP a un but purement pédagogique.
  • Dans du code industriel de production, utilisez une list.

TP : Manipulation d'une liste chaînée

TP : Manipulation d'une liste chaînée

Lien vers le sujet de TP.

Queue et FIFO

First-In, First-Out 🇬🇧

Notion de file d'attente

  • Une queue, également nommée file d'attente, est une collection.
  • Cette collection comporte 2 opérations principales :
    • Empiler un élément.
    • Dépiler le 1er élément empilé.
  • Premier entré, premier sorti.

Métaphore

Principe

Exemple avec 2 éléments

Structure de données

from dataclasses import dataclass

@dataclass
class Queue:
    """Queue utilisant une liste chainee.

    initial - doit avoir pour type 
              Noeud ou None.
    final - doit avoir pour type 
            Noeud ou None.
    """
    initial = None
    final = None

Empile

Dépile

Utilisation d'une list

def empile(queue, element):
    """Empile l'élément dans la queue.

    queue - la queue à modifier.
    element - element à empiler dans la queue.
    """
    queue.append(element)

def depile(queue):
    """Depile le 1er élément de la queue.

    queue - la queue à modifier.
    Retourne le 1er élément de la queue.
    """
    return queue.pop(0)

Exemple

queue = [6, 3, 7]
empile(queue, 10)
print(queue)       # [6, 3, 7, 10]

valeur = depile(queue)
print(valeur)      # 6
print(queue)       # [3, 7, 10]

Utilisation de deque

from collections import deque

def empile(queue, element):
    """Empile l'élément dans la queue.

    queue - la queue à modifier.
    element - element à empiler dans la queue.
    """
    queue.append(element)

def depile(queue):
    """Depile le 1er élément de la queue.

    queue - la queue à modifier.
    Retourne le 1er élément de la queue.
    """
    return queue.popleft()

Exemple

queue = deque([6, 3, 7])
empile(queue, 10)
print(queue)           # deque([6, 3, 7, 10])

valeur = depile(queue)
print(valeur)          # 6
print(queue)           # deque([3, 7, 10])

Pile et LIFO

Stack & Last-In, First-Out 🇬🇧

Notion de pile

  • Une pile est une collection.
  • Cette collection comporte 2 opérations principales :
    • Empiler un élément.
    • Dépiler le dernier élément empilé.
  • Dernier entré, premier sorti.

Métaphore

Principe

Exemple avec 2 éléments

Structure de données

from dataclasses import dataclass

@dataclass
class Pile:
    """Pile utilisant une liste chainee.

    initial - doit avoir pour type 
              Noeud ou None.
    """
    initial = None

Empile

Dépile

Utilisation d'une list

def empile(pile, element):
    """Empile l'élément dans la pile.

    pile - la pile à modifier.
    element - element à empiler dans la pile.
    """
    pile.insert(0, element)

def depile(pile):
    """Depile le 1er élément de la pile.

    pile - la pile à modifier.
    Retourne le 1er élément de la pile.
    """
    return pile.pop(0)

Exemple

pile = [6, 3, 7]
empile(pile, 10)
print(pile)           # [10, 6, 3, 7]

valeur = depile(pile)
print(valeur)         # 10
print(pile)           # [6, 3, 7]

Utilisation de deque

from collections import deque

def empile(pile, element):
    """Empile l'élément dans la pile.

    pile - la pile à modifier.
    element - element à empiler dans la pile.
    """
    pile.appendleft(element)

def depile(pile):
    """Depile le 1er élément de la pile.

    pile - la pile à modifier.
    Retourne le 1er élément de la pile.
    """
    return pile.popleft()

Exemple

pile = deque([6, 3, 7])
empile(pile, 10)
print(pile)            # deque([10, 6, 3, 7])

valeur = depile(pile)
print(valeur)          # 10
print(pile)            # deque([6, 3, 7])

Comparaison entre FIFO et LIFO

FIFO 🆚 LIFO

Anglais Français Collection
FIFO First In, First Out Premier Entré, Premier Sorti Queue
LIFO Last In, First Out Dernier Entré, Premier Sorti Pile

FIFO dans la réalité

Les queues de messages

  • Ordonnanceur de tâches (systèmes d'exploitation).
  • Traitements asynchrones dans un système.
  • Version itérative d'algorithmes récursifs.

LIFO dans la réalité

  • Pile d'appels de fonctions.
  • Interpréteur.
  • Version itérative d'algorithmes récursifs.

TP : Queues de messages simples

TP : Queues de messages simples

Lien vers le sujet de DM.

Rappels sur la théorie des ensembles

Union, intersection, différence

Intérêt

  • Nous avons vu le type set dans le cours précédent.
  • Le DM n°3 vous demande d'implémenter les opérations classiques sur les ensembles.
  • Nous revenons rapidement sur ces définitions pour préparer le DM.

Ensemble vide

Ensemble avec quelques éléments

Ensembles disjoints

Sous-ensemble

Intersection

Union

Exclusion

Différence

Rappels sur le calcul matriciel

Résolution de systèmes d'équations linéaires

Intérêt

  • En prévision du DM n°3 et de l'examen.
  • Pour faire des jeux vidéos.
  • Pour la conception assistée par ordinateur.

Déterminant d'une matrice 2x2

Expansion de Laplace

Déterminant d'une matrice 3x3

Il s'agit d'une définition récursive.

Expansion de Laplace

Déterminant d'une matrice NxN

Les sont les mineurs des matrices de la première ligne de .

Mineur d'une matrice

Le mineur d'une matrice aux indices est noté .

Système d'équations linéaires

Sous forme matricielle

Résolution du système

Problème : L'inversion matricielle n'est pas trivialle.

Règle de Cramer

Matrice Identité

L'élément neutre de la multiplication matricielle est noté et comporte des 0 partout sauf sur sa diagonale, composée de 1.

Matrice triangulaire

Soit la partie supérieure de la matrice, soit la partie inférieure de la matrice, n'est composée que de 0.

est triangulaire supérieure.
est triangulaire inférieure.

Matrice diagonale

Tous les éléments de la matrice sont nuls, sauf ceux sur sa diagonale.

Puissance d'une matrice diagonale

Donc :

Diagonalisation : principe (1/2)

  • Si une matrice est diagonalisable alors on peut l'écrire est sa matrice diagonale, et P la matrice de passage.
  • Pour diagonaliser, on calcule les valeurs propres telles que .
  • Pour chaque valeur propre, on calcule le vecteur propre associé tel que .

Diagonalisation : principe (2/2)

  • La matrice diagonale équivalent s'obtient en mettant les valeurs propres sur la diagonale.
  • La matrice de passage est obtenue en mettant les vecteurs propres obtenus en colonnes.

Matrice augmentée

Elimination de Gauss Jordan (1/3)

On sait que les opérations suivantes sont possibles pour résoudre le système sans changer la solution :

  • Interchanger 2 lignes.
  • Multiplier une ligne par un scalaire non-nul.
  • Ajouter un multiple d'une ligne à une autre.

Elimination de Gauss Jordan (2/3)

  • On parcourt chaque colonne de :
    • Le pivôt est l'élément sur la diagonale dans cette colonne : .
    • On parcourt chaque ligne de sauf celle où se trouve le pivôt :
      • On calcule le coefficient multiplicateur .
      • On soustrait fois la ligne à la ligne .

Elimination de Gauss Jordan (3/3)

La solution est la dernière colonne dont chaque élément est divisé par le pivôt sur la même ligne.

Exemple

On considère, dans , le système suivant :

Sous forme matricielle

Matrice augmentée

Sous forme de tableau

col1 col2 col3 col4
lig1 1 -0.5 -1 2
lig2 2 -1 1 1
lig3 1 -1 -2 3

1er pivôt

col1 col2 col3 col4
lig1 1 -0.5 -1 2
lig2 2 -1 1 1
lig3 1 -1 -2 3
col1 col2 col3 col4 Opérations
lig1 1 -0.5 -1 2
lig2 0 0 3 -3 lig2 -= 2 * lig1
lig3 0 -0.5 -1 1 lig3 -= 1 * lig1

Echange lig2 et lig3

col1 col2 col3 col4
lig1 1 -0.5 -1 2
lig3 0 -0.5 -1 1
lig2 0 0 3 -3

Un pivôt ne peut pas être nul.

2e pivôt

col1 col2 col3 col4
lig1 1 -0.5 -1 2
lig3 0 -0.5 -1 1
lig2 0 0 3 -3
col1 col2 col3 col4 Opérations
lig1 1 0 0 1 lig1 -= lig3
lig3 0 -0.5 -1 1
lig2 0 0 3 -3 lig2 -= 0 * lig3

3e pivôt

col1 col2 col3 col4
lig1 1 0 0 1
lig3 0 -0.5 -1 1
lig2 0 0 3 -3
col1 col2 col3 col4 Opérations
lig1 1 0 0 1 lig1 -= 0 * lig2
lig3 0 -0.5 0 0 lig3 += 1/3 * lig2
lig2 0 0 3 -3

Dernière étape

Division de la dernière colonne par les pivôts

ô

Devoir à la Maison 03

DM : Retours sur les fonctions et le débogage

Lien vers le sujet de DM.

On commence par étudier quelques structure de données supplémentaires, en complément du cours précédent. Ensuite, on prépare le terrain pour le Devoir à la Maison n°3 qui nécessite quelques bases en algèbre que l'on se propose de revoir.

Vous allez implémenter cette fonction dans le cadre du prochain TP.

En revanche, nous ne chercherons pas à construire une telle liste chaînée immutable dans le cadre de ce cours.

Une liste doublement chaînée conserve une référence vers le noeud précédent.

File d'attente de l'Empire State Building à New York. Source : Wikipedia (C)

Il s'agit ici d'une implémentation basée sur une liste chaînée. D'autres implémentations existent. Au début, on a initial = None et final = None. Lorsque le 1er noeud est inséré, initial et final pointent tous les 2 dessus.

On conserve une référence vers le dernier élément car on sait que chaque empilement se fait à la fin. Par conséquent, il faut pouvoir ajouter rapidement un élément à la fin. Nous avons vu que l'ajout à la fin d'une liste chaînée simple nécessite de traverser tous les éléments, ce qui n'est pas nécessairement rapide. Nous verrons lors du prochain cours des outils mathématiques pour évaluer la vitesse d'exécution d'un algorithme.

Vous allez implémenter cet algorithme dans le prochain TP.

Il s'agit d'une implémentation alternative basée sur une `list`. Cette implémentation est plus simple et probablement plus performante que celle avec notre liste chaînée.

Il existe une meilleure structure de données que `list` pour représenter une file d'attente. Il s'agit de la collection `deque`. Cette collection offre un profil de performances bien plus intéressant que `list` pour de grandes tailles de données.

Un empilement de pièces. On ne peut pas retirer les pièces intermédiares sans faire tomber une pièce supérieure. En tout cas, pas facilement. Cela montre un problème plus mécanique appelé "Empilement de bloc". Le surplomb maximal à chaque niveau est proportionnel à la moitié de la série harmonique. Source : Wikipedia

Il s'agit ici d'une implémentation basée sur une liste chaînée. D'autres implémentations existent.

Exactement identique à une liste chaînée.

Vous allez implémenter cet algorithme dans le prochain TP.

Il s'agit d'une implémentation alternative basée sur une list. De la même manière que l'on peut implémenter une queue avec une list, la même chose est possible pour une pile.

On peut aussi bien utiliser un deque pour représenter une queue ou une pile. Cela peut sembler contre-intuitif, mais c'est pratique.

A ne pas confondre avec FIFA...

A gauche : FIFO (queue). A droite : LIFO (stack).

Il s'agit d'un diagramme de Venn. On l'appelle vulgairement un patatoïde.

Vous avez déjà vu ces définitions dans le TD n°3. Nous repartons de ces définitions avant d'introduire de nouveaux concepts. Le calcul du déterminant d'une matrice à 2 lignes et 2 colonnes est simplement un produit en croix.

On prend la première ligne de la matrice M. Chaque élément de la première ligne est multiplié par le déterminant de la matrice 2x2 "opposée". On ajoute ou soustrait alternativement ces produits. Le fait de définir un déterminant d'une matrice 3x3 en fonction de déterminants 2x2 en fait une définition récursive. La condition de fin de la récursion est l'arrivée sur une matrice 2x2.

Le calcul d'un mineur d'une matrice est précisé sur la diapositive suivante. C'est dans cette définition que se retrouve la nature récursive de ce mode de calcul.

Un système d'équations est dit "linéaire" si ses inconnues n'ont pas d'exposant. Tout système d'équations linéaires peut s'écrire sous forme matricielle. Ici, on donne un exemple pour 3 équations à 3 inconnues.

On reviendra plus tard sur l'inversion matricielle. Il existe d'autres méthodes pour résoudre ce système d'équations sous forme matricielle.

La règle de Cramer nous permet de résoudre ce système d'équations en calculant uniquement des déterminants. Le dénominateur est le déterminant de la matrice M qui représente les coefficients du système d'équations. Le nominateur est la matrice M à laquelle on a remplacé une colonne par le vecteur D(d1, d2, d3). La règle de Cramer permet de résoudre les systèmes de N équations linéaires à N inconnues, si le déterminant de M est non nul. Nous allons voir d'autres méthodes pour résoudre ce système, certaines plus efficaces. On va commencer par quelques définitions supplémentaires

A priori, une matrice diagonale est très facile à inverser. Si on diagonalise une matrice, est-ce que cela nous aide à l'inverser ?

Une matrice n'est pas forcément diagonalisable. Il faut que le nombre de valeurs propres soit égal au rang de la matrice pour qu'elle soit diagonalisable. Vous verrez tout ceci plus en détails en cours de mathématiques.

Cette méthodologie peut être automatisée. Cela étant, il faut calculer P^{-1} pour revenir à la matrice initiale. Donc la diagonalisation n'est pas le bon procédé pour inverser une matrice. En pratique, il existe d'autres approches. L'une de ces approches consiste à utiliser la transposée de la co-matrice, mais cela entraine trop de calculs dans le cas général. Nous allons voir une approche efficace : le pivôt de Gauss, ou plus généralement l'élimination de Gauss-Jordan.

Autrement dit, il s'agit d'une simple concaténation des matrices M et D. Cette définition est utile pour l'élimination de Gauss-Jordan.

Il s'agit des étapes clés de l'algorithme.

Cet algorithme est illustré dans les diapositives suivantes. Vous aurez à l'implémenter dans le DM n°3.