Sommaire : Terminale_ipynb

1.1 - Implémentation, POO ✔
1.2 - Piles, Files, Listes ✔
1.3 - Dictionnaires ✔
1.4 - Arbres ✔
1.5 - Graphes ✔
2.1 - SQL Modèle relationnel ✔
2.2 - SQL Requêtes ✔
3.1 - Système sur puce ✔
3.2 - Processus, système ✔
3.3 - Routage ✔
3.4 - Congruences ✔
3.5 - RSA ✔
4.1 - Programme en tant que donnée ✔
4.2 - Récursivité ✔
4.3 - Modularité ✔
4.4 - Paradigmes de programmation ✔
4.5 - Gestion des bugs ✔
5.1 - Algorithme Graphes ✔
5.2 - Diviser pour régner ✔
5.3 - Programmation dynamique ✔
5.4 - Recherche textuelle ✔
01 - Hanoï ✔
02 - Hanoî (Correction) ✔
03 - Jeu 1 ✔
03 - Jeu 2 ✔
01 - Capytale Récursivité ✔
02 - Capytale Les bases du SQL ✔
03 - Capytale SQL Polynésie 2023 ✔
04 - Capytale ABR ✔
01 - POO, Piles, Files, Listes, Dictionnaires
02 - Arbre
03 - Graphe
04 - SQL
05 - Routage
06 - Récursivité et Diviser pour régner
07 - Système sur Puce et Processus Système
08 - Modularité
09 - Tri par insertion, tri par selection
10 - Les congruences

1.1 - Implémentation, POO ✔

Structures de données

Structures de données, interface et implémentation - Vocabulaire de la programmation objet : classes, attributs, méthodes, objets.

Compétences attendues :
Spécifier une structure de données par son interface.
Distinguer interface et implémentation.
Écrire plusieurs implémentations d’une même structure de données.
Écrire la définition d’une classe.
Accéder aux attributs et méthodes d’une classe.


Commentaires:
L’abstraction des structures de données est introduite après plusieurs implémentations d’une structure simple comme la file (avec un tableau ou avec deux piles)
On n’aborde pas ici tous les aspects de la programmation objet comme le polymorphisme et l’héritage.

1. Qu'est-ce qu'une structure de données ?

Une structure de données est un moyen d'organiser, de stocker et de manipuler des données dans un ordinateur de manière efficace.
Les structures de données permettent de gérer de grandes quantités de données, d'effectuer des opérations complexes et d'optimiser la vitesse et la mémoire utilisée.

Exemple :
Imaginez une bibliothèque. Sans un système organisé, il serait très difficile de trouver un livre. Les structures de données sont comme ces systèmes qui permettent de classer et de trouver rapidement des livres.

2. Interface vs Implémentation

3. Pourquoi faire la distinction ?


Séparer l'interface de l'implémentation offre plusieurs avantages:

4. Exemples courants de structures de données

Pile d'assiettes

file d'attente.jpg


Chaque structure a sa propre interface et peut avoir de nombreuses implémentations différentes.
L'essentiel est de comprendre que les structures de données, à travers leurs interfaces, nous permettent d'interagir avec des informations et des données sans nécessairement savoir comment elles sont stockées ou manipulées à l'arrière-plan (l'implémentation).

5. Programmation Orientée Objet

POO" est l'acronyme de "Programmation Orientée Objet" (en anglais : "OOP" pour "Object-Oriented Programming").

La POO est un paradigme de programmation qui utilise des "objets" et des classes pour organiser le code. Elle se base sur plusieurs concepts clés :

La POO offre de nombreux avantages, tels que la modularité, la réutilisabilité du code et une structure claire, ce qui facilite la maintenance et l'évolution du code.

Exemple : Voici comment nous pourriez implémenter une file (queue) avec un tableau (list en Python)

class FileAvecTableau: # Classe structure qui permet de définir des objets qui encapsulent des données et des fonctions.
    def __init__(self): # __init__: constructeur appelée lors de la création d'une nouvelle instance d'une classe.
                        # self est un paramètre qui fait référence à l'instance actuelle
        self.queue = [] #  attribut interne de l'objet ou de l'instance
        
    def enfile(self, item):  # Méthode
        self.queue.append(item)
        
    def defile(self):  # Méthode
        if not self.est_vide():
            return self.queue.pop(0)
        else:
            raise IndexError("Defile d'une file vide.")
            
    def est_vide(self): # Méthode
        return len(self.queue) == 0

    def taille(self):
        return len(self.queue)
    
    def __str__(self):   #__str__: Méthode appelée par print() ou str() pour obtenir une représentation de l'objet.
        return str(self.queue)  
    
ma_file = FileAvecTableau() # Créez une instance de la classe.

ma_file.enfile("Pierre")
ma_file.enfile("Paul")
ma_file.enfile("jacque")

print(ma_file.taille())

print(ma_file)
print(ma_file.defile())

print(ma_file)
print(ma_file.taille())

print(ma_file.est_vide())
3
['Pierre', 'Paul', 'jacque']
Pierre
['Paul', 'jacque']
2
False
ma_valise = FileAvecTableau() # Création  d'une autre instance avec la même classe.

ma_valise.enfile("Chemise")
ma_valise.enfile("Pullover")
ma_valise.enfile("Chaussettes")
ma_valise.enfile("Tee-shirt")
ma_valise.enfile("maillot")

print(ma_valise.taille())

print(ma_valise)
print(ma_valise.defile())

print(ma_valise)
print(ma_valise.taille())

print(ma_valise.est_vide())
5
['Chemise', 'Pullover', 'Chaussettes', 'Tee-shirt', 'maillot']
Chemise
['Pullover', 'Chaussettes', 'Tee-shirt', 'maillot']
4
False

Vidéo : Le paramètre self

Exemple : Voici comment faire la même implémentetion d'une file (queue) avec avec 2 piles

class FileAvecDeuxPiles:
    def __init__(self):
        self.pile1 = []  # Pile pour l'enfilage
        self.pile2 = []  # Pile pour le défilage

    def enfile(self, item):
        self.pile1.append(item)

    def defile(self):
        if not self.pile2:  # Si la pile2 est vide
            while self.pile1:  # Renverser la pile1 dans pile2
                self.pile2.append(self.pile1.pop())
            if not self.pile2:  # Si la pile2 est toujours vide après le renversement
                raise IndexError("Defile d'une file vide.")
        return self.pile2.pop()

    def est_vide(self):
        return not self.pile1 and not self.pile2

    def taille(self):
        return len(self.pile1) + len(self.pile2)

    def __str__(self):
        return str(self.pile2[::-1] + self.pile1)
ma_file = FileAvecDeuxPiles() # Créez une instance de la classe.

ma_file.enfile("Pierre")
ma_file.enfile("Paul")
ma_file.enfile("jacque")

print(ma_file.taille())

print(ma_file)
print(ma_file.defile())

print(ma_file)
print(ma_file.taille())


print(ma_file.est_vide())
3
['Pierre', 'Paul', 'jacque']
Pierre
['Paul', 'jacque']
2
False

Autres exemples :

Exemple 1 :

Créez une classe Personne qui a des attributs pour le nom, age et sexe de la personne. La classe doit également avoir une méthode se_presenter() qui affiche une brève introduction de la personne.

class Personne:
    def __init__(self, nom, age, sexe):
        self.nom = nom # Attribue externe
        self.age = age
        self.sexe = sexe
        
    def se_presenter(self):
        print(f"Bonjour, je m'appelle {self.nom}. J'ai {self.age} ans et je suis un {self.sexe}.")

# Test
p = Personne("Jean", 30, "homme")
p.se_presenter()
Bonjour, je m'appelle Jean. J'ai 30 ans et je suis un homme.

Exemple 2 :

Créez une classe Cercle qui prend un rayon comme attribut et a deux méthodes, l'une pour calculer la surface (surface) et l'autre pour calculer le périmètre (perimetre).

import math

class Cercle:
    def surface(self, rayon):
        return math.pi * rayon ** 2
    
    def perimetre(self, rayon):
        return 2 * math.pi * rayon

# Test
c = Cercle()
print(f"Surface: {c.surface(5)}")
print(f"Périmètre: {c.perimetre(5)}")
Surface: 78.53981633974483
Périmètre: 31.41592653589793
import math

class Cercle:
    def __init__(self, rayon):
        self.rayon = rayon
        
    def surface(self):
        return math.pi * self.rayon ** 2
    
    def perimetre(self):
        return 2 * math.pi * self.rayon

# Test
c = Cercle(5)
print(f"Surface: {c.surface()}")
print(f"Périmètre: {c.perimetre()}")
Surface: 78.53981633974483
Périmètre: 31.41592653589793
import math

class Cercle:
    def __init__(self):
        pass
        
    def definir_rayon(self, rayon):
        self.rayon = rayon
        
    def surface(self):
        if self.rayon is None:
            raise ValueError("Rayon non défini")
        return math.pi * self.rayon ** 2
    
    def perimetre(self):
        if self.rayon is None:
            raise ValueError("Rayon non défini")
        return 2 * math.pi * self.rayon

# Test
c = Cercle()
c.definir_rayon(5)  # Définir le rayon avant d'appeler les méthodes
print(f"Surface: {c.surface()}")
print(f"Périmètre: {c.perimetre()}")
Surface: 78.53981633974483
Périmètre: 31.41592653589793

Exemple 3:

Créez une classe Voiture qui a des attributs pour la marque, modele, et kilometrage. Ajoutez une méthode afficher_details() pour afficher les détails de la voiture et une autre méthode conduire() pour augmenter le kilométrage.

class Voiture:
    def __init__(self, marque, modele, kilometrage):
        self.marque = marque
        self.modele = modele
        self.kilometrage = kilometrage
        
    def afficher_details(self):
        print(f"Voiture {self.marque} {self.modele}, {self.kilometrage} km parcourus.")
        
    def conduire(self, km):
        self.kilometrage += km

# Test
v = Voiture("Peugeot", "208", 10000)
v.afficher_details()
v.conduire(150)
v.afficher_details()
Voiture Peugeot 208, 10000 km parcourus.
Voiture Peugeot 208, 10150 km parcourus.

Exercices :

Exercice 1 :

Créez une classe Rectangle qui prend la largeur et la hauteur comme attributs. Cette classe devrait avoir des méthodes pour calculer la surface (surface) et le périmètre (perimetre).

Exercice 2 :

Définissez une classe Points qui représente deux points dans un espace 2D : $A(x_1, y_1)$ et $B(x_2, y_2)$.

La classe doit avoir les méthodes suivantes :

  1. vecteur(): Renvoie les coordonnées du vecteur $\vec{AB}$.
  2. milieu(): Renvoie les coordonnées du point milieu du segment $[AB]$.
  3. distance(): Calcule la distance entre $A$ et $B$.
  4. equation_reduite(): Renvoie l'équation réduite de la droite passant par $A$ et $B$.
  5. equation_cercle(): Renvoie l'équation du cercle dont $[AB]$ est le diamètre.

Pour tester votre solution, vous pouvez utiliser les points $A(0,0)$ et $B(2,2)$ avec p = Points(0, 0, 2, 2)









































class Rectangle:
    def __init__(self, largeur, hauteur):
        self.largeur = largeur
        self.hauteur = hauteur
        
    def surface(self):
        return self.largeur * self.hauteur
    
    def perimetre(self):
        return 2 * (self.largeur + self.hauteur)

# Test
r = Rectangle(4, 6)
print(f"Surface: {r.surface()}")
print(f"Périmètre: {r.perimetre()}")
Surface: 24
Périmètre: 20
from math import *
class Rectangle:
    def __init__(self):
        pass
    
    def set_longueur(self, longueur):
        self.longueur = longueur
        
    def set_largeur(self, largeur):
        self.largeur = largeur
    
    
    def surface(self):
        return self.largeur * self.longueur
    
    def perimetre(self):
        return (2 * self.largeur) + (2* self.longueur)

# Test
r = Rectangle()
r.set_longueur(2)
r.set_largeur(1)
print(f"Surface: {r.surface()}")
print(f"Périmètre: {r.perimetre()}")
Surface: 2
Périmètre: 6
import math

class Points:
    def __init__(self, x1, y1, x2, y2):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2
    
    def vecteur(self):
        return (self.x2 - self.x1, self.y2 - self.y1)
    
    def milieu(self):
        return ((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2)
    
    def distance(self):
        return math.sqrt((self.x2 - self.x1)**2 + (self.y2 - self.y1)**2)
    
    def equation_reduite(self):
        if self.x2 - self.x1 == 0:
            return f"x = {self.x1}"
        m = (self.y2 - self.y1) / (self.x2 - self.x1)
        p = self.y1 - m * self.x1
        return f"y = {m:.2f}x + {p:.2f}"
    
    def equation_cercle(self):
        h, k = self.milieu()
        r = self.distance() / 2
        return f"(x - {h:.2f})^2 + (y - {k:.2f})^2 = {r**2:.2f}"

# Test
p = Points(0, 0, 2, 2)
print("Vecteur AB:", p.vecteur())
print("Milieu de [AB]:", p.milieu())
print(f"Distance AB: {p.distance():.2f}")
print("Equation réduite de la droite (AB):", p.equation_reduite())
print("Equation du cercle de diamètre [AB]:", p.equation_cercle())
Vecteur AB: (2, 2)
Milieu de [AB]: (1.0, 1.0)
Distance AB: 2.83
Equation réduite de la droite (AB): y = 1.00x + 0.00
Equation du cercle de diamètre [AB]: (x - 1.00)^2 + (y - 1.00)^2 = 2.00

Retour au sommaire

1.2 - Piles, Files, Listes ✔

Structures de données

Listes, piles, files : structures linéaires.

compétences attendues :
Distinguer des structures par le jeu des méthodes qui les caractérisent.
Choisir une structure de données adaptée à la situation à modéliser.


commentaires :
On distingue les modes FIFO (first in first out) et LIFO (last in first out) des piles et des files.

1) Introduction

De nombreux algorithmes "classiques" manipulent des structures de données plus complexes que des simples nombres. Nous allons ici voir quelques structures de données. Nous allons commencer par des types de structures relativement simples : les listes, les piles et les files. Ces trois types de structures sont qualifiés de linéaires.

2) Les piles

On retrouve dans les piles une partie des propriétés vues sur les listes. Dans les piles, il est uniquement possible de manipuler le dernier élément introduit dans la pile. On prend souvent l'analogie avec une pile d'assiettes : dans une pile d'assiettes la seule assiette directement accessible et la dernière assiette qui a été déposée sur la pile.

Pile d'assiettes

Les piles sont basées sur le principe LIFO (Last In First Out : le dernier rentré sera le premier à sortir). On retrouve souvent ce principe LIFO en informatique.

Voici les opérations que l'on peut réaliser sur une pile :

Exemples :

Soit une pile P composée des éléments suivants :

    22 (le sommet de la pile est 22)
    19
     7
     8
    14
    12

dépile(P) renvoie 22 et la pile P est maintenant composée des éléments suivants :

    19
     7
     8
    14
    12

taille(P) renvoie 5 empiler(42) la pile P est maintenant composée des éléments suivants :

    42
    19
     7
     8
    14
    12

si on applique dépiler(P) 6 fois de suite, estVide(P) renvoie vrai

Implémentation d'une pile

class Pile:
    def __init__(self):
        self.items = []

    def estvide(self):
        return len(self.items) == 0

    def empiler(self, item):
        self.items.append(item)

    def depiler(self):
        if self.estvide():
            print("La pile est vide!")
        else:
            element_depile = self.items.pop()
            #print(f"Élément dépilé : {element_depile}")
            #print()
            return element_depile

    def sommet(self):
        if self.estvide():
            print("La pile est vide!")
        else:
            return self.items[-1]

    def taille(self):
        print(f"Taille : {len(self.items)}")
        print()
        return len(self.items)

    def afficher(self):
        for item in reversed(self.items):
            print(f"{item:2}")
        print()
        

# Exemple d'utilisation
p = Pile()
p.empiler(12)
p.empiler(14)
p.empiler(8)
p.empiler(7)
p.empiler(19)
p.empiler(22)
p.afficher()

p.depiler()
p.afficher()
p.taille()

p.empiler(42)
p.afficher()

p.depiler()
p.depiler()
p.depiler()
p.depiler()
p.depiler()
p.depiler()
p.estvide()
22
19
 7
 8
14
12

19
 7
 8
14
12

Taille : 5

42
19
 7
 8
14
12

True

3) les files

Comme les piles, les files ont des points communs avec les listes.

Différences majeures : dans une file on ajoute des éléments à une extrémité de la file et on supprime des éléments à l'autre extrémité. On prend souvent l'analogie de la file d'attente devant un magasin pour décrire une file de données.

file d'attente.jpg

Les files sont basées sur le principe FIFO (First In First Out : le premier qui est rentré sera le premier à sortir. Ici aussi, on retrouve souvent ce principe FIFO en informatique.

Voici les opérations que l'on peut réaliser sur une file :

Exemples :

Soit une file F composée des éléments suivants : 12 → 14 → 8 → 7 → 19 → 22
(le premier élément rentré dans la file est 22 ; le dernier élément rentré dans la file est 12)
12 → 14 → 8 → 7 → 19 → 22

enfiler(42) : la file F est maintenant 42 → 12 → 14 → 8 → 7 → 19 → 22
taille(F) renvoie 6
défiler(F) renvoie 22 ,la file F est maintenant : 42 → 12 → 14 → 8 → 7 → 19
défiler(F) 6 fois de suite
estVide(F) renvoie vrai

class File:
    def __init__(self):
        self.elements = []

    def estVide(self):
        return len(self.elements) == 0

    def enfiler(self, element):
        self.elements.append(element)

    def defiler(self):
        if self.estVide():
            print("La file est vide !")
            return None
        else:
            return self.elements.pop(0)

    def taille(self):
        return len(self.elements)

    def afficher(self):
        for i, elem in enumerate(reversed(self.elements)):
            if i == len(self.elements) - 1:
                print(elem, end="")
            else:
                print(f"{elem} -> ", end="")
        print()
# Test
F = File()
F.enfiler(22)
F.enfiler(19)
F.enfiler(7)
F.enfiler(8)
F.enfiler(14)
F.enfiler(12)
F.afficher()  # Doit afficher 12 → 14 → 8 → 7 → 19 → 22
print(F.taille())  # Doit afficher : 6

F.enfiler(42)
F.afficher()  # Doit afficher 42 → 12 → 14 → 8 → 7 → 19 → 22

print(F.defiler()) #Doit afficher 22
F.afficher()  # Doit afficher 42 → 12 → 14 → 8 → 7 → 19

# Appliquer défile(F) 6 fois
for _ in range(6):
    F.defiler()

print(F.estVide())  # Doit afficher : True
12 -> 14 -> 8 -> 7 -> 19 -> 22
6
42 -> 12 -> 14 -> 8 -> 7 -> 19 -> 22
22
42 -> 12 -> 14 -> 8 -> 7 -> 19
True

4) Les listes

Une liste est une structure de données permettant de regrouper des données. Une liste L est composée de 2 parties :

Le langage de programmation Lisp, inventé par John McCarthy en 1958, a été un des premiers langages de programmation à introduire cette notion de liste. Lisp signifie "list processing".

Voici les opérations qui peuvent être effectuées sur une liste :

queue → ... → tête

class Liste:
    def __init__(self):
        self.elements = []
          
    def estVide(self):
        return len(self.elements) == 0

    def ajouter(self, element):
        self.elements.append(element)

    def dernier_element(self):
        if self.estVide():
            print("La liste est vide !")
            return None
        else:
            return self.elements[-1]
        
    def retirer(self):
        if self.estVide():
            print("La liste est vide !")
            return None
        else:
            return self.elements.pop(-1)

    def taille(self):
        return len(self.elements)

    def __str__(self):
        if not self.elements:
            return "nil"
        else:
            result = str(self.elements[0])
            for elem in self.elements[1:]:
                result += " → " + str(elem)
            return result
l = Liste()
print(l.estVide()) # Affiche: True
l.ajouter(12)
print(l) # Affiche 12
print(l.estVide()) # Affiche: False
l.ajouter(15)  # Ajoute 15 en tête
print(l) # Affiche 12 → 15
l.ajouter(11)
l.ajouter(1)
print(l) # Affiche 12 → 15 → 11 → 1
print(l.dernier_element()) # Affiche 1 mais ne le retire pas de la liste
l.retirer() # Retire 1
print(l) # Affiche 12 → 15 → 11 
print(l.taille()) # # Affiche 3
True
12
False
12 → 15
12 → 15 → 11 → 1
1
12 → 15 → 11
3

5) Types abstraits et représentation concrète des données

Nous avons évoqué ci-dessus la manipulation des types de données (liste, pile et file) par des algorithmes, mais, au-delà de la beauté intellectuelle de réfléchir sur ces algorithmes, le but de l'opération est souvent, à un moment ou un autre, de "traduire" ces algorithmes dans un langage compréhensible pour un ordinateur (Python, Java, C,...).

On dit alors que l'on implémente un algorithme.

Il est donc aussi nécessaire d'implémenter les types de données comme les listes, les piles ou les files afin qu'ils soient utilisables par les ordinateurs.

Les listes, les piles ou les files sont des "vues de l'esprit" présentes uniquement dans la tête des informaticiens, on dit que ce sont des types abstraits de données (ou plus simplement des types abstraits).

L'implémentation de ces types abstraits, afin qu'ils soient utilisables par une machine, est loin d'être une chose triviale. L'implémentation d'un type de données dépend du langage de programmation. Il faut, quel que soit le langage utilisé, que le programmeur retrouve les fonctions qui ont été définies pour le type abstrait (pour les listes, les piles et les files cela correspond aux fonctions définies ci-dessus). Certains types abstraits ne sont pas forcément implémentés dans un langage donné, si le programmeur veut utiliser ce type abstrait, il faudra qu'il le programme par lui-même en utilisant les "outils" fournis par son langage de programmation.

Pour implémenter les listes (ou les piles et les files), beaucoup de langages de programmation utilisent 2 structures : les tableaux et les listes chaînées.

Un tableau est une suite contiguë de cases mémoires (les adresses des cases mémoire se suivent). Le système réserve une plage d'adresse mémoire afin de stocker des éléments.

La taille d'un tableau est fixe : une fois que l'on a défini le nombre d'éléments que le tableau peut accueillir, il n'est pas possible modifier sa taille. Si l'on veut insérer une donnée, on doit créer un nouveau tableau plus grand et déplacer les éléments du premier tableau vers le second tout en ajoutant la donnée au bon endroit !

Dans certains langages de programmation, on trouve une version "évoluée" des tableaux : les tableaux dynamiques. Les tableaux dynamiques ont une taille qui peut varier. Il est donc relativement simple d'insérer des éléments dans le tableau. Ce type de tableaux permet d'implémenter facilement le type abstrait liste (de même pour les piles et les files).

À noter que les "listes Python" (listes Python) sont des tableaux dynamiques. Attention de ne pas confondre avec le type abstrait liste défini ci-dessus, ce sont de "faux amis".

6) Choisir une structure de données adaptée à la situation à modéliser.

Exemple 1 : Utilisation de la classe Pile pour la vérification d'équilibre de parenthèses.

pile = Pile()

def check_parentheses(expr):
    for char in expr:
        if char == "(":
            pile.empiler(char)
        elif char == ")":
            if pile.estvide():
                return False
            pile.depiler()
    return pile.estvide()

expr = "((1 + 2) * (3 + 4)*2)"
result = check_parentheses(expr)
print(f"Les parenthèses sont équilibrées: {result}")
Les parenthèses sont équilibrées: True

Exemple 2 : Utilisation de la classe File pour la gestion d'une file d'attente pour un guichet de banque.

file = File()

def serve_clients():
    while not file.estVide():
        client = file.defiler()
        print(f"Serving client {client}")

# Ajout de clients dans la file
file.enfiler("Alice")
file.enfiler("Bob")
file.enfiler("Charlie")

# Servir les clients
serve_clients()
Serving client Alice
Serving client Bob
Serving client Charlie

Exemple 3 : Utilisation de la classe Liste pour trouver le dernier élément ajouté à la liste.

liste = Liste()

# Ajout d'éléments
liste.ajouter(5)
liste.ajouter(10)
liste.ajouter(15)

# Trouver le dernier élément ajouté
dernier = liste.dernier_element()
print(f"Le dernier élément ajouté est {dernier}")

# Afficher la liste
print("La liste actuelle est:", liste)
Le dernier élément ajouté est 15
La liste actuelle est: 5 → 10 → 15

Exemple 4 : Distibution d'un jeu de carte à 2 joueus

from random import shuffle

# Jeu de 32 cartes
cartes = [i + j for i in ['7', '8', '9', '10', 'V', 'D', 'R', 'A'] for j in ["♠", "♥", "♦", "♣"]]
shuffle(cartes)

# Initialisation du jeu complet
jeu = Pile()
for carte in cartes:
    jeu.empiler(carte)

# Afficher le jeu complet
print('Jeu complet :')
jeu.afficher()
print()

# Distribution des cartes (piles)
jeu1 = Pile()
jeu2 = Pile()

while not jeu.estvide():
    carte = jeu.depiler()
    if carte:
        jeu1.empiler(carte)

    carte = jeu.depiler()
    if carte:
        jeu2.empiler(carte)

# Afficher les deux jeux de cartes après distribution
print("Jeu du joueur 1:")
jeu1.afficher()

print("\nJeu du joueur 2:")
jeu2.afficher()
Jeu complet
7♠
R♦
D♥
10♣
7♦
7♣
9♥
9♣
7♥
V♣
8♠
D♣
8♦
V♦
D♦
R♠
R♣
10♥
10♦
A♠
8♣
A♣
9♦
A♥
8♥
9♠
D♠
10♠
R♥
V♥
V♠
A♦


Jeu du joueur 1:
V♠
R♥
D♠
8♥
9♦
8♣
10♦
R♣
D♦
8♦
8♠
7♥
9♥
7♦
D♥
7♠


Jeu du joueur 2:
A♦
V♥
10♠
9♠
A♥
A♣
A♠
10♥
R♠
V♦
D♣
V♣
9♣
7♣
10♣
R♦

Retour au sommaire

1.3 - Dictionnaires ✔

Structures de données

Dictionnaires, index et clé


Capacités attendus :
Distinguer la recherche d’une valeur dans une liste et dans un dictionnaire.

Entrainement

BAC EX 2 POO Dic sujet 09 Correction sur Notebook

1) Dictionnaires

Un dictionnaire est une structure de données qui stocke des paires clé-valeur.
Chaque clé est unique et chaque clé est associée à une valeur.
Voici comment vous pouvez créer un dictionnaire en Python :

mon_dictionnaire = {
  "nom": "Alice",
  "age": 30,
  "email": "alice@email.com"
}

print(mon_dictionnaire)
{'nom': 'Alice', 'age': 30, 'email': 'alice@email.com'}
un_autre_dictionnaire = dict(nom='Bob', age=40, email='bob@email.com')
print(un_autre_dictionnaire)
{'nom': 'Bob', 'age': 40, 'email': 'bob@email.com'}

Pour accéder aux valeurs en utilisant leurs clés correspondantes :

print(mon_dictionnaire["nom"]) 
print(mon_dictionnaire["age"]) 
Alice
30

Pour modifier les valeurs associées aux clés :

mon_dictionnaire["age"] = 31
print(mon_dictionnaire["age"]) 
31

Pour ajouter de nouvelles paires clé-valeur au dictionnaire :

mon_dictionnaire["adresse"] = "123 rue du Paradis"
print(mon_dictionnaire)
{'nom': 'Alice', 'age': 31, 'email': 'alice@email.com', 'adresse': '123 rue du Paradis'}

Pour supprimer une paire clé-valeur, utilisez le mot-clé del :

del mon_dictionnaire["adresse"]
print(mon_dictionnaire)
{'nom': 'Alice', 'age': 31, 'email': 'alice@email.com'}

Pour afficher la taille d'un dictionnaire :

print(len(mon_dictionnaire))
3

Itérer sur les clés

for cle in mon_dictionnaire:
    print(cle)
nom
age
email

autre méthode :

cles = mon_dictionnaire.keys()
print(cles)  
# Pour convertir en liste
liste_cles = list(cles)
print(liste_cles)  
dict_keys(['nom', 'age', 'email'])
['nom', 'age', 'email']

Itérer sur les valeurs

for valeur in mon_dictionnaire.values():
    print(valeur)
Alice
31
alice@email.com

autre méthode :

valeurs = mon_dictionnaire.values()
print(valeurs) 

# Pour convertir en liste
liste_valeurs = list(valeurs)
print(liste_valeurs) 
dict_values(['Alice', 31, 'alice@email.com'])
['Alice', 31, 'alice@email.com']

Itérer sur les clés et les valeurs

for cle, valeur in mon_dictionnaire.items():
    print(f"{cle} : {valeur}")
nom : Alice
age : 30
email : alice@email.com

Pour rechercher une valeur dans un dictionnaire en Python

"Alice" in mon_dictionnaire.values()
True
valeur_recherchée = "Alice"

for valeur in mon_dictionnaire.values():
    if valeur == valeur_recherchée:
        print(f"La valeur '{valeur_recherchée}' a été trouvée dans le dictionnaire.")
        break
La valeur 'Alice' a été trouvée dans le dictionnaire.
autre méthode :
valeur_recherchée = "Alice"

for cle, valeur in mon_dictionnaire.items():
    if valeur == valeur_recherchée:
        print(f"La valeur '{valeur_recherchée}' a été trouvée dans le dictionnaire avec la clé '{cle}'.")
        break
La valeur 'Alice' a été trouvée dans le dictionnaire avec la clé 'nom'.

2) Index

L'index est un autre concept en programmation, généralement associé aux tableaux ou aux listes.
Une liste est une collection ordonnée d'éléments, qui peut contenir des données de types différents.
Contrairement aux dictionnaires, les éléments d'un tableau sont accessibles par leur position, ou index, qui commence généralement par 0.

ma_liste = ['Alice',  31, 'alice@email.com', 'adresse']

print(ma_liste[0])
print(ma_liste[1])
print(ma_liste[-1])
print(ma_liste[-2])
print(ma_liste[1:]) # Afficher la liste à partir de l'indice 1
print(ma_liste[1:3]) # Afficher la liste à partir de l'indice 1 à 2 !!!
print(ma_liste[:-1]) # Afficher la liste sauf le dernier
Alice
31
adresse
alice@email.com
[31, 'alice@email.com', 'adresse']
[31, 'alice@email.com']
['Alice', 31, 'alice@email.com']

Pour changer une valeur dans la liste, assignez une nouvelle valeur à l'index correspondant.

ma_liste[0] = 10
print(ma_liste)
[10, 31, 'alice@email.com', 'adresse']

Pour afficher la taille d'une liste :

print(len(mon_liste))

Pour rechercher une valeur dans une liste en Python

'alice@email.com' in ma_liste
True

autre méthode :

valeur_recherchée = 'alice@email.com'
for i, valeur in enumerate(ma_liste):
    if valeur == valeur_recherchée:
        print(f"La valeur {valeur_recherchée} est trouvée à l'indice {i}.")
        break
La valeur alice@email.com est trouvée à l'indice 2.

3) Différence entre clés et index

La principale différence entre les clés d'un dictionnaire et les index d'une liste est que les clés sont généralement des chaînes de caractères qui représentent le sens sémantique des valeurs, tandis que les index sont des entiers qui représentent la position des valeurs dans la liste.

4) P-uplets nommés

En Python, les p-uplets nommés peuvent être implémentés par des dictionnaires ou par collections.namedtuple.

personnes = {
    'Alice': {'age': 30, 'email': 'alice@email.com'},
    'Bob': {'age': 40, 'email': 'bob@email.com'},
    'Charlie': {'age': 50, 'email': 'charlie@email.com'}
}


print(personnes['Alice'])
print(personnes['Alice']['age'])
{'age': 30, 'email': 'alice@email.com'}
30
from collections import namedtuple

# Définition du p-uplet nommé
Personne = namedtuple('Personne', ['nom', 'age', 'email'])

# Création d'une instance du p-uplet nommé
alice = Personne(nom='Alice', age=30, email='alice@email.com')
bob = Personne(nom='Bob', age=40, email='bob@email.com')
charlie = Personne(nom='Charlie', age=50, email='charlie@email.com')

print(alice)
print(alice.nom)
print(alice.age)
Personne(nom='Alice', age=30, email='alice@email.com')
Alice
30

Les données EXIF d'une image

Les données EXIF (Exchangeable Image File Format) d'une image peuvent être représentées sous forme de dictionnaire.
Par exemple, pour lire les données EXIF d'une image, on utilise la bibliothèque PIL.

Pieds

Télécharger l'image ci-dessus est collée importe là dans ce fichier basthon

from PIL import Image
image = Image.open("Pieds_du_femme_dans_le_metro.jpg")
exif_data = image._getexif()
print(exif_data)
{34853: {0: b'\x02\x02\x00\x00', 1: 'N', 2: (41.0, 24.0, 9.66), 3: 'E', 4: (2.0, 9.0, 9.9), 18: 'WGS-84'}, 296: 2, 34665: 202, 271: 'Sony', 272: 'G8441', 305: '47.1.A.16.20_0_a600', 274: 1, 306: '2018:10:25 09:07:03', 282: 72.0, 283: 72.0, 36864: b'0231', 37121: b'\x01\x02\x03\x00', 37377: 5.64, 36867: '2018:10:25 09:07:03', 36868: '2018:10:25 09:07:03', 37380: 0.0, 40960: b'0100', 37383: 5, 37384: 0, 37385: 16, 37386: 4.4, 40961: 1, 40962: 1024, 41988: 1.0, 41990: 0, 41996: 0, 37520: '991195', 37521: '991195', 37522: '991195', 40963: 576, 33434: 0.02, 33437: 2.0, 41985: 0, 34855: 500, 41986: 0, 41987: 0}
for cle, valeur in exif_data.items():
    print(f"{cle} : {valeur}")
34853 : {0: b'\x02\x02\x00\x00', 1: 'N', 2: (41.0, 24.0, 9.66), 3: 'E', 4: (2.0, 9.0, 9.9), 18: 'WGS-84'}
296 : 2
34665 : 202
271 : Sony
272 : G8441
305 : 47.1.A.16.20_0_a600
274 : 1
306 : 2018:10:25 09:07:03
282 : 72.0
283 : 72.0
36864 : b'0231'
37121 : b'\x01\x02\x03\x00'
37377 : 5.64
36867 : 2018:10:25 09:07:03
36868 : 2018:10:25 09:07:03
37380 : 0.0
40960 : b'0100'
37383 : 5
37384 : 0
37385 : 16
37386 : 4.4
40961 : 1
40962 : 1024
41988 : 1.0
41990 : 0
41996 : 0
37520 : 991195
37521 : 991195
37522 : 991195
40963 : 576
33434 : 0.02
33437 : 2.0
41985 : 0
34855 : 500
41986 : 0
41987 : 0
exif_data[34853]
{0: b'\x02\x02\x00\x00', 1: 'N', 2: (41.0, 24.0, 9.66), 3: 'E', 4: (2.0, 9.0, 9.9), 18: 'WGS-84'}
exif_data[34853][1],exif_data[34853][2],exif_data[34853][3],exif_data[34853][4]
('N', (41.0, 24.0, 9.66), 'E', (2.0, 9.0, 9.9))

Retour au sommaire

1.4 - Arbres ✔

Structures de données

Arbres : structures hiérarchiques.

Arbres binaires : nœuds, racines,feuilles,sous-arbres gauches,sous-arbres droits.

Capacités Attendue :

Commentaires :
On fait le lien avec la rubrique « algorithmique »

Algorithmes sur les arbres binaires et sur les arbres binaires de recherche.

Capacités Attendue :

Commentaires :

Les arbres en informatique sont des structures de données qui représentent une hiérarchie sous forme d'une collection de nœuds reliés par des arêtes.
Ces structures évoquent l'image d'un arbre vu à l'envers, où la racine se situe en haut et les branches s'étendent vers le bas. Chaque nœud a un parent (à l'exception de la racine) et zéro ou plusieurs enfants.
La nature hiérarchique des arbres les rend extrêmement utiles pour représenter des structures de données organisées de manière non linéaire.
Par exemples :

Les arbres sont donc une pierre angulaire de l'organisation et du traitement des données en informatique.

1. Arbres binaires : nœuds, racines,feuilles,sous-arbres gauches,sous-arbres droits.

Définition :

Un arbre binaire est une structure de données hiérarchique dans laquelle :

Un arbre binaire de recherche est une structure de données hiérarchique dans laquelle on a aussi :

Un arbre binaire est dit complet si tous ses niveaux, à l'exception peut-être du dernier, sont entièrement remplis, et tous les nœuds du dernier niveau sont aussi à gauche que possible.

class Noeud:
    def __init__(self, valeur, gauche=None, droit=None):
        self.valeur = valeur
        self.gauche = gauche
        self.droit = droit

racine = Noeud(40)
n20 = Noeud(20)
n60 = Noeud(60)
n10 = Noeud(10)
n30 = Noeud(30)
n50 = Noeud(50)
n70 = Noeud(76)
n5 = Noeud(5)
n15 = Noeud(15)
n25 = Noeud(25)
n35 = Noeud(35)
n45 = Noeud(45)
n75 = Noeud(75)
n80 = Noeud(80)

# Assemblage de l'arbre
racine.gauche = n20
racine.droit = n60
n20.gauche = n10
n20.droit = n30
n60.gauche = n50
n60.droit = n70
n10.gauche = n5
n10.droit = n15
n30.gauche = n25
n30.droit = n35
n50.gauche = n45
n70.gauche = n75
n70.droit = n80


def est_arbre_binaire(noeud, gauche=None, droite=None):
    if noeud is None:
        return True
    
    if gauche and noeud.valeur <= gauche.valeur:
        print(f"Problème : {noeud.valeur} <= {gauche.valeur} !!!")
        return False
    
    if droite and noeud.valeur >= droite.valeur:
        print(f"Problème : {noeud.valeur} => {droite.valeur}  !!!")
        return False
    
    return (est_arbre_binaire(noeud.gauche, gauche, noeud) and 
            est_arbre_binaire(noeud.droit, noeud, droite))
    
def verifier(racine):
    if not est_arbre_binaire(racine):
        print("L'arbre n'est pas un arbre binaire de recherche valide.")
    else:
        print("L'arbre est un arbre binaire de recherche valide.")
        
verifier(racine)

# Dessin de l'arbre
from graphviz import Digraph
from IPython.display import display, SVG

def dessiner_arbre(noeud, graph=None):
    if graph is None:
        graph = Digraph()
    
    if noeud is not None:
        graph.node(str(noeud.valeur))
        if noeud.gauche:
            graph.node(str(noeud.gauche.valeur))
            graph.edge(str(noeud.valeur), str(noeud.gauche.valeur))
            dessiner_arbre(noeud.gauche, graph)
        if noeud.droit:
            graph.node(str(noeud.droit.valeur))
            graph.edge(str(noeud.valeur), str(noeud.droit.valeur))
            dessiner_arbre(noeud.droit, graph)
    return graph

def arbre_visuel(racine) :
    graph = dessiner_arbre(racine)
    raw_data = graph.pipe(format='svg')
    raw_text = raw_data.decode('utf-8')
    display(SVG(data=raw_text))

arbre_visuel(racine)

Définitions Métriques :

Exemple : Dans notre arbre ci-dessus :

# Fonction pour calculer la taille de l'arbre
def taille_arbre(noeud):
    if noeud is None:
        return 0
    return 1 + taille_arbre(noeud.gauche) + taille_arbre(noeud.droit)

# Fonction pour calculer la hauteur de l'arbre
def hauteur_arbre(noeud):
    if noeud is None:
        return -1  # On commence à -1 pour que le nœud racine n'ajoute pas 1 à la hauteur totale
    return 1 + max(hauteur_arbre(noeud.gauche), hauteur_arbre(noeud.droit))


# Calcul de la taille et de la hauteur
taille = taille_arbre(racine)
hauteur = hauteur_arbre(racine)

# Affichage
print(f"La taille de l'arbre est de {taille} nœuds.")
print(f"La hauteur de l'arbre est de {hauteur}.")

Rappel : Pour tout $x>0$, on a : $log_2(x)=\frac{ln(x)}{ln(2)}$

Formules :

Démonstration :

Dans le pire des cas, chaque nœud de l'arbre a seulement un enfant.
Cela donne une structure "en ligne", où chaque nouveau nœud ajoute une unité à la hauteur de l'arbre.
Par conséquent, si vous avez $n$ nœuds, la hauteur maximale de l'arbre serait $n-1$.

Dans le meilleur des cas, l'arbre est parfaitement équilibré, c'est-à-dire un arbre binaire complet. Chaque niveau $k$ de l'arbre contient $2^k$ nœuds. Par conséquent, la hauteur minimale $h$ est déterminée par le nombre total de nœuds $n$ que l'on peut distribuer sur ces niveaux :

$\sum_{k=0}^{h} 2^k = n$
$\iff 1 + 2 + 2^2 + 2^3 + \cdots + 2^h = n$
$\iff \frac{2^{h+1} - 1}{2 - 1} = n$
$\iff 2^{h+1} - 1 = n$
$\iff 2^{h+1} = n + 1$
$\iff h+1 = \log_2(n + 1)$
$\iff h = \log_2(n + 1) - 1$
Le résultat n'est pas exactement $log_2(n)$, mais il est proche et donne un ordre de grandeur pour la hauteur minimale.

Défintion : Parcours en profondeur d'un arbre

Dans un parcours en profondeur (Depth-First Search ou DFS en anglais), vous partez de la racine de l'arbre et explorez aussi loin que possible le long de chaque branche avant de revenir en arrière. Il existe trois types de parcours en profondeur :

             A
          /     \
        B         C
      /   \      /   \
     D     E    F     G
    / \   / \  / \   / \
   H   I J  K L   M N   O

Définition : Parcours en largeur d'un arbre

Dans un parcours en largeur (Breadth-First Search ou BFS en anglais), vous visitez tous les nœuds d'un niveau donné avant de passer au niveau suivant. Cela commence généralement à la racine. Exemple Parcours en largeur : A, B, C, D, E, F, G, H, I, J, K, L, M, N, O

# Parcours en largeur
def parcours_en_largeur(racine):
    file, result = [racine], []
    while file:
        noeud = file.pop(0)
        result.append(noeud.valeur)
        if noeud.gauche:
            file.append(noeud.gauche)
        if noeud.droit:
            file.append(noeud.droit)
    return "".join(result)

# Parcours en profondeur
def Préfixe(noeud):
    if noeud is not None:
        return str(noeud.valeur) + Préfixe(noeud.gauche) + Préfixe(noeud.droit)
    else:
        return ""

def Infixe(noeud):
    if noeud is not None:
        return Infixe(noeud.gauche) + str(noeud.valeur) + Infixe(noeud.droit)
    else:
        return ""

def Suffixe(noeud):
    if noeud is not None:
        return Suffixe(noeud.gauche) + Suffixe(noeud.droit) + str(noeud.valeur) 
    else:
        return ""

racine_lettre = Noeud('A', Noeud('B', Noeud('D', Noeud('H'), Noeud('I')), Noeud('E', Noeud('J'), Noeud('K'))), Noeud('C', Noeud('F', Noeud('L'), Noeud('M')), Noeud('G', Noeud('N'), Noeud('O'))))

print("Parcours en largeur : ", parcours_en_largeur(racine_lettre))
print("Parcours en Préfixe : ", Préfixe(racine_lettre))
print("Parcours en Infixe  : ", Infixe(racine_lettre))
print("Parcours en Suffixe : ", Suffixe(racine_lettre))

arbre_visuel(racine_lettre)

Rechercher une clé dans un arbre de recherche

La recherche dans un arbre de recherche binaire commence à la racine et descend dans l'arbre en comparant la clé à celle dans le nœud actuel. Puis on se dirige soit vers le sous-arbre gauche, soit vers le sous-arbre droit selon que la clé est plus petite ou plus grande que celle du nœud actuel.

def recherche(noeud, cle):
    if noeud is None:
        print(f"Clé {cle} non trouvée dans l'arbre.")
        return
    if cle == noeud.valeur:
        print(f"Clé {cle} trouvée dans l'arbre.")
        return 
    elif cle < noeud.valeur:
        return recherche(noeud.gauche, cle)
    else:
        return recherche(noeud.droit, cle)

arbre_visuel(racine)
recherche(racine, 76)

Insérer une clé.

# Insérer un nœud dans l'arbre
n42 = Noeud(42)
n47 = Noeud(47)
n45.gauche = n42
n45.droit = n47

# Affichage des parcours
arbre_visuel(racine)
verifier(racine)

La recherche dans un arbre de recherche équilibré est de coût logarithmique.

complexité $O(n)$ et complexité $O(log (n))$

La notation $O(n)$ et $O(log n)$ expriment comment la performance d'un algorithme évolue en fonction de la taille de l'entrée n.

La complexité $O(n)$ signifie que le temps d'exécution de votre algorithme est $T(n)$=a $n$+b, où T(n) est le temps d'exécution et a et b sont des constantes.

La complexité $O(log (n))$ signifie que le temps d'exécution de votre algorithme est $T(n)$=a $log(n)$+b, où T(n) est le temps d'exécution et a et b sont des constantes.

La notation $O(log (n))$ est un indicateur d'algorithmes très efficaces, plus que $O(n)$

La recherche dans un arbre binaire de recherche (ABR) équilibré est effectivement de coût logarithmique en termes de complexité temporelle. Plus précisément, cette complexité est de $O(log(⁡n))$, où $n$ est le nombre de nœuds dans l'arbre.

L'équilibrage de l'arbre est crucial pour maintenir cette performance optimale. Si l'arbre est déséquilibré, la complexité de recherche peut se dégrader vers $O(n)$ dans le pire des cas, où $n$ est également le nombre de nœuds.

Il existe des variantes d'arbres binaires de recherche qui s'auto-équilibrent pour maintenir cette complexité logarithmique, tels que les arbres AVL et les arbres rouge-noir. Ces structures s'assurent que l'arbre reste équilibré après chaque insertion ou suppression, garantissant ainsi que les opérations de recherche, d'insertion et de suppression s'exécutent toutes en temps logarithmique.

L'arbre AVL (Adelson-Velsky et Landis) est un arbre binaire de recherche qui maintient l'équilibre en gardant la différence de hauteur entre les sous-arbres gauche et droit de chaque nœud à 1 ou 0.

# Création de nouvel arbre binaire de recherche
racine2 = Noeud(30)
n20 = Noeud(20)
n40 = Noeud(40)
n10 = Noeud(10)
n5 = Noeud(5)

racine2.gauche = n20
racine2.droit = n40
n20.gauche = n10
n10.gauche = n5

arbre_visuel(racine2)
class Noeud:
    def __init__(self, valeur):
        self.valeur = valeur
        self.hauteur = 1
        self.gauche = None
        self.droit = None

def get_hauteur(n):
    if not n:
        return 0
    return n.hauteur

def get_balance(n):
    if not n:
        return 0
    return get_hauteur(n.gauche) - get_hauteur(n.droit)

def rotation_droite(y):
    x = y.gauche
    T3 = x.droit
    x.droit = y
    y.gauche = T3
    y.hauteur = max(get_hauteur(y.gauche), get_hauteur(y.droit)) + 1
    x.hauteur = max(get_hauteur(x.gauche), get_hauteur(x.droit)) + 1
    return x

def rotation_gauche(x):
    y = x.droit
    T2 = y.gauche
    y.gauche = x
    x.droit = T2
    x.hauteur = max(get_hauteur(x.gauche), get_hauteur(x.droit)) + 1
    y.hauteur = max(get_hauteur(y.gauche), get_hauteur(y.droit)) + 1
    return y

def equilibrer_avl_auto(n):
    if not n:
        return None

    n.gauche = equilibrer_avl_auto(n.gauche)
    n.droit = equilibrer_avl_auto(n.droit)

    n.hauteur = 1 + max(get_hauteur(n.gauche), get_hauteur(n.droit))
    balance = get_balance(n)

    if balance > 1:
        if get_balance(n.gauche) >= 0:
            return rotation_droite(n)
        else:
            n.gauche = rotation_gauche(n.gauche)
            return rotation_droite(n)

    if balance < -1:
        if get_balance(n.droit) <= 0:
            return rotation_gauche(n)
        else:
            n.droit = rotation_droite(n.droit)
            return rotation_gauche(n)

    return n

# Votre arbre initial
racine2 = Noeud(30)
n20 = Noeud(20)
n40 = Noeud(40)
n10 = Noeud(10)
n5 = Noeud(5)

racine2.gauche = n20
racine2.droit = n40
n20.gauche = n10
n10.gauche = n5

print("Arbre d'origine :")
arbre_visuel(racine2)
racine2 = equilibrer_avl_auto(racine2) # Équilibrage automatique de l'arbre
print("Arbre équilibré :")
arbre_visuel(racine2)

Retour au sommaire

1.5 - Graphes ✔

Structures de données

Graphes : structures relationnelles.

Sommets, arcs, arêtes, graphes orientés ou non orientés.

Capacités Attendue :

Commentaires :

1. Notion de graphe et vocabulaire

Le concept de graphe permet de résoudre de nombreux problèmes en mathématiques comme en informatique. C'est un outil de représentation très courant, et nous l'avons déjà rencontré à plusieurs reprises, en particulier lors de l'étude de réseaux.

Le problème des sept ponts de Königsberg est connu pour être à l'origine de la topologie et de la théorie des graphes. Résolu par Leonhard Euler en 1735, ce problème mathématique se présente de la façon suivante :

La ville de Königsberg (aujourd'hui Kaliningrad) est construite autour de deux îles situées sur le Pregel et reliées entre elles par un pont. Six autres ponts relient les rives de la rivière à l'une ou l'autre des deux îles, comme représentés sur le plan ci-dessus. Le problème consiste à déterminer s'il existe ou non une promenade dans les rues de Königsberg permettant, à partir d'un point de départ au choix, de passer une et une seule fois par chaque pont, et de revenir à son point de départ, étant entendu qu'on ne peut traverser le Pregel qu'en passant sur les ponts.

1.1 Exemples de situations : Réseau informatique, Réseau de transport, Réseau social...

Une multitude de problèmes concrets d'origines très diverses peuvent donner lieu à des modélisations par des graphes : c'est donc une structure essentielle en sciences, qui requiert un formalisme mathématique particulier que nous allons découvrir.

L'étude de la théorie des graphes est un champ très vaste des mathématiques : nous allons surtout nous intéresser à l'implémentation en Python d'un graphe et à différents problèmes algorithmiques qui se posent dans les graphes.

1.2 Vocabulaire

En général, un graphe est un ensemble d'objets, appelés sommets ou nœuds (vertex or nodes en anglais) reliés par des arêtes ou arcs (edges en anglais). Un graphe peut être non-orienté ou orienté .

1.2.1 Graphe non-orienté

Dans un graphe non-orienté, les arêtes peuvent être empruntées dans les deux sens, et une chaîne est une suite de sommets reliés par des arêtes, comme C - B - A - E par exemple. La longueur de cette chaîne est alors 3, soit le nombre d'arêtes.

Les sommets B et E sont adjacents au sommet A, ce sont les voisins de A.

Exemple de graphe non-orienté : le graphe des relations d'un individu sur Facebook est non-orienté.

1.2.2 Graphe orienté

Dans un graphe orienté, les arrêtes ne peuvent être empruntés que dans le sens de la flèche, et un chemin est une suite de sommets reliés par des arcs, comme B → C → D → E par exemple.

Les sommets C et D sont adjacents au sommet B (mais pas A !), ce sont les voisins de B.

Exemple de graphe orienté : le graphe des relations d'un individu sur Twitter est orienté, car on peut suivre une personne sans que elle nous suive.

1.2.3 Graphe pondéré

Un graphe est pondéré (ou valué) si on attribue à chaque arête une valeur numérique (la plupart du temps positive), qu'on appelle mesure, poids, coût ou valuation.

Par exemple:

1.2.4 Connexité

Un graphe est connexe si n'importe quelle paire de sommets peut toujours être reliée par une chaîne.

Par exemple, le graphe précédent est connexe.

Mais le suivant ne l'est pas: il n'existe pas de chaîne entre les sommets A et F par exemple.

Il possède cependant deux composantes connexes : le sous-graphe composé des sommets A, B, C, D et E d'une part et le sous-graphe composé des sommets F, G et H.

2. Modélisations d'un graphe

2.1 Représentation par matrice d'adjacence

Principe : On représente les arêtes (ou les arcs) dans une matrice, c'est-à-dire un tableau à deux dimensions où on inscrit un 1 en ligne i et colonne j si les sommets de rang i et de rang j sont adjacents. Ce tableau s'appelle une matrice d'adjacence (on aurait très bien pu l'appeler aussi matrice de voisinage).

Graphe non orienté :
Graphe orienté :
Graphe pondéré :

2.2 Liste de successeurs, liste de prédécesseurs

Dans un graphe :

Exemple :

matrice_adjacence = [[0, 1, 0, 0, 0],
                     [0, 0, 1, 0, 1],
                     [0, 1, 0, 1, 0],
                     [0, 0, 0, 0, 0],
                     [1, 0, 0, 0, 0]]

noms_sommets = ['A', 'B', 'C', 'D' , 'E']

Liste des successeurs : {'A': ['B'], 'B': ['C', 'E'], 'C': ['B', 'D'], 'D': [], 'E': ['A']}
Liste des prédécesseurs : {'A': ['E'], 'B': ['A', 'C'], 'C': ['B'], 'D': ['C'], 'E': ['B']}

A partir de la liste des successeurs, dessiner un graph possible. De même avec la liste des prédécesseurs.

3. Création d'une classe Graphe pour les graphes avec la matrice adjacente

import numpy as np
import networkx as nx
import matplotlib.pyplot as plt

class Graphe_Matrice_Adjacente:
    def __init__(self, matrice_adjacence, noms_sommets):
        self.matrice_adjacence = np.array(matrice_adjacence)
        self.noms_sommets = noms_sommets
        
    def liste_successeurs(self):
        liste_succ = {}
        n = len(self.matrice_adjacence)
        for i in range(n):
            successeurs = []
            for j in range(n):
                if self.matrice_adjacence[i][j]:
                    successeurs.append(self.noms_sommets[j])
            liste_succ[self.noms_sommets[i]] = successeurs
        return liste_succ

    def liste_predecesseurs(self):
        liste_pred = {}
        n = len(self.matrice_adjacence)
        for j in range(n):
            predecesseurs = []
            for i in range(n):
                if self.matrice_adjacence[i][j]:
                    predecesseurs.append(self.noms_sommets[i])
            liste_pred[self.noms_sommets[j]] = predecesseurs
        return liste_pred

    def afficher(self, figsize=(3, 3)):
        plt.figure(figsize=figsize)
        G = nx.DiGraph()
        edge_labels = {}
        for i, nom_sommet in enumerate(self.noms_sommets):
            for j, poids in enumerate(self.matrice_adjacence[i]):
                if poids != 0:
                    G.add_edge(nom_sommet, self.noms_sommets[j], weight=poids)
                    edge_labels[(nom_sommet, self.noms_sommets[j])] = poids
        pos = nx.spring_layout(G)
        nx.draw(G, pos, with_labels=True, font_weight='bold', node_color='skyblue', font_size=12, node_size=500, arrows=True)
        nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
        plt.show()

        
# Création d'un graphe avec la matrice d'adjacence donnée
matrice_adjacence = [[0, 5, 8, 0],
                     [0, 0, 0, 0],
                     [0, 3, 0, 6],
                     [7, 0, 0, 0]]

noms_sommets = ['A', 'B', 'C', 'D']

# Instanciation de la classe Graphe_Matrice_Adjacente
graphe = Graphe_Matrice_Adjacente(matrice_adjacence, noms_sommets)
print("Liste des successeurs :",graphe.liste_successeurs())
print("Liste des prédécesseurs :",graphe.liste_predecesseurs())
graphe.afficher()
Liste des successeurs : {'A': ['B', 'C'], 'B': [], 'C': ['B', 'D'], 'D': ['A']}
Liste des prédécesseurs : {'A': ['D'], 'B': ['A', 'C'], 'C': ['A'], 'D': ['C']}

4. Que raconte le cours de math de terminale

Graphes - Maths-cours.fr

Retour au sommaire

2.1 - SQL Modèle relationnel ✔

NSI - Terminale - Bases de données

Modèle relationnel :

relation, attribut, domaine, clef primaire, clef étrangère, schéma relationnel

Capacités attendues : Identifier les concepts définissant le modèle relationnel.

Commentaires : Ces concepts permettent d’exprimer les contraintes d’intégrité (domaine, relation et référence).

1) Introduction

Les bases de données permettent de stocker des données. Pour manipuler les données présentes dans une base de données (écrire, lire ou encore modifier), il est nécessaire d'utiliser un type de logiciel appelé "système de gestion de base de données" très souvent abrégé en SGBD.

Par rapport à une base de données, le stockage de données dans un fichier de type CSV est beaucoup plus simple à mettre en place, mais aussi beaucoup plus limité : pas de contrôle d’accès, pas de redondance des données, pas de gestion des accès concurrents.

Il existe différents types de bases de données, par exemple, les bases de données hiérarchiques, les bases de données objet, les bases de données nosql ou bien encore les bases de données relationnelles. Les bases de données relationnelles sont le plus utilisées au monde, c'est ce type de base de données que nous allons étudier.

Les bases de données relationnelles ont été mises au point en 1970 par Edgar Franck Codd, informaticien britannique (1923-2003). Ces bases de données sont basées sur la théorie mathématique des ensembles.

2) Relation

La notion de relation est au coeur des bases de données relationnelles. Une relation peut être vue comme un tableau à 2 dimensions, composé d'un en-tête et d'un corps. Le corps est lui-même composé de t-uplets (lignes) et d'attributs (colonnes). L'en-tête contient les intitulés des attributs, le corps contient les données proprement dites. À noter que l'on emploie aussi le terme "table" à la place de "relation".

CREATE TABLE livres (
    id INTEGER PRIMARY KEY,
    titre TEXT,
    auteur TEXT,
    ann_publi INTEGER,
    note INTEGER
)
INSERT INTO livres (id, titre, auteur, ann_publi, note) 
VALUES 
(1, '1984', 'Orwell', 1949, 10),
(2, 'Dune', 'Herbert', 1965, 8),
(3, 'Fondation', 'Asimov', 1951, 9),
(4, 'Le meilleur des mondes', 'Huxley', 1931, 7),
(5, 'Fahrenheit 451', 'Bradbury', 1953, 7),
(6, 'Ubik', 'K.Dick', 1969, 9),
(7, 'Chroniques martiennes', 'Bradbury', 1950, 8),
(8, 'La nuit des temps', 'Barjavel', 1968, 7),
(9, 'Blade Runner', 'K.Dick', 1968, 8),
(10, 'Les Robots', 'Asimov', 1950, 9),
(11, 'La Planète des singes', 'Boulle', 1963, 8),
(12, 'Ravage', 'Barjavel', 1943, 8),
(13, 'Le Maître du Haut Château', 'K.Dick', 1962, 8),
(14, 'Le monde des Ā', 'Van Vogt', 1945, 7),
(15, 'La Fin de l’éternité', 'Asimov', 1955, 8),
(16, 'De la Terre à la Lune', 'Verne', 1865, 10)
SELECT * FROM livres

Se rappeler :

Relation : Dans le contexte d'une base de données relationnelle, une relation est essentiellement une table qui stocke des données. Elle est composée d'un ensemble de tuples (lignes) ayant des attributs (colonnes) définis.

Attribut : Un attribut est une propriété ou une caractéristique d'une entité, et il est représenté par une colonne dans une table. Par exemple, pour une table "Personne", "nom" et "prénom" pourraient être des attributs.

3) Domaine

Pour chaque attribut d'une relation, il est nécessaire de définir un domaine : Le domaine d'un attribut donné correspond à un ensemble fini ou infini de valeurs admissibles. Par exemple, le domaine de l'attribut "id" correspond à l'ensemble des entiers (noté INT) : la colonne "id" devra obligatoirement contenir des entiers. Autre exemple, le domaine de l'attribut "titre" correspond à l'ensemble des chaînes de caractères (noté TEXT). Dernier exemple, le domaine de l'attribut "note" correspond à l'ensemble des entiers positifs.

Au moment de la création d'une relation, il est nécessaire de renseigner le domaine de chaque attribut(voir ci-dessus). Le SGBD s'assure qu'un élément ajouté à une relation respecte bien le domaine de l'attribut correspondant : si par exemple vous essayez d'ajouter une note non entière (par exemple 8.5), le SGBD signalera cette erreur et n'autorisera pas l'écriture de cette nouvelle donnée.

4) Clef primaire

Autre contrainte très importante dans les bases de données relationnelles, une relation ou table ne peut pas contenir 2 t-uplets identiques. C'est-à-dire 2 lignes identiques.

Afin d'être sûr de respecter cette contrainte des t-uplets identiques, on définit la notion de "clef primaire".

Une clef primaire est un attribut dont la valeur permet d'identifier de manière unique un t-uplet de la relation. Autrement dit, si un attribut est considéré comme clé primaire, on ne doit pas trouver dans toute la relation 2 fois la même valeur pour cet attribut.

Si on se réfère à l'exemple de la relation ci-dessous :

SELECT * FROM livres

L'attribut "note" peut-il jouer le rôle de clé primaire ? Non, car il est possible de trouver 2 fois la même note.

L'attribut "ann_publi" peut-il jouer le rôle de clé primaire ? Non, car il est possible de trouver 2 fois la même année.

L'attribut "auteur" peut-il jouer le rôle de clé primaire ? Non, car il est possible de trouver 2 fois le même auteur.

L'attribut "titre" peut-il jouer le rôle de clé primaire ? A priori oui, car l'attribut "titre" ne comporte pas 2 fois le même titre de roman. Mais, ce n'est pas forcément une bonne idée, car il est tout à fait possible d'avoir un même titre pour 2 romans différents.

Il nous reste donc l'attribut "id". En fait, l'attribut "id" ("id" comme "identifiant") a été placé là pour jouer le rôle de clé primaire. En effet, à chaque fois qu'un roman est ajouté à la relation, son "id" correspond à l'incrémentation de l'id (id du nouveau=id de l'ancien+1) du roman précédemment ajouté. Il est donc impossible d'avoir deux romans avec le même id. Ajouter un attribut "id" afin qu'il puisse jouer le rôle de clé primaire est une pratique courante (mais non obligatoire) dans les bases de données relationnelles. Dans le cas précis qui nous intéresse, il aurait été possible de ne pas utiliser d'attribut "id", car chaque livre édité possède un numéro qui lui est propre : l'ISBN, cet ISBN aurait donc pu jouer le rôle de clé primaire.

À noter qu'en toute rigueur, une clé primaire peut être constituée de plusieurs attributs, par exemple le couple "auteur" + "titre" pourrait jouer le rôle de clé primaire (à moins qu'un auteur écrive 2 romans différents, mais portant tous les deux le même titre), mais nous n'étudierons pas cet aspect des choses ici.

Se rappeler :

clef primaire : Une clef primaire est un attribut dont la valeur permet d'identifier de manière unique un t-uplet de la relation. Autrement dit, si un attribut est considéré comme clé primaire, on ne doit pas trouver dans toute la relation 2 fois la même valeur pour cet attribut.

5) clef étrangère

a) Duplication des données

Supprimons la relation livres, et créons une plus complète.

DROP TABLE livres; 
CREATE TABLE livres (
    id INT PRIMARY KEY,
    titre VARCHAR(255),
    nom_auteur VARCHAR(255),
    prenom_auteur VARCHAR(255),
    date_nai_auteur INT,
    langue_ecriture_auteur VARCHAR(255),
    ann_publi INT,
    note INT
);
INSERT INTO livres (id, titre, nom_auteur, prenom_auteur, date_nai_auteur, langue_ecriture_auteur, ann_publi, note)
VALUES
(1, '1984', 'Orwell', 'George', 1903, 'anglais', 1949, 10),
(2, 'Dune', 'Herbert', 'Frank', 1920, 'anglais', 1965, 8),
(3, 'Fondation', 'Asimov', 'Isaac', 1920, 'anglais', 1951, 9),
(4, 'Le meilleur des mondes', 'Huxley', 'Aldous', 1894, 'anglais', 1931, 7),
(5, 'Fahrenheit 451', 'Bradbury', 'Ray', 1920, 'anglais', 1953, 7),
(6, 'Ubik', 'K.Dick', 'Philip', 1928, 'anglais', 1969, 9),
(7, 'Chroniques martiennes', 'Bradbury', 'Ray', 1920, 'anglais', 1950, 8),
(8, 'La nuit des temps', 'Barjavel', 'René', 1911, 'français', 1968, 7),
(9, 'Blade Runner', 'K.Dick', 'Philip', 1928, 'anglais', 1968, 8),
(10, 'Les Robots', 'Asimov', 'Isaac', 1920, 'anglais', 1950, 9),
(11, 'La Planète des singes', 'Boulle', 'Pierre', 1912, 'français', 1963, 8),
(12, 'Ravage', 'Barjavel', 'René', 1911, 'français', 1943, 8),
(13, 'Le Maître du Haut Château', 'K.Dick', 'Philip', 1928, 'anglais', 1962, 8),
(14, 'Le monde des Ā', 'Van Vogt', 'Alfred Elton', 1912, 'anglais', 1945, 7),
(15, 'La Fin de l’éternité', 'Asimov', 'Isaac', 1920, 'anglais', 1955, 8),
(16, 'De la Terre à la Lune', 'Verne', 'Jules', 1828, 'français', 1865, 10);
SELECT * FROM livres

Nous avons un peu enrichir la relation LIVRES en ajoutant des informations supplémentaires sur les auteurs. Nous avons ajouté 3 attributs ("prenom_auteur", "date_nai_auteur" et "langue_ecriture_auteur"). Nous avons aussi renommé l'attribut "auteur" en "nom_auteur".

Comme vous l'avez peut-être remarqué, il y a pas mal d'informations dupliquées, par exemple, on retrouve 3 fois "K.Dick Philip 1928 anglais", même chose pour "Asimov Isaac 1920 anglais"...Cette duplication est-elle indispensable ? Non ! Est-elle souhaitable ? Non plus ! En effet, dans une base de données, on évite autant que possible de dupliquer l'information (sauf à des fins de sauvegarde, mais ici c'est toute autre chose). Si nous dupliquons autant de données inutilement c'est que notre structure ne doit pas être la bonne ! Mais alors, comment faire pour avoir aussi des informations sur les auteurs des livres ?

b) Notion de clé étrangère

La solution est relativement simple : travailler avec 2 relations au lieu d'une seule et créer un "lien" entre ces 2 relations :

CREATE TABLE AUTEURS (
    id INT PRIMARY KEY,
    nom VARCHAR(255),
    prenom VARCHAR(255),
    ann_naissance INT,
    langue_ecriture VARCHAR(255)
);
INSERT INTO AUTEURS (id, nom, prenom, ann_naissance, langue_ecriture)
VALUES
(1, 'Orwell', 'George', 1903, 'anglais'),
(2, 'Herbert', 'Frank', 1920, 'anglais'),
(3, 'Asimov', 'Isaac', 1920, 'anglais'),
(4, 'Huxley', 'Aldous', 1894, 'anglais'),
(5, 'Bradbury', 'Ray', 1920, 'anglais'),
(6, 'K.Dick', 'Philip', 1928, 'anglais'),
(7, 'Barjavel', 'René', 1911, 'français'),
(8, 'Boulle', 'Pierre', 1912, 'français'),
(9, 'Van Vogt', 'Alfred Elton', 1912, 'anglais'),
(10, 'Verne', 'Jules', 1828, 'français');
SELECT * FROM AUTEURS

Supprimons la relation livres

DROP TABLE livres; 

Créons une nouvelle relation LIVRES en remplacé l'attribut "auteur" par un attribut "id_auteur".

CREATE TABLE livres (
    id INT PRIMARY KEY,
    titre VARCHAR(255),
    id_auteur INT,
    ann_publi INT,
    note INT,
    FOREIGN KEY (id_auteur) REFERENCES AUTEURS(id)
);
INSERT INTO livres (id, titre, id_auteur, ann_publi, note)
VALUES
(1, '1984', 1, 1949, 10),
(2, 'Dune', 2, 1965, 8),
(3, 'Fondation', 3, 1951, 9),
(4, 'Le meilleur des mondes', 4, 1931, 7),
(5, 'Fahrenheit 451', 5, 1953, 7),
(6, 'Ubik', 6, 1969, 9),
(7, 'Chroniques martiennes', 5, 1950, 8),
(8, 'La nuit des temps', 7, 1968, 7),
(9, 'Blade Runner', 6, 1968, 8),
(10, 'Les Robots', 3, 1950, 9),
(11, 'La Planète des singes', 8, 1963, 8),
(12, 'Ravage', 7, 1943, 8),
(13, 'Le Maître du Haut Château', 6, 1962, 8),
(14, 'Le monde des Ā', 9, 1945, 7),
(15, 'La Fin de l’éternité', 3, 1955, 8),
(16, 'De la Terre à la Lune', 10, 1865, 10);
SELECT * FROM livres
SELECT * FROM AUTEURS

Nous avons créé une relation AUTEURS et nous avons modifié la relation livres : nous avons remplacé l'attribut "auteur" par un attribut "id_auteur".

Comme vous l'avez sans doute remarqué, l'attribut "id_auteur" de la relation LIVRES permet de créer un lien avec la relation AUTEURS. "id_auteur" correspond à l'attribut "id" de la relation AUTEURS. L'introduction d'une relation AUTEURS et la mise en place de liens entre cette relation et la relation LIVRES permettent d'éviter la {redondance d'informations.

Pour établir un lien entre 2 relations livres et AUTEURS, on ajoute à livres un attribut x qui prendra les valeurs de la clé primaire de AUTEURS. Cet attribut x est appelé clef étrangère (l'attribut correspond à la clef primaire d'une autre table, d'où le nom).

Dans l'exemple ci-dessus, l'attribut "id_auteur" de la relation LIVRES permet bien d'établir un lien entre la relation livres et la relation AUTEURS, "id_auteur" correspond bien à la clé primaire de la relation AUTEURS, conclusion : "id_auteur" est une clef étrangère.

Pour préserver l'intégrité d'une base de données, il est important de bien vérifier que toutes les valeurs de la clé étrangère correspondent bien à des valeurs présentes dans la clé primaire (nous aurions un problème d'intégrité de la base de données si une valeur de l'attribut "id_auteur" de la relation LIVRES ne correspondait à aucune valeur de la clé primaire de la relation AUTEURS). Certains SGBD ne vérifient pas cette contrainte (ne renvoient aucune erreur en cas de problème), ce qui peut provoquer des comportements erratiques.

6) schéma relationnel

Dernière définition, on appelle schéma relationnel l'ensemble des relations présentes dans une base de données. Quand on vous demande le schéma relationnel d'une base de données, il est nécessaire de fournir les informations suivantes :

Voici un exemple pour les relations LIVRES et AUTEURS :

AUTEURS(id : INT, nom : TEXT, prenom : TEXT, ann_naissance : INT, langue_ecriture : TEXT)

LIVRES(id : INT, titre : TEXT, #id_auteur : INT, ann_publi : INT, note : INT)

Les attributs soulignés sont des clés primaires, le # signifie que l'on a une clé étrangère.

source : https://dav74.github.io/site_nsi_term/c2c/

Retour au sommaire

2.2 - SQL Requêtes ✔

NSI - Terminale - Bases de données

Langage SQL : requêtes d’interrogation et de mise à jour d’une base de données

Capacités attendues :

Commentaires :
On peut utiliser DISTINCT, ORDER BY ou les fonctions d’agrégation sans utiliser les clauses GROUP BY et HAVING.

Entrainement

1) introduction

Nous avons eu l'occasion d'étudier la structure d'une base de données relationnelle, nous allons maintenant apprendre à réaliser des requêtes, c'est-à-dire que nous allons apprendre à créer une base des données, créer des attributs, ajouter de données, modifier des données et enfin, nous allons surtout apprendre à interroger une base de données afin d'obtenir des informations.

Pour réaliser toutes ces requêtes, nous allons devoir apprendre un langage de requêtes : SQL (Structured Query Language). SQL est propre aux bases de données relationnelles, les autres types de bases de données utilisent d'autres langages pour effectuer des requêtes.

Dans ce cours nous allons travailler avec SQLite. SQLite est un système de gestion de base de données relationnelle très répandu. Noter qu'il existe d'autres systèmes de gestion de base de données relationnelle comme MySQL ou PostgreSQL. Dans tous les cas, le langage de requête utilisé est le SQL (même si parfois on peut noter quelques petites différences). Ce qui sera vu ici avec SQLite pourra, à quelques petites modifications près, être utilisé avec, par exemple, MySQL.

CREATE TABLE AUTEURS (
    id INT PRIMARY KEY,
    nom VARCHAR(255),
    prenom VARCHAR(255),
    ann_naissance INT,
    langue_ecriture VARCHAR(255)
);
INSERT INTO AUTEURS (id, nom, prenom, ann_naissance, langue_ecriture)
VALUES
(1, 'Orwell', 'George', 1903, 'anglais'),
(2, 'Herbert', 'Frank', 1920, 'anglais'),
(3, 'Asimov', 'Isaac', 1920, 'anglais'),
(4, 'Huxley', 'Aldous', 1894, 'anglais'),
(5, 'Bradbury', 'Ray', 1920, 'anglais'),
(6, 'K.Dick', 'Philip', 1928, 'anglais'),
(7, 'Barjavel', 'René', 1911, 'français'),
(8, 'Boulle', 'Pierre', 1912, 'français'),
(9, 'Van Vogt', 'Alfred Elton', 1912, 'anglais'),
(10, 'Verne', 'Jules', 1828, 'français');
CREATE TABLE LIVRES (
    id INT PRIMARY KEY,
    titre VARCHAR(255),
    id_auteur INT,
    ann_publi INT,
    note INT,
    FOREIGN KEY (id_auteur) REFERENCES AUTEURS(id)
);
INSERT INTO LIVRES (id, titre, id_auteur, ann_publi, note)
VALUES
(1, '1984', 1, 1949, 10),
(2, 'Dune', 2, 1965, 8),
(3, 'Fondation', 3, 1951, 9),
(4, 'Le meilleur des mondes', 4, 1931, 7),
(5, 'Fahrenheit 451', 5, 1953, 7),
(6, 'Ubik', 6, 1969, 9),
(7, 'Chroniques martiennes', 5, 1950, 8),
(8, 'La nuit des temps', 7, 1968, 7),
(9, 'Blade Runner', 6, 1968, 8),
(10, 'Les Robots', 3, 1950, 9),
(11, 'La Planète des singes', 8, 1963, 8),
(12, 'Ravage', 7, 1943, 8),
(13, 'Le Maître du Haut Château', 6, 1962, 8),
(14, 'Le monde des Ā', 9, 1945, 7),
(15, 'La Fin de l’éternité', 3, 1955, 8),
(16, 'De la Terre à la Lune', 10, 1865, 10);

Nous allons travailler avec les 2 tables (relations) suivantes :AUTEURS et LIVRES

SELECT * FROM AUTEURS
SELECT * FROM LIVRES

2) Requêtes d'interrogation

a) Requêtes d'interrogation "simples"

Quand on désire extraire des informations d'une table, on effectue une requête d'interrogation à l'aide du mot clé SELECT. Voici un exemple de requête d'interrogation :

SELECT id, titre, id_auteur, ann_publi, note
FROM LIVRES

Il est possible d'obtenir uniquement certains attributs. Par exemple :

SELECT nom, prenom
FROM AUTEURS

b) sélectionner certaines lignes : la clause WHERE

Il est possible d'utiliser la clause WHERE afin d'imposer une (ou des) condition(s) permettant de sélectionner uniquement certaines lignes.

La condition doit suivre le mot-clé WHERE :

SELECT titre
FROM LIVRES 
WHERE note > 9

La requête ci-dessus permettra d'afficher uniquement les titres qui ont une note strictement supérieure à 9 (soit "1984" et "De la Terre à la Lune")

Il est possible de combiner les conditions à l'aide d'un OR ou d'un AND :

SELECT nom
FROM AUTEURS
WHERE langue_ecriture = 'français' AND ann_naissance > 1900

Il est aussi possible d'utiliser le OR à la place du AND :

SELECT nom
FROM AUTEURS
WHERE langue_ecriture = 'français' OR ann_naissance > 1920

c) mettre dans l'ordre les réponses : la clause ORDER BY

Il est possible de classer les résultats d'une requête par ordre croissant grâce à la clause ORDER BY :

SELECT nom, ann_naissance
FROM AUTEURS
WHERE langue_ecriture = 'français' ORDER BY ann_naissance

En rajoutant DESC, on obtient l'ordre décroissant :

SELECT nom, ann_naissance
FROM AUTEURS
WHERE langue_ecriture = 'français' ORDER BY ann_naissance DESC

À noter que si la clause ORDER BY porte sur une chaine de caractères, on obtient alors l'ordre alphabétique :

SELECT nom
FROM AUTEURS
WHERE langue_ecriture = 'français' ORDER BY nom

d) La clause DISTINCT

Il est possible d'éviter les doublons dans une réponse grâce à la clause DISTINCT. Imaginons la table suivante :

CREATE TABLE MACHINES (
    numero INT PRIMARY KEY,
    type VARCHAR(10),
    proprietaire VARCHAR(50)
);

INSERT INTO MACHINES (numero, type, proprietaire) VALUES
(1, 'X23', 'Marc'),
(2, 'Y43', 'Pierre'),
(3, 'Z24', 'Kevin'),
(4, 'Y44', 'Marc');
SELECT * FROM MACHINES
SELECT proprietaire
FROM MACHINES

Pour éviter ce doublon, nous pouvons écrire :

SELECT DISTINCT proprietaire
FROM MACHINES

e) Les jointures

Nous avons 2 tables, grâce aux jointures nous allons pouvoir associer ces 2 tables dans une même requête.

En général, les jointures consistent à associer des lignes de 2 tables. Elles permettent d'établir un lien entre 2 tables. Qui dit lien entre 2 tables dit souvent clé étrangère et clé primaire.

Analysons la requête suivante :

SELECT *
FROM LIVRES
INNER JOIN AUTEURS ON LIVRES.id_auteur = AUTEURS.id

Le FROM LIVRES INNER JOIN AUTEURS permet de créer une jointure entre les tables LIVRES et AUTEURS ("rassembler" les tables LIVRES et AUTEURS en une seule grande table). Le "ON LIVRES.id_auteur = AUTEURS.id" signifie qu'une ligne quelconque A de la table LIVRES devra être fusionnée avec la ligne B de la table AUTEURS à condition que l'attribut id_auteur de la ligne A soit égal à l'attribut id de la ligne B.

À noter que pour éviter toute confusion il est souvent judicieux d'ajouter le nom de la table juste devant le nom de l'attribut : on écrira AUTEURS.id au lieu de simplement id, en effet, si on écrivait seulement id, il n'y aurait aucun moyen de distinguer l'id de la table LIVRES et l'id de la table AUTEURS. Je vous conseille d'adopter cette écriture systématiquement en cas de jointure, même quand cela n'est pas obligatoire (par exemple on aurait pu écrire id_auteur à la place de LIVRES.id_auteur puisqu'il y a uniquement un id_auteur dans la table LIVRES), cela vous permettra d'éviter certains déboires.

Dans le cas d'une jointure, il est tout à fait possible de sélectionner certains attributs et pas d'autres (aucune obligation de sélectionner tous les attributs des 2 tables :

SELECT LIVRES.titre, AUTEURS.nom, AUTEURS.prenom
FROM AUTEURS
INNER JOIN LIVRES ON LIVRES.id_auteur = AUTEURS.id

f) utilisation du WHERE dans les jointures

Suite à une jointure il est possible de sélectionner certaines lignes grâce à la clause WHERE :

SELECT LIVRES.titre,AUTEURS.nom, AUTEURS.prenom
FROM LIVRES
INNER JOIN AUTEURS ON LIVRES.id_auteur = AUTEURS.id
WHERE LIVRES.ann_publi>1965

g) Les jointures plus complexes

Pour terminer avec les jointures, vous devez savoir que nous avons abordé la jointure la plus simple (INNER JOIN). Il existe des jointures plus complexes (CROSS JOIN, LEFT JOIN, RIGHT JOIN), ces autres jointures ne seront pas abordées dans ce cours.

3) requêtes d'insertion

Il est possible d'ajouter une entrée à une table grâce à une requête d'insertion :

INSERT INTO LIVRES
(id,titre,id_auteur,ann_publi,note)
VALUES
(17,'Hypérion',11,1989,8);

On emploie les mots clés INSERT INTO suivi de la table concernée (ici LIVRES). Ensuite on indique les noms des attributs que l'on désire ajouter (ici (id,titre,auteur,ann_publi,note)), et enfin pour terminer les valeurs de chaque attribut (ici (17,'Hypérion','Simmons',1989,8), attention à bien respecter l'ordre : 17 correspond à id, Hypérion correspond au titre, 11 correspond à id_auteur, 1989 correspond à ann_publi et 8 correspond à note).

4) requêtes de mise à jour

"UPDATE" va permettre de modifier une ou des entrées. Nous utiliserons "WHERE", comme dans le cas d'un "SELECT", pour spécifier les entrées à modifier. Voici un exemple de modification :

UPDATE LIVRES
SET note=10 
WHERE titre = 'Dune'

Cette requête permet de modifier la note du(des) livre(s) ayant pour titre Dune.

SELECT * FROM LIVRES

5) Requêtes de suppression

DELETE est utilisée pour effectuer la suppression d'une (ou de plusieurs) entrée(s). Ici aussi c'est le "WHERE" qui permettra de sélectionner les entrées à supprimer :

DELETE FROM LIVRES 
WHERE titre='Dune'
SELECT * FROM LIVRES
DELETE FROM LIVRES 

Attention à l'utilisation de cette requête DELETE notamment si on oublie le WHERE. on supprimerait toutes les entrées de la table LIVRES :

SELECT * FROM LIVRES

Ce qu’il faut savoir

Pour consulter des données, ajouter une entrée, modifier une entrée ou supprimer une entrée dans une base de données relationnelle, il est nécessaire d’effectuer des “requêtes SQL” (utilisation du langage SQL)

Pour ajouter des entrées à une table, on utilisera “INSERT” (exemple : INSERT INTO LIVRES (id,titre,auteur,ann_publi,note) VALUES (1,'1984','Orwell',1949,10);)

Pour interroger une table, on utilisera “SELECT” (exemple : SELECT titre FROM LIVRES WHERE auteur='Asimov')

Pour modifier une entrée, on utilisera “UPDATE” (exemple : UPDATE LIVRES SET note=10 WHERE titre = 'Dune')

Pour supprimer une entrée, on utilisera “DELETE” (exemple : DELETE FROM LIVRES WHERE titre='Dune')

Pour réaliser une jointure, il est possible d’utiliser “INNER JOIN” (exemple : SELECT FROM LIVRES INNER JOIN AUTEURS ON LIVRES.id_auteur = AUTEURS.id)

source : https://dav74.github.io/site_nsi_term/c3c/

Exercice :

Un ski-club utilise une base de données constituée de 2 tables :

Dans la table ADHERENTS on trouve un attribut “ref_station” qui permet de connaître les stations de ski préférées des adhérents.

Table ADHERENTS

num_licence nom prenom annee_naissance ref_station
12558 Doe John 1988 5
13668 Vect Alice 1974 6
1777 Dect Bob 1967 3
13447 Beau Tristan 1999 4
1141 Pabeau John 1975 3

table STATIONS

ref nom altitude_max
3 Le grand Bornand 2050
4 La clusaz 2616
5 Flaine 2510
6 Avoriaz 2466
  1. Comment appelle-t-on l’attribut ref_station de la table ADHERENTS ?
  2. Écrire la requête SQL permettant d’obtenir le nom des stations ayant une altitude maxi strictement supérieure à 2500 m.
  3. Écrire une requête SQL permettant d’obtenir le numéro de licence des adhérents nés après 1980 et ayant pour prénom John.
  4. Donnez le résultat de la requête SQL suivante :

SELECT nom 
FROM ADHERENTS 
WHERE num_licence > 2000 OR  ref_station = 3
5. Donnez le résultat de la requête SQL suivante :

SELECT STATIONS.nom
FROM STATIONS
INNER JOIN ADHERENTS ON ADHERENTS.ref_station = STATIONS.ref
WHERE annee_naissance > 1975

Réponse :

  1. L'attribut "ref_station" de la table ADHERENTS est appelé une "clé étrangère" (ou Foreign Key en anglais).
  2. SELECT nom FROM STATIONS WHERE altitude_max > 2500;
  3. ELECT num_licence FROM ADHERENTS WHERE annee_naissance > 1980 AND prenom = 'John';
  4. Doe, Vect, Beau
  5. Flaine, Avoriaz

Retour au sommaire

3.1 - Système sur puce ✔

Architectures matérielles, systèmes d’exploitation et réseaux

Composants intégrés d’un système sur puce

Capacités Attendue :
Identifier les principaux composants sur un schéma de circuit et les avantages de leur intégration en termes de vitesse et de consommation.

Commentaires :
Le circuit d’un téléphone peut être pris comme un exemple : microprocesseurs, mémoires locales, interfaces radio et filaires, gestion d’énergie, contrôleurs vidéo, accélérateur graphique, réseaux sur puce, etc.

1. Loi de Moore et miniaturisation progressive

1.1 La Loi de Moore

La loi de Moore, énoncée pour la première fois en 1965 par Gordon Moore, co-fondateur d'Intel, a prédit avec une précision remarquable la croissance exponentielle de la densité des transistors sur les puces électroniques pendant plusieurs décennies.

Cette progression fulgurante a été rendue possible grâce à des avancées constantes dans la technologie de fabrication, permettant une miniaturisation progressive des composants, et à des découvertes cruciales en physique des semi-conducteurs, qui ont optimisé la performance et la fiabilité des transistors à des échelles toujours plus réduites.

Cependant, après des décennies de croissance soutenue, nous observons aujourd'hui des signes indiquant un ralentissement de cette tendance. Les contraintes physiques, telles que les effets quantiques qui deviennent prédominants à de très petites échelles, ainsi que les défis économiques liés au coût croissant de la fabrication à l'échelle nanométrique, semblent indiquer que la loi de Moore pourrait ne pas être soutenable indéfiniment.

Alors que l'industrie des semi-conducteurs recherche des solutions innovantes pour surmonter ces obstacles, il est de plus en plus évident que la poursuite de la miniaturisation à tout prix n'est plus le seul paradigme dirigeant l'évolution des technologies informatiques.

import numpy as np
import matplotlib.pyplot as plt

# Données approximatives prises à partir du graphique
data = [
    (1971, 2300, 'Intel 4004'),
    (1972, 3500, 'Intel 8008'),
    (1974, 6000, 'Intel 8080'),
    (1978, 29000, 'Intel 8086'),
    (1982, 120000, 'Intel 80286'),
    (1985, 275000, 'Intel 80386'),
    (1989, 1200000, 'Intel 80486'),
    (1993, 3100000, 'Pentium'),
    (1995, 5500000, 'Pentium Pro'),
    (1997, 7500000, 'Pentium II'),
    (1999, 24000000, 'Pentium III'),
    (2002, 55000000, 'Pentium 4'),
    (2004, 110000000, 'Pentium M'),
    (2006, 291000000, 'Core 2 Duo'),
    (2008, 2300000000, 'Core i7'),
    (2010, 3000000000, 'Westmere i7'),
    (2012, 5000000000, 'Ivy Bridge i7'),
    (2014, 1400000000, 'Broadwell i7'),
    (2016, 3200000000, 'Kaby Lake i7'),
    (2018, 10000000000, 'AMD Ryzen'),
    (2020, 40000000000, 'Recent Chip'),
]

years, transistors, labels = zip(*data)

plt.figure(figsize=(14, 8))
plt.yscale("log")
plt.scatter(years, transistors, color='blue')
for i, txt in enumerate(labels):
    plt.annotate(txt, (years[i], transistors[i]), fontsize=8, ha='right')

# Tracer la courbe de la multiplication par 2 des transistors tous les 2 ans
start_year = 1971
start_transistors = 2300
trend_years = list(np.arange(start_year, 2023, 2))
trend_transistors = [start_transistors * (2 ** ((year - start_year) / 2)) for year in trend_years]
plt.plot(trend_years, trend_transistors, color='red', linestyle='--', label="Tendance x2 tous les 2 ans")

plt.title("Loi de Moore: Évolution du nombre de transistors")
plt.xlabel("Année")
plt.ylabel("Nombre de transistors (échelle log)")
plt.grid(True, which="both", ls="--", c='0.7')
plt.legend()

plt.show()
                         Graphique semi-logarithmique du nombre de transistors 
                         pour les microprocesseurs par rapport aux dates d'introduction

1.2 Évolution de la taille des ordinateurs

1.2.1 IBM 650, le premier ordinateur fabriqué en série (1955)
                           Cet ordinateur n'a pas encore de transistors mais des tubes à vide.

1.2.2 IBM 7090, le premier ordinateur à transistors (1959)

            Le transistor permet de laisser passer ou ne pas laisser passer un courant électrique

1.2.3 Le rôle crucial de la taille des transistors

Ainsi que l'avait prédit Moore, c'est la progression du nombre de transistors gravables sur le processeur qui guidera pendant des années l'évolution de l'informatique :

2. Composition d'un pc actuel

- CPU / Fan & Heatsink : Le CPU (Central Processing Unit) est le cerveau de l'ordinateur.
                         Le ventilateur (Fan) et le radiateur (Heatsink) sont utilisés pour refroidir le CPU. 
- RAM : La RAM (Random Access Memory) est la mémoire vive de l'ordinateur. 
- Optical Drive Bays : emplacements pour lecteurs DVD ou Blu-ray.
- GPU : Le GPU (Graphics Processing Unit) est responsable du rendu graphique. 
- Expansion Slots : emplacements sur la carte mère pour ajouter des cartes d'extension comme une carte son ou réseau.
- Hard Drive : stockage de données de l'ordinateur : système d'exploitation, applications et données personnelles.
- SSD : Le SSD (Solid State Drive) est une autre forme de stockage, similaire au disque dur, mais en mémoire flash au lieu des disques rotatifs, ce qui le rend plus rapide et moins sujet aux pannes mécaniques.
- Power Supply : L'alimentation fournit de l'électricité à tous les composants de l'ordinateur. 
- Motherboard : La carte mère est le composant principal qui relie tous les autres composants entre eux. 

Chaque composant a un rôle spécifique. Ils communiquent entre eux par des bus de différentes vitesses.

Chaque composant est remplaçable, et il est possible d'ajouter de nouveaux composants sur la carte mère qui possède des slots d'extension.

3. Tout un pc sur une seule puce : les SoC

Le principe d'un système sur puce ou System On a Chip (SoC) est d'intégrer au sein d'une puce unique un ensemble de composants habituellement physiquement dissociés dans un ordinateur classique (ordinateur de bureau ou ordinateur portable).

On peut retrouver ainsi au sein d'une même puce :

✅ Avantages d'un SoC

📛 Inconvénients

Exemple : A15 Bionic

        A15 Bionic, qui équipe les iPhone 13. Cette puce est fabriquée par TSMC.
        Cette puce contient :
        - 15 milliards de transistors (gravés à 5 nm)
        - un processeur central à 6 cœurs (2 cœurs hautes performances + 4 cœurs plus économes en énergie)
        - un GPU (processeur dédié uniquement au calcul du rendu graphique) de 5 cœurs.
        - une puce dédiée au Machine Learning (Neural Engine)

L'intégration dans un SoC n'est pas totale : il reste des puces dédiées à des tâches très spécifiques qui ne sont pas forcément intégrées dans le SoC.

Ainsi, d'après le site iFixit, on peut retrouver ceci dans l'iPhone Pro 13, au côté de la puce A15 évoquée plus haut :

                        On voit que (par exemple) qu'il existe : 
                        - une puce spécifique pour gérer l'audio, 
                        - une puce spécifique pour le module WiFi, 
                        - une puce spécifique pour le module Modem 5G...

4. Electronique «grand public»

Les cartes Arduino sont constituées d'un microcontrôleur et d'un certain nombre d'entrées/sorties numériques et analogiques.

Ces entrées/sorties permettent à la carte de se connecter à divers composants tels que des LED, des capteurs, des moteurs, etc.

Arduino est couramment utilisé pour une variété de projets allant des simples bricolages aux prototypes complexes pour les produits finis. Que ce soit pour automatiser une plante d'intérieur, construire un robot, ou créer une station météo, Arduino est une excellente option pour les débutants et les experts en électronique.

source : glassus

Retour au sommaire

3.2 - Processus, système ✔

Architectures matérielles, systèmes d’exploitation et réseaux

Gestion des processus et des ressources par un système d’exploitation.

Capacités Attendue :

Commentaires :

1. Notion de processus

1.1 Définition d'un processus

Lorsqu'un programme est exécuté sur un ordinateur, celui-ci va créer un ou plusieurs processus.

On dit que ce processus est une instance d'exécution de ce programme.

Un processus est caractérisé par :

1.2 Observation des processus.

Windows fournit plusieurs outils pour observer et gérer les processus :

    Pour comprendre la correspondance entre PID et PPID:
    - PID (ProcessId) : Il s'agit de l'identifiant unique du processus. 
    Chaque processus sur votre système d'exploitation a un PID distinct.
    -PPID (ParentProcessId) : Il s'agit de l'identifiant du processus parent qui a lancé le processus actuel. 
    Si un processus n'a pas de processus parent, cette valeur peut être 0.

Il existe plusieurs outils tiers qui peuvent vous donner une vue détaillée des processus en cours d'exécution similaires Sous Linux , on peux utilise la commade ps. Un exemple populaire est Sysinternals Process Explorer .

2. Ordonnancement

L'ordonnancement est le mécanisme par lequel un système d'exploitation décide quel processus en attente doit être alloué au processeur pour son exécution. L'ordonnancement vise à optimiser certaines propriétés du système, comme le temps de réponse, le débit ou l'utilisation du processeur.

Un ordinateur semble effectuer plusieurs opérations en même temps. Cependant, à moins que le processeur ne possède de multiples cœurs, ce n'est pas le cas.

Le système d'exploitation lance ces processus de manière séquentielle. Malgré cela, il semble qu'ils soient tous « actifs simultanément » : c'est ce qu'on appelle la programmation concurrente.

Même si ces processus existent durant la même période, ils fonctionnent successivement. Le processeur ne peut gérer qu'un processus à la fois.

Leur exécution à un rythme très soutenu donne l'impression d'une simultanéité, bien qu'elle soit illusoire.

Explorons cette notion plus en détail, considérons les scripts progA et progB ci-dessous :

Temps 2 machines.py :

import threading
import time

def progA():
    for i in range(10):
        current_time_ns = time.time_ns()
        print(f"[{current_time_ns}] programme A en cours, itération {i}")
        time.sleep(0.1)

def progB():
    for i in range(10):
        current_time_ns = time.time_ns()
        print(f"[{current_time_ns}] programme B en cours, itération {i}")
        time.sleep(0.1)

# Création de deux threads distincts pour exécuter les fonctions progA et progB.
t1 = threading.Thread(target=progA)
t2 = threading.Thread(target=progB)

# Démarrage des deux threads, leur permettant d'exécuter les fonctions en parallèle
t1.start()
t2.start()

# Attente que les deux threads aient terminé avant de poursuivre
t1.join()
t2.join()
Python 3.11.3
[1698169303153550200] programme A en cours, itération 0
[1698169303153550200] programme B en cours, itération 0
[1698169303255943600] programme A en cours, itération 1
[1698169303256450900] programme B en cours, itération 1
[1698169303356815500] programme A en cours, itération 2
[1698169303356815500] programme B en cours, itération 2
[1698169303457698400] programme A en cours, itération 3
[1698169303457698400] programme B en cours, itération 3
[1698169303558466200] programme A en cours, itération 4
[1698169303558466200] programme B en cours, itération 4
[1698169303659003900] programme A en cours, itération 5
[1698169303659003900] programme B en cours, itération 5
[1698169303759853300] programme A en cours, itération 6
[1698169303759853300] programme B en cours, itération 6
[1698169303860672100] programme A en cours, itération 7
[1698169303860672100] programme B en cours, itération 7
[1698169303960965400] programme A en cours, itération 8
[1698169303960965400] programme B en cours, itération 8
[1698169304061331000] programme B en cours, itération 9
[1698169304061331000] programme A en cours, itération 9 

Il en résulte un mauvais entrelacement entre les phrases progA en cours et progB en cours. Mais si l'on regarde les temps, ce n'est qu'un problème d'affichage car les programmes sont simultanées.

Pour mettre en évidence, l'alternance entre les divers processus, il faut augmenter le nombres de processueur pour dépasser le nombre de coeurs.

Temps n machines.py

import threading
import time

# Déterminer le nombre de programmes (threads) à exécuter
num_programs = int(input("Combien de programmes souhaitez-vous exécuter ? "))

# Initialiser le tableau pour stocker les temps pour chaque programme et chaque itération
time_table = [[0 for _ in range(num_programs)] for _ in range(10)]

# Pour stocker le temps minimum pour chaque itération
min_times = [float('inf') for _ in range(10)]

# Capturer le temps de départ en nanosecondes
start_time_ns = time.time_ns()

def prog(num):
    for i in range(10):
        current_time_ns = time.time_ns()
        elapsed_time_ns = current_time_ns - start_time_ns
        time_table[i][num] = elapsed_time_ns
        min_times[i] = min(min_times[i], elapsed_time_ns)  # Mettre à jour le temps minimum
        # print(f"[{elapsed_time_ns:20}] programme {num:2} en cours, itération {i}")
        time.sleep(0.1)

# Création des threads pour exécuter les fonctions prog
threads = []
for i in range(num_programs):
    t = threading.Thread(target=prog, args=(i,))
    threads.append(t)
    t.start()

# Attente que tous les threads aient terminé avant de poursuivre
for t in threads:
    t.join()

# Affichage des données sous forme de tableau
print("\nTableau des temps écoulés depuis le début et des différences de temps:")
header_format = "{:<8}{:<20}"
column_format = "{:<12}"

print(header_format.format('Étape', 'Min Temps'), end="")
for i in range(num_programs):
    print(column_format.format(f"Prog {i}"), end="")
print()

for i in range(10):
    print(header_format.format(i, min_times[i]), end="")
    for j in range(num_programs):
        diff = time_table[i][j] - min_times[i]  # Calculer la différence par rapport au temps minimum
        print(column_format.format(diff), end="")
    print()
Python 3.11.3 
Combien de programmes souhaitez-vous exécuter ? 5

Tableau des temps écoulés depuis le début et des différences de temps:
Étape   Min Temps           Prog 0      Prog 1      Prog 2      Prog 3      Prog 4      
0       0                   0           0           0           0           0           
1       109463600           0           0           1070200     1070200     2084700     
2       210114300           0           0           1030900     1030900     2039100     
3       310738400           0           0           605900      605900      1661500     
4       410977700           0           531500      1056900     531500      1565500     
5       511877900           0           0           574700      0           1348700     
6       612419900           0           0           544100      0           1560500     
7       713313600           0           0           0           0           1262500     
8       813911100           0           557900      0           557900      1187700     
9       914628600           0           0           0           555900      1061200

L'affichage durant l'exécution peut-être une source de ralentissement, je l'ai supprimée. Et là, enfin nous voyons de l'alternance.

Si la gestion des processus était réellement simultanée, même en considérant des ralentissements du processeur par des sollicitations extérieures, chaque processus serait ralenti de la même manière : l'entrelacement des phrases serait toujours le même.

En réalité, le processeur passe son temps à alterner entre les divers processus qu'il a à gérer, et les met en attente quand il ne peut pas s'occuper d'eux. Il obéit pour cela aux instructions de son ordonnanceur.

Différents types d'ordonnancement :

Si on vous donne 4 tâches A, B, C et D à accomplir, vous pouvez décider :

Un processeur est confronté aux mêmes choix : comment déterminer quel processus doit être traité à quel moment ? Le schéma ci-dessous présente quelques politiques d'ordonnancement :

Dans le cas (très fréquent maintenant) d'un processeur multi-cœurs, le problème reste identique. Certes, sur 4 cœurs, 4 processus pourront être traités simultanément (une réelle simultanéité) mais il reste toujours beaucoup plus de processus à traiter que de cœurs dans le processeur... et un ordonnancement est donc toujours nécessaire.

Sous Linux, l'ordonnancement est effectué par un système hybride où les processus sont exécutés à tour de rôle (on parle de tourniquet ou de Round Robin) suivant un ordre de priorité dynamique.

États d’un processus

Lorsqu’un programme est lancé, les instructions machine qui le composent sont chargées en mémoire de travail (RAM) : le processus associé est alors créé.

Pendant son existence au sein d’une machine, un processus peut avoir différents états :

Ou plus simplement :

On peut utiliser la métaphore suivante : Cela ressemble à un professeur, qui a 3 paquets de copies à corigers !!!

3. Interblocage

Un processus peut être dans l'état bloqué dans l'attente de la libération d'une ressource.

Ces ressources (l'accès en écriture à un fichier, à un registre de la mémoire...) ne peuvent être données à deux processus à la fois. Des processus souhaitant accéder à cette ressource sont donc en concurrence sur cette ressource. Un processus peut donc devoir attendre qu'une ressource se libère avant de pouvoir y accéder (et ainsi passer de l'état Bloqué à l'état Prêt). Problème : Et si deux processus se bloquent mutuellement la ressource dont ils ont besoin ?

Exemple :

Considérons 2 processus A et B, et deux ressources R et S. L'action des processus A et B est décrite ci-dessous :

A l'étape A2 de A : problème, il faut pour cela pouvoir accèder à la ressource S, qui n'est pas disponible. L'ordonnanceur va donc passer A à Bloqué et va revenir au processus B qui redevient Élu. A l'étape B2 de B : problème, il faut pour cela pouvoir accèder à la ressource R, qui n'est pas disponible. L'ordonnanceur va donc passer B à Bloqué.

Les deux processus A et B sont donc dans l'état Bloqué, chacun en attente de la libération d'une ressource bloquée par l'autre : ils se bloquent mutuellement.

Cette situation (critique) est appelée interblocage ou deadlock.

Ce type de schéma fait apparaître un cycle d'interdépendance, qui caractérise ici la situation de deadlock.

Comment s'en prémunir ?

Il existe trois stratégies pour éviter les interblocages :

source : glassus

Retour au sommaire

3.3 - Routage ✔

Architectures matérielles, systèmes d’exploitation et réseaux

Protocoles de routage

Capacités Attendue :
Identifier, suivant le protocole de routage utilisé, la route empruntée par un paquet.

Commentaires :

1. Résumé des épisodes précédents

cours de 1ère : Transmission de données dans un réseau, Protocoles de communication, Architecture d’un réseau (Réseaux 1 et 2)

Lorsqu'une machine A, d'adresse IP_A veut discuter avec une machine B, d'adresse IP_B :

Ces questions trouveront des réponses grâce à table de routage du routeur.

2. Tables de routage

Les tables de routage sont des informations stockées dans le routeur permettant d'aiguiller intelligemment les données qui lui sont transmises.

Dans le réseau ci-dessus, si l'ordinateur d'adresse 192.168.0.5 veut interroger le serveur 10.7.3.8 :

Interface et passerelle :

Les tables de routage des routeurs font très souvent apparaître deux colonnes, interface et passerelle, dont il ne faut pas confondre l'utilité :

Exemple: table de routage du routeur R1

Destination Interface Passerelle
F 192.168.0.254
A 10.0.5.152
E 172.17.1.254
B 172.17.1.254 172.17.1.123
C 10.0.5.152 10.0.5.135

Les trois réseaux F, A et E sont directement accessibles au routeur R1, puisqu'il en fait partie : il n'a donc pas besoin d'adresse passerelle pour communiquer avec ces réseaux.

Par contre, la communication avec le réseau B nécessite de confier le paquet au routeur R2 (c'est le choix de cette table de routage). Il faut donc mentionner l'adresse IP de ce routeur R2 (172.17.1.123), qu'on appelle adresse de passerelle.

De la même manière, la communication avec le réseau C nécessite de confier le paquet au routeur R3 (c'est le choix de cette table de routage). Il faut donc mentionner l'adresse IP de ce routeur R3 (10.0.5.135).

Comment sont construites les tables de routage ?

2. Le protocole RIP

Les règles du protocole RIP

Le Routing Information Protocol (RIP) est basé sur l'échange (toutes les 30 secondes) des tables de routage de chaque routeur. Au début, chaque routeur ne connaît que les réseaux auquel il est directement connecté, associé à la distance 1. Ensuite, chaque routeur va recevoir périodiquement (toutes les 30 secondes) la table des réseaux auquel il est connecté, et mettre à jour sa propre table suivant les règles ci-dessous :

Remarques et inconvénients:

3. Le protocole OSPF

Un inconvénient majeur du protocole précédent est la non-prise en compte de la bande passante reliant les routeurs.

principe fondamental du protocole OSPF (Open Shortest Path First) : Le chemin le plus rapide n'est pas forcément le plus court.

                                        En gris, le chemin RIP. En bleu, l'OSPF.

Dans le protocole OSPF, les tables de routage vont prendre en considération la vitesse de communication entre les routeurs.

Dans une première phase d'initialisation, chaque routeur va acquérir (par succession de messages envoyés et reçus) la connaissance totale du réseau (différence fondamentale avec RIP) et de la qualité technique de la liaison entre chaque routeur.

Les différents types de liaison et leur coût

On peut, approximativement, classer les types de liaison suivant ce tableau de débits théoriques :

Technologie BP descendante BP montante
Modem 56 kbit/s 48 kbit/s
Bluetooth 3 Mbit/s 3 Mbit/s
Ethernet 10 Mbit/s 10 Mbit/s
Wi-Fi 10 Mbit/s ~ 10 Gbits/s 10 Mbit/s ~ 10 Gbits/s
ADSL 13 Mbit/s 1 Mbit/s
4G 100 Mbit/s 50 Mbit/s
Satellite 50 Mbit/s 1 Mbit/s
Fast Ethernet 100 Mbit/s 100 Mbit/s
FFTH (fibre) 10 Gbit/s 10 Gbit/s
5G 20 Gbit/s 10 Gbit/s

L'idée du protocole OSPF est de pondérer chaque trajet entre routeurs (comptant simplement pour «1» dans le protocole RIP) par une valeur de coût inversement proportionnelle au débit de transfert.

Par exemple, si le débit $d$ est exprimé en bits/s, on peut calculer le coût de chaque liaison par la formule : $coût = \frac{10^8}{d}$.

Cette formule de calcul peut être différente suivant les exercices, et sera systématiquement redonnée.

Néanmoins la valeur sera toujours au dénominateur, pour assurer la proportionnalité inverse du débit.

Avec cette convention, un route entre deux routeurs reliés en Fast Ethernet (100 Mbits/s) aura a un poids de 1, une liaison satellite de 20 Mbits/s aura un poids de 5, etc.

Reprenons le réseau suivant :

et simplifions-le en ne gardant que les liens entre routeurs, en indiquant leur débit :

Notre réseau est devenu un graphe.

Nous allons pondérer ses arêtes avec la fonction coût introduite précédemment. L'unité étant le Mbit/s, l'arête entre R1 et R3 aura un poids de $coût = \frac{10^8}{20 000} = 5$.

Le graphe pondéré est donc :

Le chemin le plus rapide pour aller de l'ordinateur au serveur est donc R1-R2-R4, et non plus R1-R3 comme l'aurait indiqué le protocole RIP.

Trouver le plus court chemin dans un graphe pondéré

L'exemple précédent était très simple et de solution intuitive. Dans le cas d'un graphe pondéré complexe, existe-t-il un algorithme de détermination du plus court chemin d'un point à un autre ? La réponse est oui, depuis la découverte en 1959 par Edsger Dijkstra de l'algorithme qui porte son nom, l'algorithme de Dijkstra.

Pour le comprendre, regarder la vidéo : Dijkstra

Exercice :

Donner le plus court chemin pour aller de E à F dans le graphe ci-dessous :

Réponse :

E A B C D F Choix
0 -- -- -- -- -- E(0)
. 30vE -- 40vE 10vE -- D(10)
. 20vD -- 40vE . 80vD A(20)
. . 60vA 30vA . 80vD C(30)
. . 50vC . . 80vD B(50)
. . . . . 70vB F(70)

Le meilleur trajet est donc E-D-A-C-B-F. Attention ce trajet correspond à la colonne choix (dans l'ordre) mais c'est un hasard.

Exercice2 : (extrait du sujet 0)

On considère le réseau suivant :

On rappelle que le coût d’une liaison est donné par la formule suivante : $coût = \frac{10^8}{d}$

  1. Vérifier que le coût de la liaison entre les routeurs A et B est 0,01.

    La liaison entre le routeur B et D a un coût de 5. Quel est le débit de cette liaison ?

  2. Le routeur A doit transmettre un message au routeur G, en empruntant le chemin dont la somme des coûts sera la plus petite possible.

    Déterminer le chemin parcouru. On indiquera le raisonnement utilisé.

Réponse :

  1. $coût = \frac{10^8}{10x10^9} = 10^-2 = 0.01$

    $5 = \frac{10^8}{d}$ donc $d =\frac{10^8}{5} = 2x10^7 = 20x10^6 = 20Mb/s$

  2. On peut deviner le chemin de coût minimal entre A et G, qui est A-D-E-G (coût 1.011).

Pour le justifier, on peut (non obligatoire) faire un algorithme de Dijkstra :

Lien pour vérifier votre plus court chemin : Graph Online

source : glassus

Retour au sommaire

3.4 - Congruences ✔

Les congruences


Théorème de Bézout :

Si a et b sont des entiers non nuls, alors il existe des entiers relatifs x et y, et un entier d tels que ax + by = d où d est le PGCD de a et b.

Démonstration :
Considérons l'ensemble S de toutes les combinaisons ax + by, où x et y sont des entiers relatifs.
S contient au moins un élément positif non nul, par exemple l'entier a.
Donc l'ensemble S, il existe un plus petit élément positif, que nous appellerons d.
Par définition de S, il existe des entiers x0 et y0 tels que d = ax0 + by0.

Pour montrer que d divise à la fois a et b, considérons la division euclidienne de a par d>0 :
a = dq + r, où 0 ≤ r < d. Donc r = a - dq = a - (ax0 + by0)q = a(1-qx0) + b(-qy0).
Donc r appartient à S et 0 ≤ r < d.

Par construction, d est le plus petit élément positif non nul de S, donc r = 0.
Ainsi, d divise a. Un raisonnement similaire montre que d divise b.
Donc d est un diviseur commun de a et b.

Si un autre nombre d' est aussi un diviseur commun de a et b, alors comme d = ax0 + by0, cette autre nombre d' divisera d.
Par conséquent, d est le PGCD de a et b.

Algorithme d'Euclide (Classe de 3ième) ou PGCD de a et b

PDCD(a,b) = ?

1er cas : si b = 0
PDCD(a,b) = PDCD(a,0) = a

2ième Cas : si b > 0
Posons a = qb + r où q est le quotient et r est le reste de la division euclidienne de a par b.
d = PGCD(a,b) = PGCD(b,r)

def pgcd(a, b):
    if b == 0:
        return a
    else:
        return pgcd(b, a % b)

# Exemple d'utilisation
a = 91
b = 77
print("Le PGCD de", a, "et", b, "est :", pgcd(a, b))
Le PGCD de 91 et 77 est : 7

Algorithme d'Euclide étendu ou comment trouver des coefficients x et y de Bezout et le PDCD de a et b

1er cas : si b = 0
d =a, x = 1, y = 0 est une solution de ax + by = d

2ième Cas : si b > 0
ax + by = d <=> (qb + r)x + by = d <=> b(qx + y) + rx = d <=> bx1 + ry1 = d avec x1 = qx + y et y1 = x <=> bx1 + ry1 = d avec x = y1 et y = x1 - qy1

def euclide_etendu(a, b):
    if b == 0:
        return a, 1, 0
    else:
        pgcd, x1, y1 = euclide_etendu(b, a % b)
        x = y1
        y = x1 - (a // b) * y1
        return pgcd, x, y

# Exemple d'utilisation
a = 91
b = 77
pgcd, x, y = euclide_etendu(a, b)
print(f"Une solution de {a}x + {b}y = d est {a}({x}) + {b}({y}) = {pgcd}.")
Une solution de 91x + 77y = d est 91(-5) + 77(6) = 7.

Théorème de Gauss :

Si a, b et c sont des entiers non nuls.
Si a divise le produit bc et si a et b sont premiers entre eux alors a divise c.

Démonstration :
a divise bc, donc il existe k entier tel que bc = ka.
a et b sont premiers entre eux donc il existe deux entiers u et v tels que au + bv = 1.
D'où c = cau + cbv et bc = ka, donc c = cau + kav = a(cu + kv) ce qui prouve que a divise c.

Définition de la Congruence :

Soient a, b, n des entiers relatifs.
On dit que a est congruent à b modulo n, noté a≡b (mod n) ou a≡b[n], si n divise la différence a−b.

Propriétés des Propriétés de Congruence :

  1. Réflexivité : Pour tout entier relatif a, a ≡ a [n].

  2. Symétrie : Si a et b sont des entiers relatifs et a ≡ b [n], alors b ≡ a [n].

  3. Transitivité : Si a, b, et c sont des entiers relatifs et a ≡ b [n] et b ≡ c [n], alors a ≡ c [n].

  4. Addition : Si a, b, c, et d sont des entiers relatifs et a ≡ b [n] et c ≡ d [n], alors a + c ≡ b + d [n].

  5. Soustraction : Si a, b, c, et d sont des entiers relatifs et a ≡ b [n] et c ≡ d [n], alors a - c ≡ b - d [n].

  6. Multiplication : Si a, b, c, et d sont des entiers relatifs et a ≡ b [n] et c ≡ d [n], alors a * c ≡ b * d [n].

  7. Puissance : Si a et b sont des entiers relatifs et a ≡ b [n], alors ak ≡ bk [n] pour tout entier k.

  8. Petit Théorème de Fermat : Si p est un nombre premier et a est un entier relatif non divisible par p, alors ap-1 ≡ 1 [p].

Démonstrations des Propriétés de Congruence

  1. Réflexivité
  2. La différence a - a est égale à 0.
    Comme n divise tout nombre entier multiple de 0 (car n × 0 = 0), n divise a - a.
    Donc, a ≡ a [n].

  3. Symétrie
  4. a ≡ b [n]
    Donc il existe un entier relatif k tel que a - b = nk.
    Donc b - a = -nk.
    Donc, b ≡ a [n].

  5. Transitivité
  6. a ≡ b [n].
    Donc il existe un entier relatif k tel que a - b = nk.
    b ≡ c [n].
    Donc il existe un entier relatif l tel que b - c = nl.
    En additionnant ces deux équations, on obtient (a - b) + (b - c) = nk + nl.
    Donc a - c = n(k + l).
    Donc a ≡ c [n].

  7. Addition
  8. a ≡ b [n]
    Donc il existe un entier relatif k tel que a - b = nk.
    c ≡ d [n]
    Donc il existe un entier relatif l tel que c - d = nl.
    En additionnant ces deux équations, on obtient (a + c) - (b + d) = n(k + l).
    Donc a + c ≡ b + d [n].

  9. Soustraction
  10. a ≡ b [n]
    Donc il existe un entier relatif k tel que a - b = nk.
    c ≡ d [n]
    Donc il existe un entier relatif l tel que c - d = nl.
    Par soustraction de ces deux équations, on obtient a - b - c + d = nk - nl.
    Donc (a - c) - (b - d) = n(k - l).
    Donc a - c ≡ b - d [n].

  11. Multiplication
  12. a ≡ b [n].
    Donc il existe un entier k tel que a = b + nk.
    c ≡ d [n].
    Donc il existe un entier l tel que c = d + nl.
    En multipliant ces deux équations, on obtient ac = (b + nk)(d + nl).
    Donc ac = bd + bnl + nkd + nknl.
    Donc ac = bd + n (bl + kd + knl).
    Donc ac ≡ bd [n].

  13. Puissance
  14. Soit P(k) la propriété ak ≡ bk [n] pour tout entier k.

    Initialisation :
    Pour k = 1, a1 ≡ b1 [n] se réduit à a ≡ b [n], ce qui est notre hypothèse de départ.
    Donc, la propriété est vraie pour k = 1.

    Hérédité :
    Supposons que la propriété soit vraie pour un certain entier k, c'est-à-dire ak ≡ bk [n]
    a ≡ b [n] et ak ≡ bk [n]
    D'après la propriété de Multiplication, on a : ak * a ≡ bk * b [n].
    Donc ak+1 ≡ bk+1 [n].

    Par récurrence, la propriété est donc vraie pour tout entier 1 ≤ k .

  15. Petit Théorème de Fermat
  16. Démontrons cette propriété par récurrence.

    Soit P(a): ap ≡ a [p] pour tout entier a.

    Initialisation :
    Pour a = 0
    0p = 0, donc 0p ≡ 0[p].
    Donc P(0) est vraie.

    Hérédité :
    Supposons que la propriété soit vraie pour un certain entier a, c'est-à-dire ap ≡ a [p].
    Formule du binôme :
    (a+1)p = 1 + $\binom{p}{1}$a + ... + $\binom{p}{p-1}$ap-1 + ap

    coefficient binomial :
    Pour tous les entiers k et p, avec 0 ≤ k ≤ p
    \( \binom{p}{k} = \frac{p!}{k!(p-k)!} \) et ce nombre est un nombre entier.

    Donc pour tout entier k tel que 1 ≤ k ≤ p-1, on a : \( k \cdot \binom{p}{k} = k \times \frac{p!}{k!(p-k)!} = \frac{p \times (p-1)!}{(k-1)!(p-k)!} = p \times \binom{p-1}{k-1} \)
    Donc pour tout entier k tel que 1 ≤ k ≤ p-1, on a : p divise \( k \cdot \binom{p}{k} \).

    1 ≤ k ≤ p-1 donc p ne divise pas k.
    p divise \( k \cdot \binom{p}{k} \)
    Donc d'après le théorème de Gauss, on a : p divise \( \binom{p}{k} \).
    Donc tout entier k tel que 1 ≤ k ≤ p-1, on a : \( \binom{p}{k} \) ≡ 0[p]

    Donc (a+1)p ≡ 1+ap[p].
    Et par l'hypothèse de récurrence, on a : ap ≡ a [p].
    Donc (a+1)p ≡ 1+a[p].
    Donc (a+1)p ≡ a+1 [p].

    Par récurrence, la propriété ap ≡ a [p] est donc vraie pour tout entier 0 ≤ a .

    Donc ap ≡ a [p] est donc vraie pour tout entier 1 ≤ a.
    Donc ap - a ≡ 0 [p] est donc vraie pour tout entier 1 ≤ a.
    Donc a(ap-1 - 1) ≡ 0 [p] est donc vraie pour tout entier 1 ≤ a.

    a est un entier relatif non divisible par p.
    Donc d'après le théorème de Gauss, on a : p divise ap-1 - 1.
    Donc ap-1 - 1 ≡ 0 [p] pour tout entier 1 ≤ a où a est un entier relatif non divisible par p.

Critère de Divisibilité par 9

Un nombre entier est divisible par 9 si et seulement si la somme de ses chiffres est divisible par 9.

Démonstration :

Soit un nombre entier N.
N = 10nanan + 10n-1an-1+...+102a2+ 10a1 où ak sont ses chiffres.

10 ≡ 1 [9]
Donc pour tout entier k, on a : 10k ≡ 1 [9]
Donc N ≡ an + an-1 + ... + a0 [9]

Retour au sommaire

3.5 - RSA ✔

Architectures matérielles, systèmes d’exploitation et réseaux

Sécurisation des communications.

Capacités Attendue :

Commentaires :

1. Un exemple de Chiffrement symétrique :

Propriété :
Pour tous les nombre entiers a et b, on a :
(a XOR b) XOR b = a.

Démonstration :
(0 XOR 0) XOR 0 = 0 XOR 0 = 0
(0 XOR 1) XOR 1 = 1 XOR 1 = 0
(1 XOR 0) XOR 0 = 1 XOR 0 = 1
(1 XOR 1) XOR 1 = 0 XOR 1 = 1
Et donc dans chaque scénario, (a XOR b) XOR b = a.

Le XOR est noté ^ en python.

masque = "CETTEPHRASEESTVRAIMENTTRESTRESLONGUEMAISCESTFAITEXPRES"

def chiffre(message, masque):
    message_chiffre = ""
    for i in range(len(message)):
        lettre_chiffree = chr(ord(message[i]) ^ ord(masque[i]))
        message_chiffre += lettre_chiffree
    return message_chiffre

message ="Je suis le plus FORT !"
message_crypté = chiffre(message, masque)
print("message crypté :", message_crypté)
mesage_décrypté = chiffre(message_crypté, masque)
print("message décrypté :",mesage_décrypté)

Dans un chiffrement symétrique, c'est la même clé qui sert au chiffrement et au déchiffrement.

Avantage d'un chiffrement symétrique :
Les chiffrements symétriques sont souvent rapides, consommant peu de ressources, et donc adaptés au chiffrement de flux important d'informations. Par exemple, la sécurisation des données transitant par le protocole https est basée sur un chiffrement symétrique.

Inconvénient d'un chiffrement symétrique :
La clé ! Si deux personne ont besoin d'utiliser un chiffrement pour se parler, comment peuvent-ils échanger leurs clés puisque leur canal de transmission n'est pas sûr ! Le chiffrement symétrique impose qu'Alice et Bob aient pu se rencontrer physiquement au préalable pour convenir d'une clé secrète, ou bien qu'ils aient réussi à établir une connexion sécurisée pour s'échanger cette clé.

Principe de Kerckhoffs :
La sécurité d'un système de chiffrement ne doit reposer que sur le secret de la clé, et non pas sur la connaissance de l'algorithme de chiffrement. Cet algorithme peut même être public (ce qui est pratiquement toujours le cas).

2. Chiffrement asymétrique

Inventé par Whitfield Diffie et Martin Hellman en 1976, le chiffrement asymétrique vient résoudre l'inconvénient essentiel du chiffrement symétrique : le nécessaire partage d'un secret (la clé) avant l'établissement de la communication sécurisée.

2.1 Principe du chiffrement asymétrique

Le principe de base est l'existence d'une clé publique, appelée à être distribuée largement, et d'une clé privée, qui ne quitte jamais son propriétaire.

2.2 Communication authentifiée.

Dans la situation du 2.1, Alice, qui a distribué largement sa clé publique, ne peut pas s'assurer que le message vient bien de Bob.

Avec le protocole ci-dessous permet de s'assurer que chaque personne est bien celle qu'elle prétend être : on résout le problème d'authentification.

2.3 Méthode du chiffrement RSA :

En 1977, trois chercheurs du MIT, Ron Rivest, Adi Shamir et Len Adleman créent le premier protocole concret de chiffrement asymétrique : le chiffrement RSA.

Clifford Cocks est le véritable inventeur du RSA... mais le reste du monde ne l'apprendra qu'en 1997 au moment de la déclassification de cette information. Il avait fait cette découverte 3 ans auparavant.

Étape 1

On choisit 2 grands nombres premiers p = 11 et q = 17.
Dans la réalité ces nombres seront vraiment très grands, plus de 100 chiffres.

Étape 2

on pose n = pq.
donc n = 11 x 17 = 187

Avec de très grans nombre, il est extrêmement difficile de trouver q et p en partant de n, cela prend un temps exponentiel.
La robustesse du système RSA repose sur cette difficulté de factorisation.

Étape 3

On choisit un nombre e qui doit être premier avec phi = (p-1)(q-1) et qui est différent de 1.
Ici phi = (p-1)(q-1) = (11-1)(17-1) = 10x16 = 160

160= 2x5x2x2x2 donc nous pouvons choisir e = 3.
Mais nous aurions pu choisir 3,7,9,11,13,17,19,21,23...

Le couple (e,n) est la clé publique, elle peut être diffusé à qui veut nous écrire.
donc (e,n) = (3,187).

Étape 4

On calcule maintenant la clé privée, on doit trouver un nombre d qui vérifie l'égalité ed $\equiv 1 [phi]$.
donc 3d $\equiv 1 [160]$
3 x 107 = 321 = 2 x 160 + 1
donc on peux choisir d = 107

En pratique, il existe l'Algorithme d'Euclide étendu pour trouver cette valeur d, appelée inverse de e.

Le couple (d,n) est la clé privée. Elle ne doit pas être diffusée.
donc (d,n) = (107,187).

Étape 5

Supposons que Bob veuille écrire à Alice pour lui envoyer la lettre 'J', soit le nombre ord('J')=74.
Il possède la clé publique d'Alice, qui est (e,n) = (3,187)
Il calcul $74^3$ = 405 224 = 2 166 x 187 + 182 donc $74^3 \equiv 182[187]$
C'est cette valeur 182 qu'il transmet à Alice.

Si Marc intercepte cette valeur 182, même en connaissant la clé publique d'Alice (3,187), il peut résoudre l'équation $x^3 \equiv 182[187]$ (voir programme ci-dessous) mais pas de manière efficace lorsque il y a de grands nombres.

Étape 6

Alice reçoit la valeur 182.
La clé privée de Alice est (d,n) = (107,187)
Il lui "suffit !!!" alors d'élever 182 à la puissance 107 (sa clé privée), et de calculer le reste modulo 187 :
$182^{107}$ = 67241673631537193934036117101479548806501991262111002001744254706233567920890614697211406837000572401237441319454792304933033591086665839294597750916081076542143576548003513761297726915976490281874033126212146214405576504503866998629825773568 = 187 x 18060028181057512288308845553797179459401200529078752358110793195031781145697580921395102159974556894430381933341233949598704423208702796527307650511078722855725227084557548715716316244005752915438897000961 + 74

$\equiv 74[187]$

La valeur 74, qui est bien le message original de Bob.

Pourquoi ça marche ?

Grâce au Théorème de Bezout :
comme e et phi sont premiers entre eux, il existe des entiers u et v tel que u.e + v.phi = 1
donc $u.e \equiv 1 [phi]$
donc il existe d tel que $d.e \equiv 1 [phi]$
donc il existe d tel que $d.e \equiv 1 [(p-1)(q-1)]$

Grâce au Petit Théorème de Fermat :
Si p est un nombre premier et a n'est pas divisible par p, alors $a^{p-1} \equiv 1 [p]$.

Soit m un nombre qui n'est pas divisible par p et qui n'est pas divisible par q.
donc d'après le Petit Théorème de Fermat : $m^{p-1} \equiv 1 [p]$ et $m^{q-1} \equiv 1 [q]$
donc $m^{(p-1)(q-1)} = (m^{p-1})^{q-1} \equiv 1^{q-1} [p] \equiv 1 [p]$
de même $m^{(p-1)(q-1)} = (m^{q-1})^{p-1} \equiv 1^{p-1} [q] \equiv 1 [q]$
p et q sont des nombres premiers donc d'après le Théorème de Gauss, on a : $m^{(p-1)(q-1)} \equiv 1 [pq]$

n = pq
donc $m^{(p-1)(q-1)} \equiv 1 [n]$

On a posait d tel que ed $\equiv 1 [(p-1)(q-1)]$
donc il existe un nombre entier k tel que ed = (p-1)(q-1)k + 1
donc $m^{ed} = m^{(p-1)(q-1)k + 1} = m(m^{(p-1)(q-1)}) = m[n] $

Et donc dans notre exemple :
$74^3 \equiv 182[187]$ donc $(74^3)^d \equiv 182^d[187]$ donc $182 ^ d \equiv (74^3)^d [187]$
donc $182^{107} = 182 ^ d \equiv (74^3)^d [187] \equiv (74^e)^d [n] \equiv 74^{ed}[n] \equiv 74[n] \equiv 74[187]$

Remarques :

RSA, un système inviolable ?

Le chiffrement RSA a des défauts (notamment une grande consommation des ressources, due à la manipulation de très grands nombres). Mais le choix d'une clé publique de grande taille (actuellement 1024 ou 2048 bits) le rend pour l'instant inviolable.

Deux évènements pourraient faire s'écrouler la sécurité du RSA :

def gcd(a, b):
    while b:
        a, b = b, a % b
    return a

def find_e(phi):
    e = 2
    while e < phi:
        if gcd(e, phi) == 1:
            return e
        e += 1
    raise Exception('Aucun e approprié trouvé')

p = 11
q = 17
phi = (p-1) * (q-1)
e = find_e(phi)

print(f"e = {e}")
# Algorithme d'Euclide étendu
def extended_gcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = extended_gcd(b % a, a)
        return (g, x - (b // a) * y, y)

def modinv(e, phi):
    g, x, y = extended_gcd(e, phi)
    if g != 1:
        raise Exception("L'inverse modulaire n'existe pas")
    else:
        return x % phi

n = p * q

# Calcul de phi et d
phi = (p-1) * (q-1)
d = modinv(e, phi)

print(f"p = {p}, q = {q}")
print(f"n = {n}")
print(f"e = {e}")
print(f"phi = {phi}")
print(f"d = {d}")

Exponentiation Modulaire

L'idée de base de l'exponentiation modulaire est de décomposer l'exponentiation en une série de carrés et de multiplications, en utilisant le fait que :

$ab [m] = (a [m]) (b [m]) [m]$

Donc, au lieu de calculer $182^{107}$ directement, ce qui serait très coûteux en termes de calculs, nous décomposons l'exposant en puissances de 2, et calculons modulo 187 à chaque étape pour garder les nombres gérables.

Commençons par représenter l'exposant 107 en binaire :
$107 = 2^6 + 2^5 + 2^3 + 2^1 + 2^0$ $107 = 1101011$ en binaire

En utilisant cette représentation, $182^{107}$ est décomposé comme : $182^{107}$ = $182^{(2^6)}$ x $182^{(2^5)}$ x $182^{(2^3)}$ x $182^{(2^1)}$ x $182^{(2^0)}$

Nous allons maintenant calculer la valeur de $182^{107} [187]$ en utilisant cette décomposition et la propriété mentionnée précédemment.

$182^{(2^0)} = 182 [187]$
$182^{(2^1)} = 33 124 = 25[187]$
$182_25 = 4 550 = 62[187]$
$182^{(2^2)} = (182^{(2^1)})^2 = 25^2[187] = 625[187] = 64[187]$
$182^{(2^3)} = (182^{(2^2)})^2 = 64^2[187] = 4096[187] = 169[187]$
$62_169 = 10 478[187] = 6[187]$
$182^{2^4} = (182^{(2^3)})^2= 169^2[187] = 28 561[187] = 137[187]$
$182^{2^5} = (182^{(2^4)})^2= 137^2[187] = 18 769[187] = 69[187]$
$69_6 = 414 = 40[187]$
$182^{2^6} = (182^{(2^5)})^2 = 69^2[187] = 4761[187] = 86[187]$
$86_40 = 3440 = 74[187]$

# Exponentiation modulaire
def mod_pow(base, exponent, mod):
    binary_exponent = bin(exponent)[2:]
    result = 1
    base = base % mod
    for bit in reversed(binary_exponent):
        if bit == '1':
            result = (result * base) % mod
        base = (base * base) % mod
    return result

base = 182
exponent = 107
modulus = 187

result = mod_pow(base, exponent, modulus)
print(f"Résultat final : {base}^{exponent} mod {modulus} = {result}")
# RSA

def rsa_encrypt(message, e, n):
    encrypted = [mod_pow(ord(char), e, n) for char in message]
    return encrypted

def rsa_decrypt(encrypted, d, n):
    decrypted = ''.join([chr(mod_pow(num, d, n)) for num in encrypted])
    return decrypted

print(f"p = {p}, q = {q}")
print(f"n = p x q = {n}")
print(f"e = {e}")
print(f"d = {d}")

message = "Je suis le plus FORT!"

print(f"message : {message}")

encrypted_message = rsa_encrypt(message, e, n)
print(f"Message chiffré: {encrypted_message}")

decrypted_message = rsa_decrypt(encrypted_message, d, n)
print(f"Message déchiffré: {decrypted_message}")
# Résolution de 𝑥^3≡182[187] avec l'exponentiation modulaire
for k in range(201):
    if mod_pow(k, 3, 187) == 182:
           print(k)

3. HTTPS : exemple d'utilisation conjointe d'un chiffrement asymétrique et d'un chiffrement symétrique.

Plus de 90 % du trafic internet est chiffré avec HTTPS, combinant :

TLS : Génère une clé partagée via chiffrement asymétrique.
HTTP : Transmet les données chiffrées avec la clé partagée (AES).

Le chiffrement asymétrique, trop lent, est utilisé uniquement pour l'échange de clés, tandis que le chiffrement symétrique, plus rapide, gère le reste.

Retour au sommaire

4.1 - Programme en tant que donnée ✔

Langages et programmation

Notion de programme en tant que donnée. Calculabilité, décidabilité.

Capacités Attendue :

Commentaires :
L’utilisation d’un interpréteur ou d’un compilateur, le téléchargement de logiciel, le fonctionnement des systèmes d’exploitation permettent de comprendre un programme comme donnée d’un autre programme.

1. Notion de programme en tant que donnée

Nous allons tout d'abord expliciter un point important qui sera le fondement de la théorie de la calculabilité : un programme est aussi une donnée.

Cela peut paraître étonnant à première vue puisqu'on est habitué à traiter :

Fonctions et variables sont des objets de nature différente en apparence. Si on se raccroche à ce que l'on connaît en python, une fonction se déclare avec le mot clé def et une variable s'initialise avec l'opérateur d'affectation =.

Prenons en exemple l'algorithme d'Euclide, un algorithme vieux de plus de 2500 ans, permettant de calculer le PGCD de 2 nombres. On peut l'écrire à l'aide d'une fonction Python:

def euclide(a,b):
    if a < b:
        a,b=b,a
    while b:
        a,b=b,a%b
    return a

euclide(35, 49)
7

Dans ce programme Python, euclide est une fonction et a et b sont des données. Ils ne semblent pas être de nature comparable.

Et pourtant, à y regarder de plus près, notre algorithme programmé dans la fonction euclide n'est rien d'autre qu'une succession de caractères. On peut même pousser la réflexion jusqu'à créer une chaîne de caractère contenant ce programme :

mon_programme = "def euclide(a,b):\n\tif a < b: a,b=b,a\n\twhile b: a,b=b,a%b\n\treturn a"

Maintenant mon algorithme est devenu une variable. On peut alors construire une machine universelle capable d'évaluer n'importe quelle donnée contenant un algorithme formalisé dans le langage Python :

def universel(algo, *args):
    exec(algo)
    ligne1 = algo.split('\n')[0]
    nom = ligne1.split('(')[0][4:]
    return eval(f"{nom}{args}")

A présent, il est possible d'invoquer la machine universelle en lui passant en données :

universel(mon_programme, 35, 49)
7

Dans l'exemple ci-dessus, vous pouvez constater que le programme et les données sur lesquelles il agit sont de même nature : ce sont 3 variables passées en paramètres à ma fonction universelle. on en déduit donc :

Propriété : Un programme est une donnée.

Certains programmes utilisent comme données le code source d'autres programmes. Les compilateurs sont des bons exemples.Une fois le code source terminée, le compilateur (qui est un logiciel comme un autre) "transforme" ce code source en langage machine.

Il existe d'autres exemples de programmes qui utilisent comme données d'autres programmes :

On trouve même des programmes capables de détecter certaines erreurs dans le code source d'autres programmes ou même encore des programmes capables de prouver (mathématiquement parlant) qu'un autre programme est correct (qu'il fait bien ce pour quoi il a été conçu).

2. Décidable, calculable

Un problème de décision est dit décidable s'il existe un algorithme, une procédure mécanique qui se termine en un nombre fini d'étapes, qui le décide, c'est-à-dire qui réponde par oui ou par non à la question posée par le problème.

S'il n'existe pas de tels algorithmes, le problème est dit indécidable.

Exemples :

Une fonction ƒ est une fonction calculable s'il existe une méthode précise qui, étant donné un argument , permet d'obtenir l'image en un nombre fini d'étapes.

Exemples :

Attention :

Si un problème est indécidable cela ne veut pas dire que l’on n’est pas capable de résoudre ce problème, cela veut juste dire qu’il n’existe pas d’algorithme capable de résoudre ce problème.
Par exemple :
Soient f et g deux fonctions, et deux programmes def fonction_f(x): return f(x) et def fonction_g(x): return g(x).
Les programmes sont-ils égaux ?
On ne peut pas tester une infinité de valeurs.

3. Le problème de l’arrêt est indécidable.

Vidéo : Fabrizio Bucella Université de Bruxelles - Physicien

L'existence des problèmes indécidables a été prouvé en 1937 par Alonzo Church et Alan Turing.
Propriété : Le problème de l'arrêt est indécidable.

Voici une version de la démonstration du problème de l'arrêt, telle qu'introduite par Alan Turing, sans formalisme !

Démonstration :

Raisonnon par l'absurde

Supposons l'existence d'un programme nommé $H$, capable de décider du problème de l'arrêt.
Ce programme $H$ prend en entrée un programme $P$ et une entrée $X$.

$H(P, X) = \begin{cases} oui & \text{si } P \text{ s'arrête avec l'entrée } X, \\ non & \text{si } P \text{ entre dans une boucle infinie avec l'entrée } X, \end{cases}$

Création d'un programme contradictoire

Construisons maintenant un programme $D_H$ utilisant $H$.
$D_H$ prend en entrée un programme $P$ et fait ce qui suit :

$D_H(P) = \begin{cases} & \text{entre dans une boucle infinie si } H(P,P) = oui \\ & \text{s'arrête si } H(P,P) = non, \end{cases}$

Contradiction

Conclusion :

Il n'existe pas d'algorithme général qui puisse déterminer si un autre algorithme s'arrête ou non pour toutes les entrées possibles.

Retour au sommaire

4.2 - Récursivité ✔

Terminale NSI - Programmation : récursion

Capacités attendues :

Commentaire :
Des exemples relevant de domaines variés sont à privilégier.

1) Méthode itérative

Une fonction itérative est une fonction dans laquelle des instructions sont exécutées dans des boucles while ou for.

On souhaite implémenter une fonction somme() prenant nen argument, et qui renvoie la somme des n premiers entiers.

Par exemple :

>>> somme(5)
15

on calcule la somme 0 + 1 + 2 + 3 + 4 + 5, ce qui fait 15.

def somme_iteratif(n):
    total = 0
    for nombre in range(0, n+1, 1):
        total = total + nombre
    return total

somme_iteratif(5)
from tutor import tutor

def somme_iteratif(n):
    total = 0
    for nombre in range(0, n+1, 1):
        total = total + nombre
    return total

somme_iteratif(5)

tutor()

a) Combien de fois la fonction somme_iteratif est-elle exécutée ?

Réponse : La fonction somme_iteratif est exécutée une seule fois dans le code que vous avez fourni, à savoir lors de l'appel somme_iteratif(5).

b) Quelles sont les différentes valeurs de l'argument n ?

Réponse : Dans le code fourni, l'argument n prend une seule valeur, à savoir 5.

c) Combien il y a-t-il de valeurs renvoyées ?

Réponse : Une seule valeur est renvoyée par la fonction lorsqu'elle est appelée. Pour l'appel somme_iteratif(5), la valeur renvoyée est 15.

d) Comment est calculée la valeur renvoyée ?

Réponse :

La valeur renvoyée est la somme des entiers de 0 à n inclus. Cette somme est calculée de manière itérative en utilisant une boucle for. À chaque itération de la boucle, la variable total est augmentée de la valeur de l'itérateur nombre jusqu'à ce que l'itérateur atteigne la valeur de n. Une fois la boucle terminée, la somme totale est renvoyée.

2) Méthode récursive

Une fonction récursive est une fonction qui s'appelle elle-même.

def somme_recursif(n):
    if n == 0:
        return 0
    else:
        return n + somme_recursif(n-1)

somme_recursif(5)
from tutor import tutor

def somme_recursif(n):
    if n == 0:
        return 0
    else:
        return n + somme_recursif(n-1)

somme_recursif(5)

tutor()

a) Combien de fois la fonction somme_recursif est-elle exécutée ?

Réponse : La fonction somme_recursif est exécutée 6 fois.

b) Quelles sont les différentes valeurs de l'argument n ?

Réponse : Les différentes valeurs de l'argument n sont 5, 4, 3, 2, 1 et 0.

c) Combien il y a-t-il de valeurs renvoyées ?

Réponse : Il y a 6 valeurs renvoyées, une pour chaque appel récursif de la fonction.

d) Laquelle (ou lesquelles) ?

Réponse : Les valeurs renvoyées sont 0, 1, 3, 6, 10 et 15.

3) Vérification de la Correction des Algorithmes Récursifs

Note : La section suivante peut être approfondie ultérieurement.

Pour prouver qu’un algorithme récursif fonctionne on doit prouver qu’il vérifie deux propriétés :

La logique sous-jacente à cette vérification est analogique à celle utilisée dans une preuve par récurrence en mathématiques.

def somme_recursif(n):
    if n == 0:
        return 0
    else:
        return n + somme_recursif(n-1)

somme_recursif(5)

Correction :

— Initialisation (cas de base) : pour n = 0 on a bien somme_recursif(0)= 0 et la premier valeur de la somme des entier est aussi 0

— Conservation : Suppose que pour n fixé les appels internes récursifs sont valides soit : somme_recursif(n) = 0 + 1 + ... + n alors puisque notre relation de récurrence est : somme_recursif(n) = n +somme_recursif(n − 1) donc somme_recursif(n+1) = (n+1) + somme_recursif(n) = 0 + 1 + ... + n + (n+1) On obtient bien notre hypothèse de récurrence.

Terminaison :

L’algorithme se termine car à chaque tour de boucle n diminue de 1 et on fini par arriver au cas n = 0.

Conclusion :

Les propriétés de correction et de terminaison sont juste, donc l'algorithme récursif fonctionne.

4) Temps d'exécution d'un fonction itérative et récursive

On peut comparer le temps d'exécution des deux fonctions précédentes grâce au module timeit.

from timeit import timeit

n = 100

duree_execution_iteratif = timeit(lambda:somme_iteratif(n), number=100)
print(duree_execution_iteratif)

duree_execution_recursif = timeit(lambda:somme_recursif(n), number=100)
print(duree_execution_recursif)
0.0010000000002037268
0.005000000001018634

Augmentez la valeur de n.

a) Quelle est la version la plus rapide ?

Réponse : La fonction somme_iteratif

b) Que se passe-t-il si n est trop grand ?

Réponse : Seulement la fonction somme_iteratif marche .

c) Quelle est l'erreur rencontrée ?

Réponse : RecursionError: maximum recursion depth exceeded in comparison

Exercices :

Exercice 1 :

Factorielle est une opération mathématique notée avec un point d'exclamation : n!. On dira « factorielle n » ou « n factoriel ». La factorielle d'un entier naturel n est le produit des nombres entiers strictement positifs inférieurs ou égaux à n. Par convention 0! = 1.

On a donc 1! = 1, 2! = 2 x 1 = 2, 3! = 3 x 2 x 1 = 6,…

a) Proposez une fonction itérative en Python qui permette de calculer n!.

def factorielle(n):
    f = 1
    for i in range(n):
        f = f * (i + 1)
    return f

factorielle(4)

b) Proposez une fonction récursive en Python qui permette de calculer n!.

def fact(n):
    if n == 1:
        return 1
    else:
        return n * fact(n - 1)
    
fact(4)

Exercice 2 :

La suite de Fibonacci est une suite doublement récurrente définie ainsi :

$$(u_n):\begin{cases}u_0 = 0 \ u_1=1\\forall n\in\mathbf N, u_{n+2}=u_{n+1}+u_n\end{cases}$$

a) Calculer le terme $u_{5}$.

$u_{5}$ = $u_{4}$ + $u_{3}$

$u_{4}$ = $u_{3}$ + $u_{2}$

$u_{3}$ = $u_{2}$ + $u_{1}$

$u_{2}$ = $u_{1}$ + $u_{0}$

donc

$u_{2}$ = 1 + 0 = 1

$u_{3}$ = 1 + 1 = 2

$u_{4}$ = 2 + 1 = 3

$u_{5}$ = 3 + 2 = 5

b) Écrivez une fonction itérative Fibonacci qui donnera le n ième terme de la suite de Fibonacci pour n>1.

def fibonacci(n):
    a, b = 0, 1
    for _ in range(2, n+1):
        a, b = b, a + b
    return b

fibonacci(5)

c) Écrivez une fonction récursive Fibonacci qui donnera le n ième terme de la suite de Fibonacci pour n>1.

def Fibonacci(n):
    if n < 2:
        return n
    else:
        return Fibonacci(n-1) + Fibonacci(n-2)
    
Fibonacci(5)

Exercice 3 :

L'algorithme d'Euclide permet de déterminer le plus grand diviseur commun à deux entiers naturels a et b avec a > b.

Il repose sur la propriété suivante :

a. Proposer une formulation itérative de cet algorithme

def pgcd(a, b):
    while b != 0:
        a, b = b, a % b
    return a

# Exemple d'utilisation
a = 56
b = 98
print(f"PGCD({a}, {b}) =", pgcd(a, b))
PGCD(56, 98) = 14

b. Proposer une formulation récursive de cet algorithme.

def pgcd(a, b):
    if b == 0:
        return a
    else:
        return pgcd(b, a % b)

# Exemple d'utilisation
a = 56
b = 98
print(f"PGCD({a}, {b}) =", pgcd(a, b))
PGCD(56, 98) = 14

Exercice 4 : Flocon de Koch

Nous allons utiliser le module Python Turtle. Ce module permet de dessiner très simplement.

Étudiez le Wikibook consacré au module Turtle (wikibook Turtle) afin d'acquérir les bases de ce module.

Visionnez la vidéo consacrée au flocon de Koch : vidéo consacrée au flocon de Koch.

Proposer une formulation récursive du flocon de Koch.

from turtle import *

def koch(longueur, n):
    if n == 0:
        forward(longueur)
    else:
        koch(longueur/3, n-1)
        left(60)
        koch(longueur/3, n-1)
        right(120)
        koch(longueur/3, n-1)
        left(60)
        koch(longueur/3, n-1)

def flocon(taille, etape):
    koch(taille, etape)
    right(120)
    koch(taille, etape)
    right(120)
    koch(taille, etape)

flocon(100, 3)

Exercice 5 : Tour de Hanoï

Voir Hanoï sur la page d'accueil

Bilan sur la récursivité

La récursivité est généralement plus simple à programmer une fois qu'on a trouvé la bonne relation de récursion. Par contre la consommation mémoire est généralement plus importante que pour la programmation itérative.

Retour au sommaire

4.3 - Modularité ✔

Langages et programmation

Modularité

Conpétences exigibles :
Utiliser des API (Application Programming Interface) ou des bibliothèques.
Exploiter leur documentation.
Créer des modules simples et les documenter.

1. Qu'est-ce que la modularité?

La modularité est une stratégie de conception qui décompose un système en modules distincts, où chaque module gère une fonctionnalité spécifique du système global. L'idée est de créer des pièces réutilisables qui peuvent être assemblées de différentes manières pour créer des systèmes variés.

Avantages de la modularité :

Comment appliquer la modularité?

Fonctions: C'est la forme la plus basique de modularité. Une fonction encapsule un bloc de code qui effectue une tâche spécifique.

Classes et objets: Dans la programmation orientée objet, les classes peuvent être vues comme des modules. Une classe encapsule des données (attributs) et des méthodes pour manipuler ces données.

Bibliothèques et Frameworks: Ce sont des collections de fonctions, de classes et d'autres ressources que les développeurs peuvent utiliser pour éviter de "réinventer la roue".

Exemple de langages supportant la modularité:

Bonnes pratiques en matière de modularité:

2. Les modules Python

Pour vraiment maîtriser l'utilisation des modules en Python, il est essentiel de savoir comment accéder aux informations sur ce qu'ils contiennent et comment ils fonctionnent.

Liste des fonctions et classes d'un module:

Après avoir importé un module, vous pouvez utiliser la fonction dir() pour lister tous ses attributs, fonctions, et classes:

import math
print(dir(math))
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'lcm', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'nextafter', 'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc', 'ulp']

Cela vous donne une liste de toutes les fonctions et constantes disponibles dans le module math.

Utiliser l'aide:

La fonction help() est intégrée à Python et permet d'afficher la documentation d'une fonction, classe ou module:

import math
help(math.sqrt)
Help on built-in function sqrt in module math:

sqrt(x, /)
    Return the square root of x.

Cela affiche une explication sur la fonction sqrt() du module math.

Accéder à la documentation (docstrings):

La plupart des fonctions, classes, et modules en Python contiennent des chaînes de documentation, ou docstrings, qui fournissent une explication concise de leur utilité et de leur utilisation. Vous pouvez y accéder en utilisant l'attribut doc:

print(math.cos.__doc__)
Return the cosine of x (measured in radians).

Cela affichera la docstring pour la fonction cos().

Lorsque vous travaillez avec des packages tiers (ceux que vous installez via pip, par exemple), la documentation officielle ou la page GitHub du package est souvent le meilleur endroit pour trouver des informations. Les grands packages tels que NumPy, pandas ou Flask ont des documentations complètes disponibles en ligne.

Des environnements de développement intégrés (IDE) comme PyCharm, Visual Studio Code (avec l'extension Python), et autres offrent souvent des fonctionnalités d'introspection qui permettent de voir rapidement la documentation ou la signature d'une fonction en passant simplement la souris dessus ou en appuyant sur une combinaison de touches.

Si vous avez besoin d'une compréhension plus profonde de la manière dont un module ou une fonction fonctionne, et que la documentation ne suffit pas, vous pouvez souvent consulter directement le code source. Pour les modules de la bibliothèque standard ou les packages installés via pip, vous pouvez généralement trouver le code source sur GitHub ou d'autres plateformes de gestion de code source.

3. Les déclarations import en Python

Les déclarations import en Python sont utilisées pour accéder aux fonctions, classes, variables et autres éléments d'un module. Je vais décomposer chaque forme d'importation.

3.1 import

La déclaration import est utilisée pour importer un module entier.

Après avoir exécuté l'instruction ci-dessous, vous pouvez accéder à toutes les fonctions et variables définies dans le module math en utilisant la notation pointée. Par exemple, math.sqrt(2) renverrait 1.4142135623730951.

import math
math.sqrt(2)
1.4142135623730951

3.2 from ... import ...

La déclaration from ... import ... est utilisée pour importer des éléments spécifiques (comme des fonctions, des classes ou des variables) d'un module.

Avec cette forme d'importation, vous n'avez plus besoin d'utiliser la notation pointée pour accéder à la fonction sqrt. Vous pouvez simplement écrire sqrt(2).

from math import sqrt
sqrt(2)
1.4142135623730951

3.3 from ... import *

La déclaration from ... import * importe tous les éléments d'un module directement dans l'espace de noms courant. Cela signifie que vous n'aurez pas à utiliser la notation pointée pour accéder à ces éléments. Après cette instruction, vous pouvez directement utiliser des fonctions comme sqrt, sin, cos, etc., sans préfixe.

from math import *
sqrt(2)
1.4142135623730951

Attention : Bien que cette méthode d'importation puisse sembler pratique, elle est généralement déconseillée car elle peut rendre le code moins lisible (il devient plus difficile de déterminer d'où proviennent certaines fonctions ou variables) et peut causer des conflits si deux modules importés ont des fonctions ou des variables avec le même nom.

3.4 from module_name import element_name as alias

Vous pouvez également attribuer un alias à des éléments spécifiques lors de leur importation.

from math import sqrt as racine_carree
racine_carree(2)
1.4142135623730951

3.5 import module_name as alias

Lorsque vous importez un module entier, vous pouvez lui attribuer un alias pour rendre votre code plus concis ou pour éviter des conflits de noms.

import numpy
A = numpy.array([[1, 2], [3, 4]])
print(A)
[[1 2]
 [3 4]]
import numpy as np
A = np.array([[1, 2], [3, 4]])
print(A)
[[1 2]
 [3 4]]

4. Créer un module avec un autre programme Python

Supposons que nous voulions développer un petit système pour gérer les opérations mathématiques basiques comme l'addition, la soustraction, la multiplication, et la division.

Nous pouvons diviser chaque opération mathématique en un module distinct. Pour simplifier, nous les garderons dans un seul fichier, mais dans de grands projets, chaque module pourrait être dans un fichier séparé.

# operations.py

def addition(a, b):
    return a + b

def soustraction(a, b):
    return a - b

def multiplication(a, b):
    return a * b

def division(a, b):
    if b == 0:
        return "Erreur: Division par zéro!"
    return a / b

Maintenant, nous allons créer un fichier principal (main.py) qui importera et utilisera notre module operations.py.

# main.py

import operations

a = 10
b = 5

print(f"{a} + {b} = {operations.addition(a, b)}")
print(f"{a} - {b} = {operations.soustraction(a, b)}")
print(f"{a} * {b} = {operations.multiplication(a, b)}")
print(f"{a} / {b} = {operations.division(a, b)}")

Lorsque vous exécutez main.py, vous obtiendrez le résultat des opérations.

Avantages de cette approche

Cet exemple est simpliste, mais il montre le concept de base de la modularité en Python. Dans des projets réels, vous auriez probablement des modules plus complexes, et vous pourriez utiliser des packages pour organiser ces modules en groupes logiques.

5. La Programmation Orientée Objet (POO)

La Programmation Orientée Objet (POO) est une approche de la programmation qui regroupe les données et les fonctions qui opèrent sur ces données en une seule entité, appelée objet. Elle est basée sur quelques concepts clés que nous allons explorer.

Concepts clés de la POO:

POO en Python:

En Python, tout est un objet. Même les types de base comme les entiers, les chaînes et les listes sont des objets avec leurs propres méthodes.

Exemple simple de classe et d'objet en Python:

La Programmation Orientée Objet (POO) est une approche de la programmation qui regroupe les données et les fonctions qui opèrent sur ces données en une seule entité, appelée objet. Elle est basée sur quelques concepts clés que nous allons explorer.

Concepts clés de la POO:

POO en Python:

En Python, tout est un objet. Même les types de base comme les entiers, les chaînes et les listes sont des objets avec leurs propres méthodes.

# Classe
class Animal:
    def __init__(self, nom, espece):  # Le constructeur __init__ initialise l'objet lors de sa création
        self.nom = nom          # Attribut 
        self.__espece = espece  # Attribut 

    def parler(self):
        return f"Je suis {self.nom}, un(e) {self.__espece}"

# Objet
lion = Animal("Simba", "lion")  # Création d'un objet "lion" de la classe Animal

print(lion.parler())  # Appel de la méthode parler() de l'objet lion
Je suis Simba, un(e) lion

Autre exemple :

class Vehicule:
    def __init__(self, marque):
        self.marque = marque

    def afficher(self):
        print(f"C'est un véhicule de marque {self.marque}")

class Moto(Vehicule):
    def afficher_type(self):
        print("C'est une moto")

ma_moto = Moto("Honda") # Création d'un objet "Honda" de la classe Moto
ma_moto.afficher_type()
ma_moto.afficher()
C'est une moto
C'est un véhicule de marque Honda

6. La création de documentation pour un module en Python

La création de documentation pour un module en Python repose généralement sur deux étapes :

Les docstrings sont entourés de triples guillemets (simples ou doubles). Voici un exemple de module avec des docstrings:

"""
Module pour gérer des opérations mathématiques basiques.
"""

def addition(a, b):
    """
    Retourne la somme de a et b.
    
    Args:
        a (int ou float): Premier nombre.
        b (int ou float): Deuxième nombre.
        
    Returns:
        int ou float: Somme de a et b.
    """
    return a + b

L'outil le plus couramment utilisé pour générer une documentation à partir de docstrings est Sphinx. Installation de Sphinx: pip install sphinx Sphinx génére des fichiers HTML dans docs/build/html que vous pouvez ouvrir dans un navigateur pour voir la documentation.

N'oubliez pas de tenir vos docstrings à jour lorsque vous modifiez ou ajoutez du code à votre module. Une documentation précise et à jour est essentielle pour assurer la clarté et la maintenabilité de votre code.

Retour au sommaire

4.4 - Paradigmes de programmation ✔

Langages et programmation

Paradigmes de programmation

Capacités Attendue :

Commentaires :
Avec un même langage de programmation, on peut utiliser des paradigmes différents. Dans un même programme, on peut utiliser des paradigmes différents.

1. Paradigme Impératif

Dans le paradigme impératif, le programme est constitué d'une séquence d'instructions qui modifient l'état de la machine. Il est très similaire à la façon dont les humains accomplissent les tâches étape par étape.

Exemple en Python :

# Calcul de la somme de 1 à n
n = 10
somme = 0

for i in range(1, n+1):
    somme += i
    
print("La somme est :", somme)
La somme est : 55

2. Paradigme Fonctionnel

Le paradigme fonctionnel se concentre sur l'évaluation des fonctions. Il évite de modifier l'état et de manipuler des données. En général, les fonctions sont "pures", c'est-à-dire qu'elles donnent toujours la même sortie pour une entrée donnée et n'ont pas d'effets secondaires.

Exemple en Python :

# Calcul de la somme de 1 à n en utilisant une approche fonctionnelle
def somme_recursive(n):
    if n == 0:
        return 0
    else:
        return n + somme_recursive(n-1)

print("La somme est :", somme_recursive(10))
La somme est : 55

3. Paradigme Orienté Objet (POO)

Dans le paradigme orienté objet, le programme est constitué d'objets qui contiennent à la fois des données et des méthodes pour manipuler ces données. Le but est de regrouper les données et les fonctions qui les manipulent afin de faciliter la conception, l'organisation et la maintenance du code.

Exemple en Python :

# Classe pour calculer la somme de 1 à n
class Somme:
    def __init__(self, n):
        self.n = n
    
    def calculer(self):
        somme = 0
        for i in range(1, self.n + 1):
            somme += i
        return somme

s = Somme(10)
print("La somme est :", s.calculer())
La somme est : 55

4. Choisir le Paradigme Selon le Champ d'Application

5. Utilisation de Paradigmes Différents dans un Même Programme

Un langage de programmation polyvalent comme Python permet aux développeurs d'utiliser différents paradigmes de programmation pour résoudre différents problèmes. Par exemple, vous pourriez utiliser une approche impérative pour une tâche qui nécessite un contrôle étroit du flux du programme, tout en utilisant une approche orientée objet pour structurer votre code autour d'entités complexes.

Il est également possible d'utiliser plusieurs paradigmes dans un seul et même programme. Voici un exemple simple en Python qui combine les paradigmes impératif, fonctionnel et orienté objet :

# Fonction fonctionnelle pour calculer la somme d'une liste
def somme_liste(lst):
    if len(lst) == 0:
        return 0
    else:
        return lst[0] + somme_liste(lst[1:])

# Classe pour représenter un étudiant (Orienté Objet)
class Etudiant:
    def __init__(self, nom, notes):
        self.nom = nom
        self.notes = notes
    
    # Méthode pour calculer la note moyenne (Impératif)
    def note_moyenne(self):
        total = 0
        for note in self.notes:
            total += note
        return total / len(self.notes)
    
    # Méthode pour calculer la note moyenne en utilisant la fonction fonctionnelle (Fonctionnel)
    def note_moyenne_fonctionnelle(self):
        total = somme_liste(self.notes)
        return total / len(self.notes)

# Création d'un objet Etudiant
etudiant = Etudiant("Alice", [90, 85, 77, 92])

# Utilisation d'une méthode impérative
print(f"Note moyenne (Impératif): {etudiant.note_moyenne()}")

# Utilisation d'une méthode fonctionnelle
print(f"Note moyenne (Fonctionnel): {etudiant.note_moyenne_fonctionnelle()}")
Note moyenne (Impératif): 86.0
Note moyenne (Fonctionnel): 86.0

Dans cet exemple, la classe Etudiant utilise une méthode impérative (note_moyenne) pour calculer la note moyenne en utilisant une boucle for. Par ailleurs, elle utilise également une méthode fonctionnelle (note_moyenne_fonctionnelle) qui fait appel à la fonction somme_liste, une fonction écrite dans le style fonctionnel.

Ainsi, différents paradigmes peuvent coexister dans un même programme pour résoudre différents problèmes, chacun d'une manière qui est le plus naturel ou efficace pour ce problème spécifique.

Retour au sommaire

4.5 - Gestion des bugs ✔

Langages et programmation

Mise au point des programmes. Gestion des bugs.

Capacités Attendue :
Dans la pratique de la programmation, savoir répondre aux causes typiques de bugs : problèmes liés au typage, effets de bord non désirés, débordements dans les tableaux, instruction conditionnelle non exhaustive, choix des inégalités, comparaisons et calculs entre flottants, mauvais nommage des variables, etc.

Commentaires :

1. Erreurs les plus courantes en Python

Lors de l'écriture de code Python, les erreurs sont «gentiment» rappelées par l'interpréteur Python à l'exécution du code.

Type d'erreur

Objet Python

Erreurs courantes

Exemple

Erreur de syntaxe

SyntaxError

Erreur de parenthèse, : manquant avant un bloc d'instruction....

len([1,2,3))

Erreurs d'indexation

IndexError

Accès à un index non présent dans une liste. Accès à un index non présent dans une liste, ou un tuple, str...

[12,15,14][4]

Erreurs de nom

NameError

Nom de fonction ou de variable mal orthographié.

print(Bonjour) ou prout("Bonjour")

Erreurs d'indentation

IndentationError

Indentation oubliée, ou trop grande, les blocs sont alors mal délimités.

Erreurs de type

TypeError

Opération impossible entre deux types(str - int). Conversion de type impossible.

"3" * "5"


En général, ces erreurs nécessitent de modifier le code pour corriger le «bug».

2. Tour d'horizon des bonnes pratiques de programmation

Pour développer et mettre au point un programme, il vous faudra :

# Erreur de syntaxe, parenthèse fermante manquante
print("Hello, world"  
# Erreur d'indentation
def ma_fonction():
print("Indentation incorrecte")
# Erreur de guillemets
print('J'ai fain')
# Utiliser les mécanismes d'assertions pour s'assurer de la bonne exécution du programme 
def diviser(a, b):
    assert b != 0, "Division par zéro impossible" #
    return a / b

result = diviser(10, 0)  # Déclenche une AssertionError
# Vérifier les boucles
n = 5
while n > 0:
    print(n)
    n += 1
⚠ ∞ !!!!!!!!!!!!!
# Résultat imprévisible en raison de la représentation des nombres flottants
a = 0.1 + 0.1 + 0.1
b = 0.3
print(a == b)
# IndexError : L'indice 5 est hors de la plage valide
ma_liste = [1, 2, 3, 4, 5]
print(ma_liste[5])
# Problèmes liés au typage
x = "42"
y = x + 10

4. Construction de Jeux de Tests :

Pour anticiper les erreurs, il est essentiel de créer des jeux de tests complets qui couvrent divers scénarios d'utilisation du programme. Les tests unitaires, les tests d'intégration et les tests de validation sont tous importants..

Trouver les erreurs ci-dessous :

def ajouter(a, b):
    """
    Cette fonction ajoute deux nombres.
    :param a: Premier nombre
    :param b: Deuxième nombre
    :return: La somme de a et b
    """
    assert isinstance(a, int), "a doit être un nombre entier"
    assert isinstance(b, (int, float)), "b doit être un nombre"
    
    return a + b

# Jeu de tests
assert ajouter(1, 2) == 3
assert ajouter(-1.5, 1) == -0.5
assert ajouter(0, 0) == 0

5. try...except

Parfois ces erreurs "sont prévues" et nécessitent d'être gérées sans arrêter complètement le programme.
Gestion des exceptions avec try : ... except ...

Prenons l'exemple de la gestion d'une entrée utilisateur (on a dit qu'"« il fallait s'attendre à tout ... »")

Vous demandez l'âge d'une personne et vous attendez un entier pour vérifier son accès.

Il faut lui reposer la question jusqu'à ce qu'il rentre une valeur conforme à nos attentes.

try:
    nombre = int("abc")  # Cela va lever une ValueError car "abc" ne peut pas être converti en int.
except ValueError as erreur:
    print("Une erreur de valeur est survenue :", erreur)
# Anticiper et gérer les erreurs de saisie de l'utilisateur
try:
    num = int(input("Entrez un nombre : "))
    print("Vous avez saisi :", num)
except ValueError:
    print("Erreur : Veuillez entrer un nombre valide.") 
age = None
while not age: 
    try:
        age = int(input("Quel âge avez-vous? "))
    except ValueError:
        print("Veuillez entrer votre âge sous forme de chiffres")

# on est sur d'avoir un age entier ici
if age >= 13:
    print("Vous pouvez vous inscrire")
else:
    print("Les réseaux sociaux sont interdits aux moins de 13 ans.")
try:
    # Tentative d'ouverture d'un fichier non-existant
    f = open('non_existent_file.txt', 'r')
except IOError:
    # Ce bloc sera exécuté si l'ouverture du fichier échoue
    print('Erreur: Le fichier ne peut pas être trouvé ou lu.')

Retour au sommaire

5.1 - Algorithme Graphes ✔

Algorithmique

Algorithmes sur les graphes

Capacités Attendue :

Commentaires :

0. Rappels :

import numpy as np
import networkx as nx # nécessaire pour afficher le graphe
import matplotlib.pyplot as plt # nécessaire pour afficher le graphe

class Graphe_Matrice_Adjacente:
    def __init__(self, matrice_adjacence, noms_sommets):
        self.matrice_adjacence = matrice_adjacence
        self.noms_sommets = noms_sommets
        
    def liste_successeurs(self):
        liste_succ = {}
        n = len(self.matrice_adjacence)
        for i in range(n):
            successeurs = []
            for j in range(n):
                if self.matrice_adjacence[i][j]:
                    successeurs.append(self.noms_sommets[j])
            liste_succ[self.noms_sommets[i]] = successeurs
        return liste_succ

    def liste_predecesseurs(self):
        liste_pred = {}
        n = len(self.matrice_adjacence)
        for j in range(n):
            predecesseurs = []
            for i in range(n):
                if self.matrice_adjacence[i][j]:
                    predecesseurs.append(self.noms_sommets[i])
            liste_pred[self.noms_sommets[j]] = predecesseurs
        return liste_pred
    
    
    def afficher(self, figsize=(6, 6)):  # n'est pas demandé dans le cours de NSI
        plt.figure(figsize=figsize)
        G = nx.DiGraph()
        edge_labels = {}
        for i, nom_sommet in enumerate(self.noms_sommets):
            for j, poids in enumerate(self.matrice_adjacence[i]):
                if poids != 0:
                    G.add_edge(nom_sommet, self.noms_sommets[j], weight=poids)
                    edge_labels[(nom_sommet, self.noms_sommets[j])] = poids
        pos = nx.spring_layout(G)
        nx.draw(G, pos, with_labels=True, font_weight='bold', node_color='skyblue', font_size=12, node_size=500, arrows=True)
        nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
        plt.show()
        
matrice_adjacence = [
    [0, 2, 1, 3, 0, 0, 0, 0],  # A
    [2, 0, 0, 0, 1, 3, 0, 0],  # B
    [1, 0, 0, 0, 0, 0, 3, 2],  # C
    [3, 0, 0, 0, 0, 0, 0, 0],  # D
    [0, 1, 0, 0, 0, 0, 0, 0],  # E
    [0, 3, 0, 0, 0, 0, 0, 0],  # F
    [0, 0, 3, 0, 0, 0, 0, 1],  # G
    [0, 0, 2, 0, 0, 0, 1, 0],  # H
]

noms_sommets = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']


graphe = Graphe_Matrice_Adjacente(matrice_adjacence, noms_sommets)
graphe.afficher()
dictionnaire_successeurs = graphe.liste_successeurs()
dictionnaire_predecesseur = graphe.liste_predecesseurs()
print('Successeurs : ', dictionnaire_successeurs)
print('Prédécesseurs : ',dictionnaire_predecesseur)
Successeurs :  {'A': ['B', 'C', 'D'], 'B': ['A', 'E', 'F'], 'C': ['A', 'G', 'H'], 'D': ['A'], 'E': ['B'], 'F': ['B'], 'G': ['C', 'H'], 'H': ['C', 'G']}
Prédécesseurs :  {'A': ['B', 'C', 'D'], 'B': ['A', 'E', 'F'], 'C': ['A', 'G', 'H'], 'D': ['A'], 'E': ['B'], 'F': ['B'], 'G': ['C', 'H'], 'H': ['C', 'G']}

1. DFS et BFS

DFS est l'acronyme de Depth-First Search, qui se traduit en français par Parcours en profondeur d'abord.
Le parcours DFS est un parcours où on va aller «le plus loin possible» sans se préoccuper des autres voisins non visités, ni des poids des arêtes : on va visiter le premier de ses voisins non traités, qui va faire de même, etc. Lorsqu'il n'y a plus de voisin, on revient en arrière pour aller voir le dernier voisin non visité.

BFS est l'acronyme de Breadth-First Search, qui se traduit en français par Parcours en Largeur.
le parcours BFS en selectionnant un nœud et il explore tous les nœuds voisins avant de passer aux nœuds au niveau suivant, indépendamment des poids des arêtes. Un parcours BFS peut donc trouver le plus court chemin entre 2 nœuds, mais en prenant des poids de 1 pour les arêtes.

def DFS(graph,debut):
    result = []
    a_visite = [debut]
    while a_visite:
        noeux=a_visite.pop(0)
        if noeux not in result:
            result.append(noeux)
            a_visite = graph[noeux] + a_visite
    return result

def BFS(graph,debut):
    result = []
    a_visite = [debut]
    while a_visite:
        noeux = a_visite.pop(0)
        if noeux not in result:
            result.append(noeux)
            a_visite = a_visite +  graph[noeux]
    return result

# Exemple de graphe
graph_successeurs =  {'A': ['B', 'C', 'D'],
                      'B': ['A', 'E', 'F'],
                      'C': ['A', 'G', 'H'],
                      'D': ['A'],
                      'E': ['B'], 
                      'F': ['B'], 
                      'G': ['C', 'H'], 
                      'H': ['C', 'G']}

# Lancer la recherche en profondeur à partir de 'A'
resultat = DFS(graph_successeurs, 'A')
print("Ordre de visite DFS :", resultat)
# Lancer le parcours en largeur à partir de 'A'
resultat = BFS(graph_successeurs, 'A')
print("Ordre de visite BFS :", resultat)
Ordre de visite DFS : ['A', 'B', 'E', 'F', 'C', 'G', 'H', 'D']
Ordre de visite BFS : ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']

2. Labyrinthe ou chemin

Dans le contexte d'un labyrinthe, le BFS pour trouve le chemin le plus court (poids = 1) entre le point de départ et le point d'arrivée.

def chemin(graphe, depart, arrivee):
    file = [([depart], [])]  # La file contient des tuples de chemin et de liste d'arêtes utilisées
    chemins = []
    while file:
        chemin_actuel, aretes_utilisees = file.pop(0)
        noeud_actuel = chemin_actuel[-1]
        if noeud_actuel == arrivee and len(chemin_actuel) > 1:
            chemins.append(chemin_actuel)
            continue
        for voisin in graphe.get(noeud_actuel, []):
            arete = (noeud_actuel, voisin)
            if arete not in aretes_utilisees:  # Vérifie si l'arête a déjà été utilisée
                nouvelles_aretes = aretes_utilisees + [arete, (voisin, noeud_actuel)]  # Ajoute l'arête dans les deux sens
                nouveau_chemin = chemin_actuel + [voisin]
                file.append((nouveau_chemin, nouvelles_aretes))
    if chemins:
        print(f"Listes de tous les chemins trouvé(s) de {depart} vers {arrivee} :")
        for chemin in chemins:
            print(f"  - {chemin}")
    else:
        print(f"Aucun chemin de {depart} vers {arrivee} trouvé!")

# Exemples
graph_successeurs =  {'A': ['B', 'C', 'D'],
                      'B': ['A', 'E', 'F'],
                      'C': ['A', 'G', 'H'],
                      'D': ['A'],
                      'E': ['B'], 
                      'F': ['B'], 
                      'G': ['C', 'H'], 
                      'H': ['C', 'G']}

chemin(graph_successeurs, 'G', 'F')
chemin(graph_successeurs, 'C', 'C')
Listes de tous les chemins trouvé(s) de G vers F :
  - ['G', 'C', 'A', 'B', 'F']
  - ['G', 'H', 'C', 'A', 'B', 'F']
Listes de tous les chemins trouvé(s) de C vers C :
  - ['C', 'G', 'H', 'C']
  - ['C', 'H', 'G', 'C']

3. Détection d'un cycle

# Exemples
graph_successeurs =  {'A': ['B', 'C', 'D'],
                      'B': ['A', 'E', 'F'],
                      'C': ['A', 'G', 'H'],
                      'D': ['A'],
                      'E': ['B'], 
                      'F': ['B'], 
                      'G': ['C', 'H'], 
                      'H': ['C', 'G']}

chemin(graph_successeurs, 'D','D')
chemin(graph_successeurs, 'C','C')
Aucun chemin de D vers D trouvé!
Listes de tous les chemins trouvé(s) de C vers C :
  - ['C', 'G', 'H', 'C']
  - ['C', 'H', 'G', 'C']

4. Algorithme de Dijkstra

Image

Edsger Wybe Dijkstra né à Rotterdam le 11 mai 1930 et mort2 à Nuenen le 6 août 2002, est un mathématicien et informaticien néerlandais du XXe siècle. Il reçoit en 1972 le prix Turing pour ses contributions sur la science et l’art des langages de programmation et au langage Algol. Juste avant sa mort, en 2002, il reçoit le prix PoDC de l'article influent, pour ses travaux sur l'autostabilisation. L'année suivant sa mort, le prix sera renommé en son honneur prix Dijkstra.

Exemple : Dijkstra

# Algorithme de Dijkstra orienté avec poids
import networkx as nx
import matplotlib.pyplot as plt
import pandas as pd

class GrapheOrienteAvecPoids:
    def __init__(self, liste_sommets):
        self.liste_sommets = liste_sommets
        self.adjacents = {sommet: {} for sommet in liste_sommets}

    def ajoute_arete(self, sommetA, sommetB, poids):
        self.adjacents[sommetA][sommetB] = poids

    def voisins(self, sommet):
        return self.adjacents[sommet]

    def sont_voisins(self, sommetA, sommetB):
        return sommetB in self.adjacents[sommetA]

    def poids(self, sommetA, sommetB):
        return self.adjacents[sommetA].get(sommetB, None)

    def matrice_adjacente(self):
        matrice = []
        for sommetA in self.liste_sommets:
            ligne = []
            for sommetB in self.liste_sommets:
                ligne.append(self.poids(sommetA, sommetB) if self.sont_voisins(sommetA, sommetB) else 0)
            matrice.append(ligne)
        return matrice

    def liste_successeurs(self):
        return {sommet: list(self.voisins(sommet).keys()) for sommet in self.liste_sommets}

    def liste_predecesseurs(self):
        predecesseurs = {sommet: [] for sommet in self.liste_sommets}
        for sommet in self.liste_sommets:
            for voisin in self.voisins(sommet):
                predecesseurs[voisin].append(sommet)
        return predecesseurs

    def nombre_aretes_par_sommet(self):
        degres_sortants = {sommet: len(self.voisins(sommet)) for sommet in self.liste_sommets}
        degres_entrants = {sommet: 0 for sommet in self.liste_sommets}

        for sommet in self.liste_sommets:
            for voisin in self.voisins(sommet):
                degres_entrants[voisin] += 1

        return {"degres_sortants": degres_sortants, "degres_entrants": degres_entrants}

    def dessiner(self, figsize=(6, 6)):
        plt.figure(figsize=figsize)
        G = nx.DiGraph()
        for sommet, voisins in self.adjacents.items():
            for voisin, poids in voisins.items():
                G.add_edge(sommet, voisin, weight=poids)

        pos = nx.spring_layout(G)
        nx.draw(G, pos, with_labels=True, font_weight='bold', node_color='skyblue', font_size=12, node_size=500, font_color='black')
        labels = nx.get_edge_attributes(G, 'weight')
        nx.draw_networkx_edge_labels(G, pos, edge_labels=labels)
        plt.show()

    def dijkstra_tableau(self, debut, fin=None):
        distances = {sommet: float('infinity') for sommet in self.liste_sommets}
        predecesseurs = {sommet: None for sommet in self.liste_sommets}
        distances[debut] = 0
        selectionnes = set()
        tableau = pd.DataFrame(columns=self.liste_sommets + ['Choix'])

        while len(selectionnes) < len(self.liste_sommets):
            non_selectionnes = {sommet: distances[sommet] for sommet in self.liste_sommets if sommet not in selectionnes}
            sommet_courant = min(non_selectionnes, key=non_selectionnes.get)

            if fin is not None and sommet_courant == fin:
                break

            for voisin, poids in self.voisins(sommet_courant).items():
                if distances[voisin] > distances[sommet_courant] + poids:
                    distances[voisin] = distances[sommet_courant] + poids
                    predecesseurs[voisin] = sommet_courant

            selectionnes.add(sommet_courant)

            ligne_actuelle = [f"X" if sommet in selectionnes else f"{distances[sommet]}v{predecesseurs[sommet]}" if distances[sommet] != float('infinity') else "∞" for sommet in self.liste_sommets]
            ligne_actuelle.append(f"{sommet_courant}({distances[sommet_courant]})")

            tableau = pd.concat([tableau, pd.DataFrame([ligne_actuelle], columns=tableau.columns)], ignore_index=True)

        return tableau, self.calcule_chemin(predecesseurs, debut, fin)

    def calcule_chemin(self, predecesseurs, debut, fin):
        chemin = []
        sommet_actuel = fin
        while sommet_actuel and sommet_actuel != debut:
            chemin.append(sommet_actuel)
            sommet_actuel = predecesseurs[sommet_actuel]
        if sommet_actuel:
            chemin.append(debut)
        chemin.reverse()
        distance = sum([self.poids(chemin[i], chemin[i + 1]) for i in range(len(chemin) - 1)]) if chemin else float('infinity')
        return chemin, distance

def format_chemin(chemin):
    return " → ".join(chemin)


g = GrapheOrienteAvecPoids(['A', 'B', 'C', 'D', 'E', 'F'])
g.ajoute_arete('A', 'B', 8)
g.ajoute_arete('A', 'E', 3)

g.ajoute_arete('B', 'A', 7)
g.ajoute_arete('B', 'F', 10)
g.ajoute_arete('B', 'C', 7)


g.ajoute_arete('C', 'B', 6)
g.ajoute_arete('C', 'D', 10)

g.ajoute_arete('D', 'A', 12)
g.ajoute_arete('D', 'F', 3)

g.ajoute_arete('E', 'D', 11)
g.ajoute_arete('E', 'B', 6)

g.ajoute_arete('F', 'E', 11)
g.ajoute_arete('F', 'C', 1)

g.dessiner()


tableau_dijkstra, chemin_info = g.dijkstra_tableau('A', 'F')
print(tableau_dijkstra)
print()
print("Chemin le plus court", format_chemin(chemin_info[0]), "de distance", chemin_info[1])
   A    B     C     D    E     F  Choix
0  X  8vA     ∞     ∞  3vA     ∞   A(0)
1  X  8vA     ∞  14vE    X     ∞   E(3)
2  X    X  15vB  14vE    X  18vB   B(8)
3  X    X  15vB     X    X  17vD  D(14)
4  X    X     X     X    X  17vD  C(15)

Chemin le plus court A → E → D → F de distance 17
# Création du graphe avec les sommets
graphe = GrapheOrienteAvecPoids(['Paris', 'Lyon', 'Marseille', 'Toulouse'])

# Ajout des arêtes directement avec leurs poids
graphe.ajoute_arete('Paris', 'Lyon', 465)
graphe.ajoute_arete('Lyon','Paris', 465)
graphe.ajoute_arete('Lyon', 'Marseille', 320)
graphe.ajoute_arete('Marseille','Lyon', 320)
graphe.ajoute_arete('Lyon', 'Toulouse', 537)
graphe.ajoute_arete('Toulouse','Lyon', 537)
graphe.ajoute_arete('Marseille', 'Toulouse', 403)
graphe.ajoute_arete('Toulouse', 'Marseille', 403)

# Dessin du graphe
graphe.dessiner()

# Utiliser l'algorithme de Dijkstra pour trouver le chemin le plus court de Paris à Marseille
tableau_dijkstra, chemin_info = graphe.dijkstra_tableau('Paris', 'Marseille')
print(tableau_dijkstra)
print()
print("Chemin le plus court", format_chemin(chemin_info[0]), "de distance", chemin_info[1])

  Paris       Lyon Marseille   Toulouse      Choix
0     X  465vParis         ∞          ∞   Paris(0)
1     X          X  785vLyon  1002vLyon  Lyon(465)

Chemin le plus court Paris → Lyon → Marseille de distance 785

Routage dans internet

Dans le contexte du routage Internet, les nœuds du graphe pourraient représenter des routeurs ou des commutateurs, et les arêtes pourraient représenter les connexions physiques ou virtuelles entre eux. Les poids sur les arêtes pourraient représenter des mesures de latence, de bande passante, ou d'autres métriques de performance de réseau.

# Création du graphe avec les sommets
noms_sommets = ['Routeur1', 'Routeur2', 'Routeur3', 'Routeur4', 'Routeur5', 
                'Ordinateur1', 'Ordinateur2', 'Serveur1', 'Serveur2', 'DNS1', 'DNS2']

matrice_adjacence =    [[0, 2, 3, 0, 0, 1, 0, 0, 0, 0, 0],  # Routeur1
                        [2, 0, 1, 2, 0, 0, 0, 1, 0, 0, 0],  # Routeur2
                        [3, 1, 0, 4, 5, 0, 0, 0, 0, 5, 0],  # Routeur3
                        [0, 2, 4, 0, 3, 0, 0, 0, 2, 0, 0],  # Routeur4
                        [0, 0, 5, 3, 0, 0, 3, 0, 0, 0, 2],  # Routeur5
                        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # Ordinateur1
                        [0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0],  # Ordinateur2
                        [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],  # Serveur1
                        [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0],  # Serveur2
                        [0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0],  # DNS1
                        [0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0]]  # DNS2

graphe = GrapheOrienteAvecPoids(noms_sommets)

# Ajout des arêtes directement
for i, sommetA in enumerate(noms_sommets):
    for j, sommetB in enumerate(noms_sommets):
        poids = matrice_adjacence[i][j]
        if poids > 0:  # Ajouter une arête uniquement si le poids est non nul
            graphe.ajoute_arete(sommetA, sommetB, poids)
            graphe.ajoute_arete(sommetB, sommetA, poids)

# Dessin du graphe
graphe.dessiner(figsize=(8, 8))

# Utiliser l'algorithme de Dijkstra pour trouver le chemin le plus court de Routeur1 à Serveur2
tableau_dijkstra, chemin_info = graphe.dijkstra_tableau('Ordinateur1', 'Serveur2')
print(tableau_dijkstra)
print()
print("Chemin le plus court", format_chemin(chemin_info[0]), "de distance", chemin_info[1])
        Routeur1    Routeur2    Routeur3  ...        DNS1 DNS2           Choix
0  1vOrdinateur1           ∞           ∞  ...           ∞    ∞  Ordinateur1(0)
1              X  3vRouteur1  4vRouteur1  ...           ∞    ∞     Routeur1(1)
2              X           X  4vRouteur1  ...           ∞    ∞     Routeur2(3)
3              X           X           X  ...  9vRouteur3    ∞     Routeur3(4)
4              X           X           X  ...  9vRouteur3    ∞     Serveur1(4)
5              X           X           X  ...  9vRouteur3    ∞     Routeur4(5)

[6 rows x 12 columns]

Chemin le plus court Ordinateur1 → Routeur1 → Routeur2 → Routeur4 → Serveur2 de distance 7

Sujet BAC : Sujet 0A 24-NSIZERO-A Exercice n°3 + Correction

Retour au sommaire

5.2 - Diviser pour régner ✔

Algorithmique

Méthode « diviser pour régner ».

Capacités attendues :
Écrire un algorithme utilisant la méthode « diviser pour régner ».

Commentaires :
La rotation d’une image bitmap d’un quart de tour avec un coût en mémoire constant est un bon exemple. L’exemple du tri fusion permet également d’exploiter la récursivité et d’exhiber un algorithme de coût en $n log_2 n$ dans les pires des cas.

La méthode "diviser pour régner" est une technique algorithmique récursive qui permet de résoudre des problèmes complexes en les divisant en sous-problèmes plus simples, que l'on résout ensuite pour assembler la solution finale. Principe de base :

Exemple 1: Tri Fusion (Merge Sort)

Le tri fusion est un bon exemple pour comprendre la méthode "diviser pour régner". Le problème de trier une liste peut être divisé en deux sous-problèmes de trier deux listes plus petites. Ensuite, on fusionne ces listes triées pour obtenir une liste triée complète.

Pseudo-code :

MergeSort(liste)
  Si la longueur de la liste <= 1
    Retourner liste
  Sinon
    Diviser la liste en deux moitiés : gauche, droite
    gauche_triee = MergeSort(gauche)
    droite_triee = MergeSort(droite)
    Retourner Merge(gauche_triee, droite_triee)

Merge(gauche, droite)
  resultat = []
  Tant que gauche et droite sont non vides
    Si premier élément de gauche < premier élément de droite
      Ajouter premier élément de gauche à resultat
      Supprimer premier élément de gauche
    Sinon
      Ajouter premier élément de droite à resultat
      Supprimer premier élément de droite
  Ajouter tous les éléments restants de gauche et droite à resultat
  Retourner resultat
def merge_sort(arr):
    # Cas de base: si la liste contient 0 ou 1 élément, elle est déjà triée.
    if len(arr) <= 1:
        return arr

    # Divise la liste en deux moitiés
    mid = len(arr) // 2
    left_half = arr[:mid]
    right_half = arr[mid:]

    # Trie récursivement chaque moitié
    sorted_left = merge_sort(left_half)
    sorted_right = merge_sort(right_half)

    # Fusionne les deux moitiés triées
    return merge(sorted_left, sorted_right)

def merge(left, right):
    result = []
    i = j = 0

    # Compare chaque élément des deux listes et ajoute le plus petit à 'result'
    while i < len(left) and j < len(right):
        if left[i] < right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1

    # Ajoute les éléments restants des deux listes (l'un des deux sera vide)
    while i < len(left):
        result.append(left[i])
        i += 1

    while j < len(right):
        result.append(right[j])
        j += 1

    return result

# Test de la fonction
if __name__ == "__main__":
    arr = [4, 3, 8, 2, 7, 1, 5]
    sorted_arr = merge_sort(arr)
    print("Tableau trié :", sorted_arr)

Coût en temps de l'algorithme de tri fusion :


Propriété :
Pour trier une liste de taille n, le coût en temps de l'algorithme de tri fusion est O(n log2 n).

Démonstration :

Soit $N(n)$ le nombre d'opération mis par l'algorithme Tri Fusion (Merge Sort) pour trier une liste de longueur $n \geq 2$.

la divison des listes n'a pas de coût ?

Remontons l'arbre ci-dessus :

k est le plus petit entier tel que $\frac{n}{2^k}\leq1$

$\frac{n}{2^k}\leq1 <=> n \leq 2^k <=> 2^k \geq n <=> k \geq log_2 n$

donc $ k \leq log_2 n + 1$

L'algorithme a besoin de $n$ opérations pour la fusion par niveau,

donc l'algorithme a besoin de $𝑛k$ opérations pour la fusion : $nk \leq n(log_2 n + 1)$

Pour la division des listes et des sous-listes, il y a aussi k niveau, autant de niveau que pour le fusion. Et pour diviser une $p$ listes de taille totale $n$, il faut déplacer $n$ nombres donc il faut n opérations par niveau. donc la aussi $nk$ opérations pour les division des sous-listes.

donc $N(n) = nk + nk \leq 2n(log_2 n + 1)$

donc $N(n) \leq 2n(log_2 n + log_2 n$) car $2 \leq n => 1 \leq log_2 n$

donc $N(n) \leq 4nlog_2 n$

Et donc le coût en terme de temps pour l'algorithme est $O(nlog_2 n)$. CQFD

Rotation d'une image avec un coût en mémoire constant en utilisant "diviser pour régner".

La méthode est illustrée sur le schéma ci-dessous :

Nous utiliserons le module PIL.
L'image est à l'adresse : https://webftts.com/index_fichiers/Monstre.jpg
La taille de l'image est égale à une puissance de 2.

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

def rotate_quarter_turn(image):
    n = image.shape[0]

    # rotation de 1 pixel inutile
    if n == 1:
        return image

    #création d'une image de même taille
    new_image = np.zeros_like(image)

    # division en 4 quadrants
    half = n // 2
    A = image[:half, :half]
    B = image[:half, half:]
    C = image[half:, :half]
    D = image[half:, half:]

    # Permutation des quadrants
    new_image[:half, :half] = rotate_quarter_turn(B)
    new_image[:half, half:] = rotate_quarter_turn(D)
    new_image[half:, :half] = rotate_quarter_turn(A)
    new_image[half:, half:] = rotate_quarter_turn(C)

    return new_image


image = np.array(Image.open('Monstre.jpg'))
rotated_image = rotate_quarter_turn(image)
plt.imshow(rotated_image)
plt.title("Image après rotation récursive de 90 degrés")
plt.show()
Propriété :
La rotation d'une image bitmap d'un quart de tour avec un coût en mémoire constant peut être réalisée en O(n2) pour une image de n x n pixels

Démonstration :

Soit $N(n)$ le nombre d'opération mis par l'algorithme Trotation pour une image de côté $n$.

À chaque étape de la récursion, l'image est divisée en 4 parties plus petites, et la fonction de rotation est appelée 4 fois, une fois pour chaque quadrant.

Si $n$ est la longueur d'un côté de l'image (en supposant une image carrée), la profondeur de la récursion est $k$ tel que : $\frac{n}{2^k}\leq1 <=> n \leq 2^k <=> 2^k \geq n <=> k \geq log_2 n$ donc $ k \leq log_2 n + 1$

A chaque niveau de récursion, il y a 4 fois plus d'appels récursifs qu'au niveau précédent.

Ainsi, le nombre total d'appels récursifs est au maximum : $4^{log_2 n + 1}$

$4^{\log_2(n) + 1} = 4^{\log_2(n)} \times 4^1 = (2^2)^{\log_2(n)} \times 2^2 = 2^{2\log_2(n)} \times 2^2 = n^2 \times 4$

donc le nombre d'appels récurssif est $n^2 \times 4$.

Le coût des opérations dans chaque appel récursif de votre fonction de rotation d'image est constant car bien que la taille de l'image réduise de moitié dans chaque dimension à chaque appel récursif, les opérations effectuées restent les mêmes (diviser en quadrants, échanger les quadrants). Le fait que la taille de l'image soit plus petite ne change pas le nombre d'opérations élémentaires (comme les affectations ou les accès aux indices) qui doivent être effectuées.

Donc comme le coût des opérations dans chaque appel récursif est constant et égal à $c$, alors le nombre total d'opérations est $c \times $nombre d’appels recursifs, c'est-à-dire $c \times n^2 \times 4$.

donc $N(n) = c \times n^2 \times 4 = O(n^2)$

Considérations Pratiques :

Pour les très grandes images, cette méthode pourrait être inefficace en termes d'utilisation de la mémoire et de temps d'exécution, principalement en raison du grand nombre d'appels récursifs.

Retour au sommaire

5.3 - Programmation dynamique ✔

Algorithmique

Programmation dynamique

Capacités attendues :
Utiliser la programmation dynamique pour écrire un algorithme.

Commentaires :

1. Principes Fondamentaux

La programmation dynamique est une méthode algorithmique utilisée pour résoudre des problèmes en décomposant ces derniers en sous-problèmes plus simples. Elle est couramment employée pour résoudre des problèmes d'optimisation, où l'objectif est de trouver la meilleure solution parmi un ensemble de solutions possibles.

La programmation dynamique repose sur deux principes fondamentaux :

2. Exemple : Suite de Fibonacci

Revenons sur ce qui a été vu dans le cours consacré à la récursivité. On vous demandait d'écrire une fonction récursive qui permet de calculer le n ième terme de la suite de Fibonacci. Voici normalement ce que vous aviez dû obtenir :

from tutor import tutor

def Fibonacci1(n) :
  if n < 2 :
    return n
  else :
    return Fibonacci1(n-1)+Fibonacci1(n-2)

Fibonacci1(5)

tutor()

Pour n=6, il est possible d'illustrer le fonctionnement de ce programme avec le schéma ci-dessous :

En observant attentivement le schéma ci-dessus, vous avez remarqué que de nombreux calculs sont inutiles, car effectué 2 fois :

On pourrait donc grandement simplifier le calcul en "mémorisant" le résultat et en le réutilisant quand nécessaire :

from tutor import tutor

def Fibonacci2(n):
    if n < 2:
        return n
    else:
        fib = [0] * (n + 1)
        fib[1] = 1
        for i in range(2, n + 1):
            fib[i] = fib[i - 1] + fib[i - 2]
        return fib[n]

Fibonacci2(6)

tutor()

Complexité temporelle

Complexité spatiale

Le coût en mémoire, aussi connu sous le terme de complexité spatiale, fait référence à la quantité de mémoire utilisée par un algorithme ou un programme informatique pendant son exécution. Ce coût est généralement mesuré en termes de la quantité de mémoire requise par rapport à la taille des données d'entrée.

Bien que la nature de l'utilisation de la mémoire est différente, la programmation dynamique utilise un espace mémoire pour le stockage de données (le tableau), tandis que la méthode récursive utilise la pile d'appels, les deux méthodes ont une complexité spatiale théorique de $O(n)$;

3. Problème du rendu de monaie

Le problème du rendu de monaie consiste à trouver le nombre minimal de pièces nécessaires pour rendre une somme donnée, en utilisant un ensemble prédéfini de valeurs de pièces.

Supposons que nous ayons des pièces de 1, 2, et 5 euros.
Le but est de rendre la somme de 11 euros en utilisant le moins de pièces possible.

def rendu_monaie_recursive(somme, pieces):
    if somme == 0:
        return 0
    min_pieces = float('inf')
    for piece in pieces:
        if somme >= piece:
            num_pieces = rendu_monaie_recursive(somme - piece, pieces)
            if num_pieces != float('inf') and num_pieces + 1 < min_pieces:
                min_pieces = num_pieces + 1
    return min_pieces

pieces = [1, 2, 5]
somme = 11
result = rendu_monaie_recursive(somme, pieces)
print(f"Nombre minimal de pièces nécessaires pour rendre {somme} euros : {result}")
Nombre minimal de pièces nécessaires pour rendre 11 euros : 3
def rendu_monaie_dynamique(somme,pieces):
    tableau = [float('inf')] * (somme + 1)
    tableau[0] = 0
  
    for i in range(1, somme + 1):
        for piece in pieces:
            if i >= piece:
                tableau[i] = min(tableau[i], tableau[i - piece] + 1)

    return tableau[somme]

pieces = [1, 2, 5]
somme = 11
result = rendu_monaie_dynamique(somme, pieces)
print(f"Nombre minimal de pièces nécessaires pour rendre {somme} euros : {result}")
Nombre minimal de pièces nécessaires pour rendre 11 euros : 3

Complexité temporelle

Cependant, cette estimation est assez grossière. En réalité, la complexité dépendra de la façon dont les valeurs de pièces disponibles se combinent pour former la somme cible. Dans le pire des cas, cela peut être de l'ordre $O(m^n)$.

Complexité spatiale

Pour la programmation dynamique,pour une somme $n$, le nombre de mémoire est la taille du tableau $n+1$. Pour la programmation reccursive, le nombre d'appel recursif est au pire $n$. Doncune un pile de taille $n$. Donc là encore, les deux méthodes ont une complexité spatiale théorique de $O(n)$.

Mais, il est important de noter que, dans la pratique, l'approche récursive peut conduire à une très grande quantité d'appels récursifs pour de grandes sommes, ce qui peut entraîner un dépassement de pile (stack overflow) en raison de la limite de taille de la pile d'appels.

4. Alignement de séquences

La programmation dynamique est également fréquemment utilisée dans les problèmes d'alignement de séquences, comme l'alignement de séquences d'ADN ou de protéines. Un algorithme célèbre pour cela est l'algorithme de Needleman-Wunsch pour l'alignement de séquences globales ou l'algorithme de Smith-Waterman pour l'alignement de séquences locales.

4.1 Algorithme de Needleman-Wunsch (Alignement global)

Le concept d'alignement global est comme un jeu de puzzle où vous avez deux chaînes de caractères et vous essayez de les faire "correspondre" du mieux possible.

Pour faire cet alignement, nous utilisons des algorithmes qui calculent le "meilleur" alignement possible en fonction d'un "score". Ce score est calculé en fonction des règles que vous définissez. Par exemple, vous pourriez dire :

+1 point si les lettres sont identiques (match).
-1 point si les lettres sont différentes (mismatch).
-1 point si vous devez insérer un "trou" ou "gap" (représenté par un tiret '-') pour faire correspondre les lettres.

L'algorithme examine toutes les possibilités et trouve l'alignement qui donne le score le plus élevé.

Lors du remplissage de la matrice d'alignement, l'une des trois actions possibles :

Le Move choisie et celui qui donne le meilleur score.

def needleman_wunsch_with_traceback(seq1, seq2, match=1, mismatch=-1, gap=-1):
    n, m = len(seq1), len(seq2)
    dp = [[0] * (m + 1) for _ in range(n + 1)]
    traceback = [[None] * (m + 1) for _ in range(n + 1)]

    for i in range(n + 1):
        dp[i][0] = i * gap
    for j in range(m + 1):
        dp[0][j] = j * gap
    
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            match_score = match if seq1[i - 1] == seq2[j - 1] else mismatch
            choices = [dp[i-1][j-1] + match_score, dp[i-1][j] + gap, dp[i][j-1] + gap]
            dp[i][j] = max(choices)
            traceback[i][j] = choices.index(dp[i][j])

    align1, align2 = "", ""
    i, j = n, m

    while i > 0 or j > 0:
        if traceback[i][j] == 0:  # Diagonal move
            align1 = seq1[i - 1] + align1
            align2 = seq2[j - 1] + align2
            i -= 1
            j -= 1
        elif traceback[i][j] == 1:  # Vertical move
            align1 = seq1[i - 1] + align1
            align2 = '-' + align2
            i -= 1
        else:  # Horizontal move
            align1 = '-' + align1
            align2 = seq2[j - 1] + align2
            j -= 1
    
    return align1, align2, dp[-1][-1]

# Test
align1, align2, score = needleman_wunsch_with_traceback("GATTACA", "GCATGCU", match=1, mismatch=-1, gap=-1)
print("Score:", score)
print(align1)
print(align2)
Score: 0
G-ATTACA
GCA-TGCU

4.2 Algorithme de Smith-Waterman (Alignement local)

L'alignement local est une autre variante de l'alignement de séquences, mais contrairement à l'alignement global, il cherche à identifier les régions de séquences qui sont les plus similaires, même si ces régions sont de petite taille.Dans un alignement local, le calcul du score se focalise uniquement sur les segments les plus similaires des deux séquences, plutôt que d'essayer d'aligner les séquences dans leur intégralité. En d'autres termes, le but est de trouver la meilleure correspondance possible entre certaines parties des séquences, sans se soucier du reste.

def smith_waterman_with_traceback(seq1, seq2, match=1, mismatch=-1, gap=-1):
    n, m = len(seq1), len(seq2)
    dp = [[0] * (m + 1) for _ in range(n + 1)]
    traceback = [[None] * (m + 1) for _ in range(n + 1)]
    
    max_score = 0
    max_i, max_j = 0, 0

    for i in range(1, n + 1):
        for j in range(1, m + 1):
            match_score = match if seq1[i - 1] == seq2[j - 1] else mismatch
            choices = [0, dp[i-1][j-1] + match_score, dp[i-1][j] + gap, dp[i][j-1] + gap]
            dp[i][j] = max(choices)
            traceback[i][j] = choices.index(dp[i][j])

            if dp[i][j] >= max_score:
                max_score = dp[i][j]
                max_i, max_j = i, j
    
    align1, align2 = "", ""
    i, j = max_i, max_j

    while dp[i][j] != 0:
        if traceback[i][j] == 1:  # Diagonal move
            align1 = seq1[i - 1] + align1
            align2 = seq2[j - 1] + align2
            i -= 1
            j -= 1
        elif traceback[i][j] == 2:  # Vertical move
            align1 = seq1[i - 1] + align1
            align2 = '-' + align2
            i -= 1
        else:  # Horizontal move
            align1 = '-' + align1
            align2 = seq2[j - 1] + align2
            j -= 1
    
    return align1, align2, max_score

# Test
align1, align2, score = smith_waterman_with_traceback("GATTACA", "GCATGCU", match=1, mismatch=-1, gap=-1)
print("Score:", score)
print(align1)
print(align2)
Score: 2
CA
CA

Complexité temporelle

Pour les algorithmes d'alignement de Needleman-Wunsch (alignement global) et Smith-Waterman (alignement local), la complexité est $O(n×m)$$n$ et $m$ sont les longueurs des deux séquences à aligner. Car, on doit approximativement $n×m$ calculs pour remplir la matrice d'alignement. Chaque cellule de cette matrice $n×m$ nécessite un calcul pour déterminer son score optimal en fonction des cellules adjacentes. Donc si vous avez deux séquences de longueurs $n$ et $m$ le nombre total de calculs sera proportionnel au produit de ces deux longueurs, soit $n×m$.

Complexité Spatiale :

Pour les algorithmes d'alignement de Needleman-Wunsch (alignement global) et Smith-Waterman (alignement local), la complexité spatiale est le nombre total de cellules dans la matrice, qui est $(n+1)× (m+1)$. Donc la complexité spatiale est aussi $O(n × m)$.

source : Pixees

Retour au sommaire

5.4 - Recherche textuelle ✔

Algorithmique

Recherche textuelle

Capacités attendues :
Étudier l’algorithme de Boyer-Moore pour la recherche d’un motif dans un texte.

Commentaires :

1. Recherche simple

def recherche_simple(texte, motif):
    indices = []
    i = 0
    while i <= len(texte) - len(motif):
        k = 0
        while k < len(motif) and texte[i+k] == motif[k]:
            k = k + 1
        if k == len(motif):
            indices.append(i)
        i = i + 1
    return indices

texte = "UNE MAGNIFIQUE MAISON BLEUE, UNE MAISON AVEC UN TOIT ROUGE." 
motif = "MAISON"
print("Le motif a été trouvé aux positions :", recherche_simple(texte,motif))
Le motif a été trouvé aux positions : [15, 33]

2. Algorithme de Boyer-Moore

L'idée est d'améliorer le code précédent en sautant directement au prochain endroit potentiellement valide.

Explication du fonctionnement de l'algorithme

def recherche_boyer_moore(texte, motif):
    lm = len(motif)
    table = {}
    for p in range(lm - 1):
        table[motif[p]] = p

    positions = []
    i = lm - 1
    while i < len(texte):
        k = 0
        while k < lm and motif[lm - 1 - k] == texte[i - k]:
            k = k + 1

        if k == lm:
            positions.append(i - lm + 1)
        if texte[i] in table:
            i = i + lm - 1 - table[texte[i]]
        else:
            i = i + lm
    return positions

texte = "UNE MAGNIFIQUE MAISON BLEUE, UNE MAISON AVEC UN TOIT ROUGE."
motif = "MAISON"
print("Le motif a été trouvé aux positions :", recherche_boyer_moore(texte, motif))

texte = "MAMAMAMAMAMAMAMAMAMAMAMAMA"
motif = "MAMA"
print("Le motif a été trouvé aux positions :", recherche_boyer_moore(texte, motif))
Le motif a été trouvé aux positions : [15, 33]
Le motif a été trouvé aux positions : [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]

Remarque :

Plus le motif recherché est long, plus la recherche est rapide.

Le Projet Gutenberg permet de télécharger légalement des ouvrages libres de droits dans différents formats.

Vérifions notre remarque, avec le Tome 1 du roman Les Misérables de Victor Hugo, à télécharger ici ici au format txt, puis à importer dans basthon.

# Ouvre le fichier en mode de lecture ('r')
with open('Les_Miserables.txt', 'r') as fichier:
    # Lit le contenu du fichier et le stocke dans la variable 'contenu'
    texte = fichier.read()

print(texte[0:1000])
Chapitre I

Monsieur Myriel


En 1815, M. Charles-François-Bienvenu Myriel était évêque de Digne.
C'était un vieillard d'environ soixante-quinze ans; il occupait le siège
de Digne depuis 1806.

Quoique ce détail ne touche en aucune manière au fond même de ce que
nous avons à raconter, il n'est peut-être pas inutile, ne fût-ce que
pour être exact en tout, d'indiquer ici les bruits et les propos qui
avaient couru sur son compte au moment où il était arrivé dans le
diocèse. Vrai ou faux, ce qu'on dit des hommes tient souvent autant de
place dans leur vie et surtout dans leur destinée que ce qu'ils font. M.
Myriel était fils d'un conseiller au parlement d'Aix; noblesse de robe.
On contait de lui que son père, le réservant pour hériter de sa charge,
l'avait marié de fort bonne heure, à dix-huit ou vingt ans, suivant un
usage assez répandu dans les familles parlementaires. Charles Myriel,
nonobstant ce mariage, avait, disait-on, beaucoup fait parler de lui. Il
était bien fait de sa personne
import time
def recherche_time(texte, motif):
    début = time.time()
    indices = recherche_simple(texte,motif)
    fin = time.time()
    print('indices :',indices)
    print('temps :',fin-début)
    print()
    
recherche_time(texte,"Jean Valjean avait laissé, le plus d'argent possible aux pauvres")
recherche_time(texte,"maison")
indices : [654457]
temps : 2.065999984741211

indices : [7264, 9090, 9547, 9745, 10936, 17820, 23978, 38192, 41639, 41651, 41840, 42493, 48028, 48393, 51448, 53353, 70867, 72692, 72768, 75608, 77855, 108489, 115739, 130629, 132983, 138870, 143681, 144600, 153114, 155973, 158709, 160700, 163649, 169164, 169181, 171761, 171967, 182642, 186413, 190534, 219378, 220314, 224518, 225098, 227579, 296302, 345108, 345893, 346740, 349677, 359727, 362025, 389945, 395690, 434118, 438068, 457795, 457886, 464696, 469403, 501768, 514980, 520667, 520878, 520926, 520968, 522707, 529329, 598128, 601390, 645915]
temps : 2.116000175476074

import time
def recherche_time(texte, motif):
    début = time.time()
    indices = recherche_boyer_moore(texte,motif)
    fin = time.time()
    print('indices :',indices)
    print('temps :',fin-début)
    print()
    
recherche_time(texte,"Jean Valjean avait laissé, le plus d'argent possible aux pauvres")
recherche_time(texte,"maison")
indices : [654457]
temps : 0.03899979591369629

indices : [7264, 9090, 9547, 9745, 10936, 17820, 23978, 38192, 41639, 41651, 41840, 42493, 48028, 48393, 51448, 53353, 70867, 72692, 72768, 75608, 77855, 108489, 115739, 130629, 132983, 138870, 143681, 144600, 153114, 155973, 158709, 160700, 163649, 169164, 169181, 171761, 171967, 182642, 186413, 190534, 219378, 220314, 224518, 225098, 227579, 296302, 345108, 345893, 346740, 349677, 359727, 362025, 389945, 395690, 434118, 438068, 457795, 457886, 464696, 469403, 501768, 514980, 520667, 520878, 520926, 520968, 522707, 529329, 598128, 601390, 645915]
temps : 0.1119999885559082

import os
repertoire = "."
fichiers = [f for f in os.listdir(repertoire) if os.path.isfile(os.path.join(repertoire, f))]
print(f"fichiers : {fichiers}")

for fichier in fichiers:
    chemin_complet = os.path.join(repertoire, fichier)
    os.remove(chemin_complet)
    print(f"Le fichier {fichier} a été supprimé.")
fichiers : ['Les_Miserables.txt']
Le fichier Les_Miserables.txt a été supprimé.

source : glassus

Retour au sommaire

01 - Hanoï ✔

La tour de Brahma

Dans ses récits de voyages, $B.$ $Rynhe$ $de$ $Tioch$ dit avoir vu, un temple où les moines veillent sur 64 disques sacrés en or.

Tous de tailles différentes, ils sont enfilés sur trois aiguilles de diamant. Au commencement Brahma les confia sur la première aiguille au Mahābhārata avec la mission de les déplacer sur la troisième en respectant les 2 règles intangibles :

  • Un disque ne peut reposer que sur un plus large.
  • Les disques ne peuvent être manipulés qu’un à un d’une aiguille vers une autre.

La tour de Brahma

Nuit et jour, les moines se succèdent sur les marches de l’autel, occupés à transporter la tour de la première aiguille vers la troisième sans s’écarter des règles.

Ils ont en permanence en tête la terrible prophétie : Quand tout sera fini, la tour et les brahmes tomberont, et ce sera la fin des mondes !

Il peut sembler impossible de trouver le nombre de déplacements que les moines effectueront.

Cependant,

Aussi étonnant que cela puisse paraitre, cela suffit pour programmer le calcul du nombre minimal de déplacements à effectuer pour un nombre donné de disques.

  • Lisez attentivement l'algorithme ci-dessous :

L'algorithme "récursif" :

fonction nb_deplace(n):
    """cette fonction reçoit un nombre n de disques
    et renvoie le nombre de déplacements nécessaires """
    si n = 1 :
        renvoyer 1  # 1 disque donc 1 déplacement
    sinon :
        d = nb_deplace(n-1) # Il y a n-1 disques à bouger pour "libérer"  le plus grand.
        renvoyer d + 1 + d 
        # d mouvements pour les n-1 disques au-dessus du grand disque 
        # 1 mouvement pour le disque libéré 
        # d mouvements pour amener les n-1 disques sur le plus grand 

Il s'agit d'une fonction qui s'appelle elle-même, c'est pour cela qu'on la qualifie de récursive.

Lorsque que $n$ est un entier strictement positif, l'algorithme se termine !

En effet,

Attention, un appel de la fonction avec un nombre négatif ou un nombre non entier génère une infinité d'appel de la fonction par elle-même et alors l'algorithme ne se termine jamais.

  • Complétez la traduction de cet algorithme en python.
def nb_deplace(n):
    if n == 1:
        return …
    else :
        d = nb_deplace(…)
        return d + 1 + d

Vérifions les premiers résultats :

On sait qu'une tour de 1 disque est déplacée en 1 mouvement.
nb_deplace(1)
  • Complétez les pointillés ci-dessous jusqu'au calcul du temps nécessaire.

Selon l'algorithme,

nb_deplace(2)

Prenez 2 pièces de monnaie différentes (ou 2 objets superposables), trouvez l'ordre dans lequel elles doivent être déplacées et complétez ce tableau. | mouvement | depuis l'aiguille | vers l'aiguille| |:---|:----:|:-----:| |1| … | … | |2| … | … | |3| … | … |

Selon l'algorithme,

nb_deplace(3)

Prenez 3 pièces de monnaie différentes (ou 3 objets superposables), trouvez l'ordre dans lequel elles doivent être déplacées et complétez ce tableau. Ajoutez des lignes autant que nécessaire. | mouvement | depuis l'aiguille | vers l'aiguille| |:---|:----:|:-----:| |1| … | … | |2| … | … | |3| … | … | |4| … | … | |5| … | … | |6| … | … |

Comptez le nombre de déplacements sur cette animation pour 4 disques et vérifiez que la fonction donne le même nombre.

avec 4 disques

nb_deplace(4)

Le nombre de déplacements indiqué par la fonction semble correct.

  • Calculez le nombre de déplacement pour la tour de Brahma.
nb_deplace(…) # Brahma a confié 64 disques au Mahābhārata 

Le temps nécessaire

Comme le nombre de déplacements est grand, pour nous faire une idée, décidons qu'un disque est déplacé en une seconde.

  • Écrivez ci-dessous le calcul donnant le nombre de disques déplacés en une année.

Voici quelques points de repère :

  • L'homme a inventé l'écriture il y a 5 à 6 mille ans.
  • L'homme serait apparu (en fait une très lente évolution de plusieurs centaines de milliers d'années) sur terre, il y a 3 millions d'années.
  • L'âge de la terre est estimé à 4,5 milliards d'années.
  • L'âge du système solaire est estimé à 4,6 milliards d'années.
  • D'ici environ 5 milliards d'années le soleil aura considérablement grossi, il sera devenu une géante rouge et aura englouti la terre.
  • un travail récent de l'Université de Cornell estime l'âge de l'univers à 13,7 milliards d'années.
  • Que pensez-vous de la prophétie : « Quand tout sera fini, la tour et les brahmes tomberont, et ce sera la fin des mondes ! » ?
# vous pouvez faire des calculs ici

Cette situation a été créée dans des termes très proches par Edouard Lucas en 1889.
Merci à Bruno HENRY

Grand Oral

Grand Oral Les tours de Hanoï

Python

# Résulution de Chatgpt pour la tour de Hanoï

def hanoi(n, source, target, auxiliary, towers):
    if n == 1:
        disk = towers[source].pop()
        towers[target].append(disk)
        draw_towers(towers)
        return

    hanoi(n-1, source, auxiliary, target, towers)

    disk = towers[source].pop()
    towers[target].append(disk)
    draw_towers(towers)

    hanoi(n-1, auxiliary, target, source, towers)

def draw_towers(towers):
    for name, tower in towers.items():
        print(f"{name}: {tower}")
    print("-----")

def init_towers(n):
    towers = {
        'A': list(range(n, 0, -1)),  # [n, n-1, ..., 2, 1]
        'B': [],
        'C': []
    }
    return towers

n = int(input("Enter the number of disks: "))  # Get the number of disks from the user
towers = init_towers(n)

draw_towers(towers)  # Draw initial state
hanoi(n, 'A', 'C', 'B', towers)
Enter the number of disks: 5
A: [5, 4, 3, 2, 1]
B: []
C: []
-----
A: [5, 4, 3, 2]
B: []
C: [1]
-----
A: [5, 4, 3]
B: [2]
C: [1]
-----
A: [5, 4, 3]
B: [2, 1]
C: []
-----
A: [5, 4]
B: [2, 1]
C: [3]
-----
A: [5, 4, 1]
B: [2]
C: [3]
-----
A: [5, 4, 1]
B: []
C: [3, 2]
-----
A: [5, 4]
B: []
C: [3, 2, 1]
-----
A: [5]
B: [4]
C: [3, 2, 1]
-----
A: [5]
B: [4, 1]
C: [3, 2]
-----
A: [5, 2]
B: [4, 1]
C: [3]
-----
A: [5, 2, 1]
B: [4]
C: [3]
-----
A: [5, 2, 1]
B: [4, 3]
C: []
-----
A: [5, 2]
B: [4, 3]
C: [1]
-----
A: [5]
B: [4, 3, 2]
C: [1]
-----
A: [5]
B: [4, 3, 2, 1]
C: []
-----
A: []
B: [4, 3, 2, 1]
C: [5]
-----
A: [1]
B: [4, 3, 2]
C: [5]
-----
A: [1]
B: [4, 3]
C: [5, 2]
-----
A: []
B: [4, 3]
C: [5, 2, 1]
-----
A: [3]
B: [4]
C: [5, 2, 1]
-----
A: [3]
B: [4, 1]
C: [5, 2]
-----
A: [3, 2]
B: [4, 1]
C: [5]
-----
A: [3, 2, 1]
B: [4]
C: [5]
-----
A: [3, 2, 1]
B: []
C: [5, 4]
-----
A: [3, 2]
B: []
C: [5, 4, 1]
-----
A: [3]
B: [2]
C: [5, 4, 1]
-----
A: [3]
B: [2, 1]
C: [5, 4]
-----
A: []
B: [2, 1]
C: [5, 4, 3]
-----
A: [1]
B: [2]
C: [5, 4, 3]
-----
A: [1]
B: []
C: [5, 4, 3, 2]
-----
A: []
B: []
C: [5, 4, 3, 2, 1]
-----

Retour au sommaire

02 - Hanoî (Correction) ✔

La tour de Brahma - Correction

Dans ses récits de voyages, $B.$ $Rynhe$ $de$ $Tioch$ dit avoir vu, un temple où les moines veillent sur 64 disques sacrés en or.

Tous de tailles différentes, ils sont enfilés sur trois aiguilles de diamant. Au commencement Brahma les confia sur la première aiguille au Mahābhārata avec la mission de les déplacer sur la troisième en respectant les 2 règles intangibles :

  • Un disque ne peut reposer que sur un plus large.
  • Les disques ne peuvent être manipulés qu’un à un d’une aiguille vers une autre.

La tour de Brahma

Nuit et jour, les moines se succèdent sur les marches de l’autel, occupés à transporter la tour de la première aiguille vers la troisième sans s’écarter des règles.

Ils ont en permanence en tête la terrible prophétie : Quand tout sera fini, la tour et les brahmes tomberont, et ce sera la fin des mondes !

Il peut sembler impossible de trouver le nombre de déplacements que les moines effectueront.

Cependant,

Aussi étonnant que cela puisse paraitre, cela suffit pour programmer le calcul du nombre minimal de déplacements à effectuer pour un nombre donné de disques.

  • Lisez attentivement l'algorithme ci-dessous :

L'algorithme "récursif" :

fonction nb_deplace(n):
    """cette fonction reçoit un nombre n de disques
    et renvoie le nombre de déplacements nécessaires """
    si n = 1 :
        renvoyer 1  # 1 disque donc 1 déplacement
    sinon :
        d = nb_deplace(n-1) # Il y a n-1 disques à bouger pour "libérer"  le plus grand.
        renvoyer d + 1 + d 
        # d mouvements pour les n-1 disques au-dessus du grand disque 
        # 1 mouvement pour le disque libéré 
        # d mouvements pour amener les n-1 disques sur le plus grand 

Il s'agit d'une fonction qui s'appelle elle-même, c'est pour cela qu'on la qualifie de récursive.

Lorsque que $n$ est un entier strictement positif, l'algorithme se termine !

En effet,

Attention, un appel de la fonction avec un nombre négatif ou un nombre non entier génère une infinité d'appel de la fonction par elle-même et alors l'algorithme ne se termine jamais.

  • Complétez la traduction de cet algorithme en python.
def nb_deplace(n):
    if n == 1:
        return 1
    else :
        d = nb_deplace(n-1)
        return d + 1 + d

Vérifions les premiers résultats :

On sait qu'une tour de 1 disque est déplacée en 1 mouvement.
nb_deplace(1)
1
  • Complétez les pointillés ci-dessous jusqu'au calcul du temps nécessaire.

Selon l'algorithme,

nb_deplace(2)
3

Prenez 2 pièces de monnaie différentes (ou 2 objets superposables), trouvez l'ordre dans lequel elles doivent être déplacées et complétez ce tableau. | mouvement | depuis l'aiguille | vers l'aiguille| |:---|:----:|:-----:| |1| 1 | 2 | |2| 1 | 3 | |3| 2 | 3 |

Selon l'algorithme,

nb_deplace(3)
7

Une tour de 3 disques est déplacée en 7 mouvements.

Prenez 3 pièces de monnaie différentes (ou 3 objets superposables), trouvez l'ordre dans lequel elles doivent être déplacées et complétez ce tableau. Ajoutez des lignes autant que nécessaire. | mouvement | depuis l'aiguille | vers l'aiguille| |:---|:----:|:-----:| |1| 1 | 3 | |2| 1 | 2 | |3| 3 | 2 | |4| 1 | 3 | |5| 2 | 1 | |6| 2 | 3 | |7| 1 | 3 |

Comptez le nombre de déplacements sur cette animation pour 4 disques et vérifiez que la fonction donne le même nombre.

avec 4 disques

nb_deplace(4)
15

Le nombre de déplacements indiqué par la fonction semble correct.

  • Calculez le nombre de déplacement pour la tour de Brahma.
nb_deplace(64) # Brahma a confié 64 disques au Mahābhārata 
18446744073709551615

Le temps nécessaire

Comme le nombre de déplacements est grand, pour nous faire une idée, décidons qu'un disque est déplacé en une seconde.

  • Écrivez ci-dessous le calcul donnant le nombre de disques déplacés en une année.
nb_deplace(64)/(60*60*24*365)
584942417355.072

Voici quelques points de repère :

  • L'homme a inventé l'écriture il y a 5 à 6 mille ans.
  • L'homme serait apparu (en fait une très lente évolution de plusieurs centaines de milliers d'années) sur terre, il y a 3 millions d'années.
  • L'âge de la terre est estimé à 4,5 milliards d'années.
  • L'âge du système solaire est estimé à 4,6 milliards d'années.
  • D'ici environ 5 milliards d'années le soleil aura considérablement grossi, il sera devenu une géante rouge et aura englouti la terre.
  • un travail récent de l'Université de Cornell estime l'âge de l'univers à 13,7 milliards d'années.
  • Que pensez-vous de la prophétie : « Quand tout sera fini, la tour et les brahmes tomberont, et ce sera la fin des mondes ! » ?
print(nb_deplace(64)/(60*60*24*365*10**9), " milliards d'années")
584.942417355072  milliards d'années
La fin de l'univers aura lieu avant.

Cette situation a été créée dans des termes très proches par Edouard Lucas en 1889.
Merci à Bruno HENRY

Grand Oral

Grand Oral Les tours de Hanoï

Python

# Résulution de Chatgpt pour la tour de Hanoï

def hanoi(n, source, target, auxiliary, towers):
    if n == 1:
        disk = towers[source].pop()
        towers[target].append(disk)
        draw_towers(towers)
        return

    hanoi(n-1, source, auxiliary, target, towers)

    disk = towers[source].pop()
    towers[target].append(disk)
    draw_towers(towers)

    hanoi(n-1, auxiliary, target, source, towers)

def draw_towers(towers):
    for name, tower in towers.items():
        print(f"{name}: {tower}")
    print("-----")

def init_towers(n):
    towers = {
        'A': list(range(n, 0, -1)),  # [n, n-1, ..., 2, 1]
        'B': [],
        'C': []
    }
    return towers

n = int(input("Enter the number of disks: "))  # Get the number of disks from the user
towers = init_towers(n)

draw_towers(towers)  # Draw initial state
hanoi(n, 'A', 'C', 'B', towers)
Enter the number of disks: 3
A: [3, 2, 1]
B: []
C: []
-----
A: [3, 2]
B: []
C: [1]
-----
A: [3]
B: [2]
C: [1]
-----
A: [3]
B: [2, 1]
C: []
-----
A: []
B: [2, 1]
C: [3]
-----
A: [1]
B: [2]
C: [3]
-----
A: [1]
B: []
C: [3, 2]
-----
A: []
B: []
C: [3, 2, 1]
-----

Retour au sommaire

03 - Jeu 1 ✔

Tic Tac Toc - Puissance 4 - Tour de Hanoï

Tic Tac Toc vs Computer - Récursivité

Vidéo : WarGames 1983

# Évaluation des états du tableau
def evaluate(board):
    for row in board:
        if row[0] == row[1] == row[2]:
            if row[0] == "X":
                return 10
            elif row[0] == "O":
                return -10

    for col in range(3):
        if board[0][col] == board[1][col] == board[2][col]:
            if board[0][col] == "X":
                return 10
            elif board[0][col] == "O":
                return -10

    if board[0][0] == board[1][1] == board[2][2]:
        if board[0][0] == "X":
            return 10
        elif board[0][0] == "O":
            return -10

    if board[0][2] == board[1][1] == board[2][0]:
        if board[0][2] == "X":
            return 10
        elif board[0][2] == "O":
            return -10

    return 0

# Fonction Minimax
def minimax(board, depth, maximizing):
    score = evaluate(board)

    if score == 10:
        return score

    if score == -10:
        return score

    if not any("_" in row for row in board):
        return 0

    if maximizing:
        maxEval = float("-inf")
        for i in range(3):
            for j in range(3):
                if board[i][j] == "_":
                    board[i][j] = "X"
                    eval = minimax(board, depth + 1, False)
                    board[i][j] = "_"
                    maxEval = max(maxEval, eval)
        return maxEval

    else:
        minEval = float("inf")
        for i in range(3):
            for j in range(3):
                if board[i][j] == "_":
                    board[i][j] = "O"
                    eval = minimax(board, depth + 1, True)
                    board[i][j] = "_"
                    minEval = min(minEval, eval)
        return minEval

def findBestMove(board):
    bestValue = float("-inf")
    bestMove = (-1, -1)

    for i in range(3):
        for j in range(3):
            if board[i][j] == "_":
                board[i][j] = "X"
                moveValue = minimax(board, 0, False)
                board[i][j] = "_"
                if moveValue > bestValue:
                    bestMove = (i, j)
                    bestValue = moveValue

    return bestMove



# Fonction principale pour jouer à Tic-Tac-Toe
if __name__ == "__main__":
    board = [
        ["_", "_", "_"],
        ["_", "_", "_"],
        ["_", "_", "_"]
    ]

    pad_to_idx = {
        "1": (0, 0), "2": (0, 1), "3": (0, 2),
        "4": (1, 0), "5": (1, 1), "6": (1, 2),
        "7": (2, 0), "8": (2, 1), "9": (2, 2)
    }
    for row in board:
        print(" ".join(row))
    print()
    
    while True:
        move = input("Enter your move (1-9 like the numeric pad) or q to quit: ")

        if move.lower() == 'q':
            print("Game terminated by user.")
            break

        if move in pad_to_idx:
            x, y = pad_to_idx[move]

            if board[x][y] not in ["X", "O"]:
                board[x][y] = "O"
                
                # Afficher le plateau après le coup de l'utilisateur
                print("Your move:")
                for row in board:
                    print(" ".join(row))
                print()

                if evaluate(board) == -10:
                    print("You win!")
                    break

                bestMove = findBestMove(board)
                board[bestMove[0]][bestMove[1]] = "X"

                if evaluate(board) == 10:
                    print("Computer wins!")
                    for row in board:
                        print(" ".join(row))
                    print()
                    break

                if not any("_" in row for row in board):
                    print("It's a draw!")
                    for row in board:
                        print(" ".join(row))
                    print()
                    break

                # Afficher le plateau après le coup de l'ordinateur
                print("Computer played:")
                for row in board:
                    print(" ".join(row))
                print()

            else:
                print("Invalid move. Try again.")
        else:
            print("Invalid number. Try again.")




_ _ _
_ _ _
_ _ _

Puissance 4 avec choix de la profondeur de récursivité

import copy

def print_board(board):
    for row in board:
        print(" ".join(row))
    print("0 1 2 3 4 5 6\n")
    print()  # Ajout d'une ligne vide pour la clarté

def is_valid_move(board, col):
    return board[0][col] == "-"

def make_move(board, col, player):
    for i in reversed(range(len(board))):
        if board[i][col] == "-":
            board[i][col] = player
            return True
    return False

def is_winner(board, player):
    for row in range(6):
        for col in range(7):
            if board[row][col] == player:
                # Vérifiez à l'horizontale
                if col <= 3 and all(board[row][c] == player for c in range(col, col + 4)):
                    return True
                # Vérifiez à la verticale
                if row <= 2 and all(board[r][col] == player for r in range(row, row + 4)):
                    return True
                # Vérifiez la diagonale vers la droite
                if row <= 2 and col <= 3 and all(board[row + r][col + c] == player for r, c in zip(range(4), range(4))):
                    return True
                # Vérifiez la diagonale vers la gauche
                if row <= 2 and col >= 3 and all(board[row + r][col - c] == player for r, c in zip(range(4), range(4))):
                    return True
    return False


def minimax(board, depth, maximizing, alpha, beta):
    if is_winner(board, "X"):
        return -10
    if is_winner(board, "O"):
        return 10
    if all(cell != "-" for row in board for cell in row) or depth == 0:
        return 0

    if maximizing:
        max_eval = float('-inf')
        for col in range(7):
            if is_valid_move(board, col):
                new_board = copy.deepcopy(board)
                make_move(new_board, col, "O")
                eval = minimax(new_board, depth-1, False, alpha, beta)
                max_eval = max(max_eval, eval)
                alpha = max(alpha, eval)
                if beta <= alpha:
                    break
        return max_eval
    else:
        min_eval = float('inf')
        for col in range(7):
            if is_valid_move(board, col):
                new_board = copy.deepcopy(board)
                make_move(new_board, col, "X")
                eval = minimax(new_board, depth-1, True, alpha, beta)
                min_eval = min(min_eval, eval)
                beta = min(beta, eval)
                if beta <= alpha:
                    break
        return min_eval

if __name__ == "__main__":
    board = [["-" for _ in range(7)] for _ in range(6)]
    depth = int(input("Entrez la profondeur de recherche pour l'ordinateur: "))

    while True:
        print_board(board)
        col = int(input("Votre coup (0-6): "))
        if is_valid_move(board, col):
            make_move(board, col, "X")
            if is_winner(board, "X"):
                print_board(board)
                print("Vous avez gagné!")
                break

        best_val = float('-inf')
        best_col = 0
        for col in range(7):
            if is_valid_move(board, col):
                new_board = copy.deepcopy(board)
                make_move(new_board, col, "O")
                move_val = minimax(new_board, depth, False, float('-inf'), float('inf'))
                if move_val > best_val:
                    best_col = col
                    best_val = move_val
        make_move(board, best_col, "O")

        if is_winner(board, "O"):
            print_board(board)
            print("L'ordinateur a gagné!")
            break
Entrez la profondeur de recherche pour l'ordinateur: 5
- - - - - - -
- - - - - - -
- - - - - - -
- - - - - - -
- - - - - - -
- - - - - - -
0 1 2 3 4 5 6


Votre coup (0-6): 0
- - - - - - -
- - - - - - -
- - - - - - -
- - - - - - -
O - - - - - -
X - - - - - -
0 1 2 3 4 5 6


Votre coup (0-6): 1
- - - - - - -
- - - - - - -
- - - - - - -
O - - - - - -
O - - - - - -
X X - - - - -
0 1 2 3 4 5 6


Votre coup (0-6): 2
- - - - - - -
- - - - - - -
- - - - - - -
O - - - - - -
O - - - - - -
X X X O - - -
0 1 2 3 4 5 6


Votre coup (0-6): 0
- - - - - - -
O - - - - - -
X - - - - - -
O - - - - - -
O - - - - - -
X X X O - - -
0 1 2 3 4 5 6


Votre coup (0-6): 3
O - - - - - -
O - - - - - -
X - - - - - -
O - - - - - -
O - - X - - -
X X X O - - -
0 1 2 3 4 5 6


Votre coup (0-6): 2
O - - - - - -
O - - - - - -
X - - - - - -
O - - - - - -
O O X X - - -
X X X O - - -
0 1 2 3 4 5 6


Votre coup (0-6): 2
O - - - - - -
O - - - - - -
X - O - - - -
O - X - - - -
O O X X - - -
X X X O - - -
0 1 2 3 4 5 6


Votre coup (0-6): 1
O - - - - - -
O - - - - - -
X O O - - - -
O X X - - - -
O O X X - - -
X X X O - - -
0 1 2 3 4 5 6


Votre coup (0-6): 3
O - - - - - -
O O - - - - -
X O O - - - -
O X X X - - -
O O X X - - -
X X X O - - -
0 1 2 3 4 5 6


Votre coup (0-6): 4
O O - - - - -
O O - - - - -
X O O - - - -
O X X X - - -
O O X X - - -
X X X O X - -
0 1 2 3 4 5 6


Votre coup (0-6): 5
O O - - - - -
O O O - - - -
X O O - - - -
O X X X - - -
O O X X - - -
X X X O X X -
0 1 2 3 4 5 6


Votre coup (0-6): 5
O O O - - - -
O O O - - - -
X O O - - - -
O X X X - - -
O O X X - X -
X X X O X X -
0 1 2 3 4 5 6


Votre coup (0-6): 4
O O O - - - -
O O O - - - -
X O O - - - -
O X X X - - -
O O X X X X -
X X X O X X -
0 1 2 3 4 5 6


Vous avez gagné!

Tour de Hanoï résolution récursive

def hanoi(n, source, target, auxiliary, towers):
    if n == 1:
        disk = towers[source].pop()
        towers[target].append(disk)
        draw_towers(towers)
        return

    hanoi(n-1, source, auxiliary, target, towers)

    disk = towers[source].pop()
    towers[target].append(disk)
    draw_towers(towers)

    hanoi(n-1, auxiliary, target, source, towers)

def draw_towers(towers):
    for name, tower in towers.items():
        print(f"{name}: {tower}")
    print("-----")

def init_towers(n):
    towers = {
        'A': list(range(n, 0, -1)),  # [n, n-1, ..., 2, 1]
        'B': [],
        'C': []
    }
    return towers

n = int(input("Enter the number of disks: "))  # Get the number of disks from the user
towers = init_towers(n)

draw_towers(towers)  # Draw initial state
hanoi(n, 'A', 'C', 'B', towers)
Enter the number of disks: 5
A: [5, 4, 3, 2, 1]
B: []
C: []
-----
A: [5, 4, 3, 2]
B: []
C: [1]
-----
A: [5, 4, 3]
B: [2]
C: [1]
-----
A: [5, 4, 3]
B: [2, 1]
C: []
-----
A: [5, 4]
B: [2, 1]
C: [3]
-----
A: [5, 4, 1]
B: [2]
C: [3]
-----
A: [5, 4, 1]
B: []
C: [3, 2]
-----
A: [5, 4]
B: []
C: [3, 2, 1]
-----
A: [5]
B: [4]
C: [3, 2, 1]
-----
A: [5]
B: [4, 1]
C: [3, 2]
-----
A: [5, 2]
B: [4, 1]
C: [3]
-----
A: [5, 2, 1]
B: [4]
C: [3]
-----
A: [5, 2, 1]
B: [4, 3]
C: []
-----
A: [5, 2]
B: [4, 3]
C: [1]
-----
A: [5]
B: [4, 3, 2]
C: [1]
-----
A: [5]
B: [4, 3, 2, 1]
C: []
-----
A: []
B: [4, 3, 2, 1]
C: [5]
-----
A: [1]
B: [4, 3, 2]
C: [5]
-----
A: [1]
B: [4, 3]
C: [5, 2]
-----
A: []
B: [4, 3]
C: [5, 2, 1]
-----
A: [3]
B: [4]
C: [5, 2, 1]
-----
A: [3]
B: [4, 1]
C: [5, 2]
-----
A: [3, 2]
B: [4, 1]
C: [5]
-----
A: [3, 2, 1]
B: [4]
C: [5]
-----
A: [3, 2, 1]
B: []
C: [5, 4]
-----
A: [3, 2]
B: []
C: [5, 4, 1]
-----
A: [3]
B: [2]
C: [5, 4, 1]
-----
A: [3]
B: [2, 1]
C: [5, 4]
-----
A: []
B: [2, 1]
C: [5, 4, 3]
-----
A: [1]
B: [2]
C: [5, 4, 3]
-----
A: [1]
B: []
C: [5, 4, 3, 2]
-----
A: []
B: []
C: [5, 4, 3, 2, 1]
-----

Retour au sommaire

03 - Jeu 2 ✔

Rudi's Cube 2x2 - Rudi's Cube 3x3

Site de résolution de nombreus de Rubi's cubes : https://rubiks-cube-solver.com/2x2/

Rudi's Cube 2x2 - récurcivité et force brute

# 	Temps en s	Nd de coups essayés
# 1	0	                 13
# 2	0,01562	            170
# 3	0,18502	           2055
# 4	2,20833     	  24676
# 5	24,4204	         296129
# 6	289,04738	    3553566
# 7	3649,51091	   42642811
# 8	41209,38342	  511713752
# 9	494512,601 = 137 h (prévision)
#10 5934151,212
#11 71209814,55 = 2,25 ans (prévision) n=11 est la borne de Dieu !!!

# site de résolution : https://rubiks-cube-solver.com/2x2/

import numpy as np
import time

# U pour "Up", D pour "Down", L pour "Left", R pour "Right", F pour "Front", B pour "Back"
# 'w' pour blanc, 'g' pour vert, 'r' pour rouge, 'b' pour bleu, 'o' pour orange, 'y' pour jaune


# F: Rotation de la face avant dans le sens horaire.
# B: Rotation de la face arrière dans le sens horaire.
# R: Rotation de la face droite dans le sens horaire.
# L: Rotation de la face gauche dans le sens horaire.
# U: Rotation de la face supérieure dans le sens horaire.
# D: Rotation de la face inférieure dans le sens horaire.


def affichage(cube):
    # La face U (Up)
    for row in cube[0]:
        print("        ", end="")
        print(" ".join(row))
    print()

    # Les faces L (Left), F (Front), R (Right), B (Back)
    for i in range(2):
        for face in [2, 4, 3, 5]:
            print(" ".join(cube[face][i]), end="   ")
        print()
    print()

    # La face D (Down)
    for row in cube[1]:
        print("        ", end="")
        print(" ".join(row))
    print()

def F(cube):
    cube[4] = np.rot90(cube[4], -1)
    temp_U = np.copy(cube[0][1, :])
    temp_R = np.copy(cube[3][:, 0])
    temp_D = np.copy(cube[1][0, :])
    temp_L = np.copy(cube[2][:, 1])
    cube[0][1, :] = temp_L[::-1]
    cube[3][:, 0] = temp_U
    cube[1][0, :] = temp_R[::-1]
    cube[2][:, 1] = temp_D
    return cube

def F_inv(cube):
    cube[4] = np.rot90(cube[4], 1)
    temp_U = np.copy(cube[0][1, :])
    temp_R = np.copy(cube[3][:, 0])
    temp_D = np.copy(cube[1][0, :])
    temp_L = np.copy(cube[2][:, 1])
    cube[0][1, :] = temp_R
    cube[3][:, 0] = temp_D[::-1]
    cube[1][0, :] = temp_L
    cube[2][:, 1] = temp_U[::-1]
    return cube

def L(cube):
    cube[2] = np.rot90(cube[2], -1)
    temp_U = np.copy(cube[0][:, 0])
    temp_F = np.copy(cube[4][:, 0])
    temp_D = np.copy(cube[1][:, 0])
    temp_B = np.copy(cube[5][:, 1])
    cube[0][:, 0] = temp_B[::-1]
    cube[4][:, 0] = temp_U
    cube[1][:, 0] = temp_F
    cube[5][:, 1] = temp_D[::-1]
    return cube

def L_inv(cube):
    cube[2] = np.rot90(cube[2], 1)
    temp_U = np.copy(cube[0][:, 0])
    temp_F = np.copy(cube[4][:, 0])
    temp_D = np.copy(cube[1][:, 0])
    temp_B = np.copy(cube[5][:, 1])
    cube[0][:, 0] = temp_F
    cube[4][:, 0] = temp_D
    cube[1][:, 0] = temp_B[::-1]
    cube[5][:, 1] = temp_U[::-1]
    return cube

def U(cube):
    cube[0] = np.rot90(cube[0], -1)
    temp_F = np.copy(cube[4][0, :])
    temp_R = np.copy(cube[3][0, :])
    temp_B = np.copy(cube[5][0, :])
    temp_L = np.copy(cube[2][0, :])
    cube[4][0, :] = temp_L
    cube[3][0, :] = temp_F
    cube[5][0, :] = temp_R
    cube[2][0, :] = temp_B
    return cube

def U_inv(cube):
    cube[0] = np.rot90(cube[0], 1)
    temp_F = np.copy(cube[4][0, :])
    temp_R = np.copy(cube[3][0, :])
    temp_B = np.copy(cube[5][0, :])
    temp_L = np.copy(cube[2][0, :])
    cube[4][0, :] = temp_R
    cube[3][0, :] = temp_B
    cube[5][0, :] = temp_L
    cube[2][0, :] = temp_F
    return cube

def B(cube):
    cube[5] = np.rot90(cube[5], -1)
    temp_U = np.copy(cube[0][0, :])
    temp_R = np.copy(cube[3][:, 1])
    temp_D = np.copy(cube[1][1, :])
    temp_L = np.copy(cube[2][:, 0])
    cube[0][0, :] = temp_R
    cube[3][:, 1] = temp_D[::-1]
    cube[1][1, :] = temp_L
    cube[2][:, 0] = temp_U[::-1]
    return cube

def B_inv(cube):
    cube[5] = np.rot90(cube[5], 1)
    temp_U = np.copy(cube[0][0, :])
    temp_R = np.copy(cube[3][:, 1])
    temp_D = np.copy(cube[1][1, :])
    temp_L = np.copy(cube[2][:, 0])
    cube[0][0, :] = temp_L[::-1]
    cube[3][:, 1] = temp_U
    cube[1][1, :] = temp_R[::-1]
    cube[2][:, 0] = temp_D
    return cube

def D(cube):
    cube[1] = np.rot90(cube[1], -1)
    temp_F = np.copy(cube[4][1, :])
    temp_R = np.copy(cube[3][1, :])
    temp_B = np.copy(cube[5][1, :])
    temp_L = np.copy(cube[2][1, :])
    cube[4][1, :] = temp_L
    cube[3][1, :] = temp_F
    cube[5][1, :] = temp_R
    cube[2][1, :] = temp_B
    return cube

def D_inv(cube):
    cube[1] = np.rot90(cube[1], 1)
    temp_F = np.copy(cube[4][1, :])
    temp_R = np.copy(cube[3][1, :])
    temp_B = np.copy(cube[5][1, :])
    temp_L = np.copy(cube[2][1, :])
    cube[4][1, :] = temp_R
    cube[3][1, :] = temp_B
    cube[5][1, :] = temp_L
    cube[2][1, :] = temp_F
    return cube

def R(cube):
    cube[3] = np.rot90(cube[3], -1)
    temp_U = np.copy(cube[0][:, 1])
    temp_F = np.copy(cube[4][:, 1])
    temp_D = np.copy(cube[1][:, 1])
    temp_B = np.copy(cube[5][:, 0])
    cube[0][:, 1] = temp_B[::-1]
    cube[4][:, 1] = temp_U
    cube[1][:, 1] = temp_F
    cube[5][:, 0] = temp_D[::-1]
    return cube

def R_inv(cube):
    cube[3] = np.rot90(cube[3], 1)
    temp_U = np.copy(cube[0][:, 1])
    temp_F = np.copy(cube[4][:, 1])
    temp_D = np.copy(cube[1][:, 1])
    temp_B = np.copy(cube[5][:, 0])
    cube[0][:, 1] = temp_F
    cube[4][:, 1] = temp_D
    cube[1][:, 1] = temp_B[::-1]
    cube[5][:, 0] = temp_U[::-1]
    return cube



def test_rotation(func, initial_state):
    state = initial_state.copy()
    for _ in range(4):
        state = func(state)
    return np.array_equal(state, initial_state)

# Rubik's Cube 2x2 en une seule liste
initial_state = np.array([
    [['w', 'w'], ['w', 'w']],  # U
    [['y', 'y'], ['y', 'y']],  # D
    [['g', 'g'], ['g', 'g']],  # L
    [['b', 'b'], ['b', 'b']],  # R
    [['r', 'r'], ['r', 'r']],  # F
    [['o', 'o'], ['o', 'o']]   # B
])

affichage(initial_state)
#affichage(F(initial_state))


def test_rotation(func, initial_state):
    state = initial_state.copy()
    for _ in range(4):
        state = func(state)
    return np.array_equal(state, initial_state)

print("4*F     = id ?", test_rotation(F, initial_state))
print("4*F_inv = id ?", test_rotation(F_inv, initial_state))
print("4*L     = id ?", test_rotation(L, initial_state))
print("4*L_inv = id ?", test_rotation(L_inv, initial_state))
print("4*U     = id ?", test_rotation(U, initial_state))
print("4*U_inv = id ?", test_rotation(U_inv, initial_state))
print("4*B     = id ?", test_rotation(B, initial_state))
print("4*B_inv = id ?", test_rotation(B_inv, initial_state))


print("4*R     = id ?", test_rotation(R, initial_state))
print("4*R_inv = id ?", test_rotation(R_inv, initial_state))

print("4*D     = id ?", test_rotation(D, initial_state))
print("4*D_inv = id ?", test_rotation(D_inv, initial_state))
print()



def test_rotation3(func,func2, initial_state):
    state = initial_state.copy()
    for _ in range(3):
        state = func(state)
    return np.array_equal(state, func2(initial_state))

print("3*F = F_inv ?", test_rotation3(F, F_inv,initial_state))
print("3*L = L_inv ?", test_rotation3(L, L_inv,initial_state))
print("3*B = B_inv ?", test_rotation3(B, B_inv,initial_state))
print("3*R = R_inv ?", test_rotation3(R, R_inv,initial_state))
print("3*U = U_inv ?", test_rotation3(R, R_inv,initial_state))
print("3*D = D_inv ?", test_rotation3(R, R_inv,initial_state))
print()

def test_inverse(func, func_inv, initial_state):
    state = func(initial_state.copy())
    state = func_inv(state)
    return np.array_equal(state, initial_state)

print("F F_inv = id ?", test_inverse(F, F_inv, initial_state))
print("B B_inv = id ?", test_inverse(B, B_inv, initial_state))
print("R R_inv = id ?", test_inverse(R, R_inv, initial_state))
print("L L_inv = id ?", test_inverse(L, L_inv, initial_state))
print("U U_inv = id ?", test_inverse(U, U_inv, initial_state))
print("D D_inv = id ?", test_inverse(D, D_inv, initial_state))
print()



def is_solved(cube):
    # Vérifie si chaque face du cube est d'une seule couleur
    for face in cube:
        if not np.all(face == face[0,0]):
            return False
    return True

def solve(r, moves, max_depth, counter):
    counter[0] += 1  # Augmenter le compteur à chaque appel
    if is_solved(r):
        return moves
    if max_depth == 0:
        return None

    for move, func in [('F', F), ('F_inv', F_inv), ('B', B), ('B_inv', B_inv), ('R', R), ('R_inv', R_inv), ('L', L), ('L_inv', L_inv), ('U', U), ('U_inv', U_inv), ('D', D), ('D_inv', D_inv)]:
        new_r = func(r.copy())
        new_moves = moves + [move]
        solution = solve(new_r, new_moves, max_depth-1, counter)
        if solution:
            return solution
    return None


def apply_moves_to_state(moves, initial_state):
    current_state = initial_state.copy()
    for move in moves:
        if move == 'F':
            current_state = F(current_state)
        elif move == 'F_inv':
            current_state = F_inv(current_state)
        elif move == 'B':
            current_state = B(current_state)
        elif move == 'B_inv':
            current_state = B_inv(current_state)
        elif move == 'R':
            current_state = R(current_state)
        elif move == 'R_inv':
            current_state = R_inv(current_state)
        elif move == 'L':
            current_state = L(current_state)
        elif move == 'L_inv':
            current_state = L_inv(current_state)
        elif move == 'U':
            current_state = U(current_state)
        elif move == 'U_inv':
            current_state = U_inv(current_state)
        elif move == 'D':
            current_state = D(current_state)
        elif move == 'D_inv':
            current_state = D_inv(current_state)
    return current_state

# Exemple d'utilisation :
start_time = time.time()
initial_state = np.array([
    [['w', 'w'], ['w', 'w']],  # U
    [['y', 'y'], ['y', 'y']],  # D
    [['g', 'g'], ['g', 'g']],  # L
    [['b', 'b'], ['b', 'b']],  # R
    [['r', 'r'], ['r', 'r']],  # F
    [['o', 'o'], ['o', 'o']]   # B
])

r = L(R(U(F(initial_state))))


affichage(r)

counter = [0]  # Compteur pour stocker le nombre de coups essayés

for k in range(1,11):
    solution = solve(r, [], k, counter)

    if solution:
        print("Solution brute trouvée:")
        print(solution)
        # Appliquer les mouvements de la solution pour vérifier
        final_state = apply_moves_to_state(solution, r)
        affichage(final_state)
        break
    else:
        end_time = time.time()
        elapsed_time = end_time - start_time
        print(f"Temps écoulé : {elapsed_time:.5f} secondes")
        print("Aucune solution trouvée dans la profondeur ",k," donnée.")
    print("Nombre de coups essayés jusqu'à présent:", counter[0])
    print()
end_time = time.time()
elapsed_time = end_time - start_time
print(f"Temps écoulé : {elapsed_time:.5f} secondes")
print("Nombre total de coups essayés:", counter[0])

Rudi's Cube 3x3 - récurcivité et force brute

import numpy as np
import time

# U pour "Up", D pour "Down", L pour "Left", R pour "Right", F pour "Front", B pour "Back"
# 'w' pour blanc, 'g' pour vert, 'r' pour rouge, 'b' pour bleu, 'o' pour orange, 'y' pour jaune


# F: Rotation de la face avant dans le sens horaire.
# B: Rotation de la face arrière dans le sens horaire.
# R: Rotation de la face droite dans le sens horaire.
# L: Rotation de la face gauche dans le sens horaire.
# U: Rotation de la face supérieure dans le sens horaire.
# D: Rotation de la face inférieure dans le sens horaire.


def affichage(cube):
    # La face U (Up)
    for row in cube[0]:
        print("        ", end="")
        print(" ".join(row))
    print()

    # Les faces L (Left), F (Front), R (Right), B (Back)
    for i in range(3):
        for face in [2, 4, 3, 5]:
            print(" ".join(cube[face][i]), end="   ")
        print()

    print()

    # La face D (Down)
    for row in cube[1]:
        print("        ", end="")
        print(" ".join(row))
    print()


# def rotate_face_90(face):
#     """Fait pivoter une face de 90 degrés dans le sens horaire"""
#     return np.rot90(face, -1)

def F(cube):
    # Faire pivoter la face F elle-même
    cube[4] = np.rot90(cube[4], -1)

    # Sauvegarder les bords adjacents
    temp_U = np.copy(cube[0][2])
    temp_R = np.copy(cube[3][:, 0])
    temp_D = np.copy(cube[1][0])
    temp_L = np.copy(cube[2][:, 2])

    # Mettre à jour les bords adjacents
    cube[0][2] = temp_L[::-1]
    cube[3][:, 0] = temp_U
    cube[1][0] = temp_R[::-1]
    cube[2][:, 2] = temp_D
    return cube


def F_inv(cube):
    # Faire pivoter la face F elle-même
    cube[4] = np.rot90(cube[4], 1)  # Rotation inverse

    # Sauvegarder les bords adjacents
    temp_U = np.copy(cube[0][2])
    temp_R = np.copy(cube[3][:, 0])
    temp_D = np.copy(cube[1][0])
    temp_L = np.copy(cube[2][:, 2])

    # Mettre à jour les bords adjacents
    cube[0][2] = temp_R
    cube[3][:, 0] = temp_D[::-1]
    cube[1][0] = temp_L
    cube[2][:, 2] = temp_U[::-1]
    return cube


def L(cube):
    # Faire pivoter la face L elle-même
    cube[2] = np.rot90(cube[2], -1)

    # Sauvegarder les bords adjacents
    temp_U = np.copy(cube[0][:, 0])
    temp_F = np.copy(cube[4][:, 0])
    temp_D = np.copy(cube[1][:, 0])
    temp_B = np.copy(cube[5][:, 2])

    # Mettre à jour les bords adjacents
    cube[0][:, 0] = temp_F
    cube[4][:, 0] = temp_D
    cube[1][:, 0] = temp_B[::-1]
    cube[5][:, 2] = temp_U[::-1]
    return cube


def L_inv(cube):
    # Faire pivoter la face L elle-même
    cube[2] = np.rot90(cube[2], 1)  # Rotation dans le sens des aiguilles d'une montre

    # Sauvegarder les bords adjacents
    temp_U = np.copy(cube[0][:, 0])
    temp_F = np.copy(cube[4][:, 0])
    temp_D = np.copy(cube[1][:, 0])
    temp_B = np.copy(cube[5][:, 2])

    # Mettre à jour les bords adjacents
    cube[0][:, 0] = temp_B[::-1]
    cube[4][:, 0] = temp_U
    cube[1][:, 0] = temp_F
    cube[5][:, 2] = temp_D[::-1]
    return cube



def U(cube):
    # Faire pivoter la face U elle-même
    cube[0] = np.rot90(cube[0], -1)

    # Sauvegarder les bords adjacents
    temp_F = np.copy(cube[4][0, :])
    temp_R = np.copy(cube[3][0, :])
    temp_B = np.copy(cube[5][0, :])
    temp_L = np.copy(cube[2][0, :])

    # Mettre à jour les bords adjacents
    cube[4][0, :] = temp_L
    cube[3][0, :] = temp_F
    cube[5][0, :] = temp_R
    cube[2][0, :] = temp_B
    return cube


def U_inv(cube):
    # Faire pivoter la face U elle-même
    cube[0] = np.rot90(cube[0], 1)

    # Sauvegarder les bords adjacents
    temp_F = np.copy(cube[4][0, :])
    temp_R = np.copy(cube[3][0, :])
    temp_B = np.copy(cube[5][0, :])
    temp_L = np.copy(cube[2][0, :])

    # Mettre à jour les bords adjacents
    cube[4][0, :] = temp_R
    cube[3][0, :] = temp_B
    cube[5][0, :] = temp_L
    cube[2][0, :] = temp_F
    return cube


def B(cube):
    # Faire pivoter la face B elle-même
    cube[5] = np.rot90(cube[5], -1)

    # Sauvegarder les bords adjacents
    temp_U = np.copy(cube[0][0, :])
    temp_R = np.copy(cube[3][:, 2])
    temp_D = np.copy(cube[1][2, :])
    temp_L = np.copy(cube[2][:, 0])

    # Mettre à jour les bords adjacents
    cube[0][0, :] = temp_R[::-1]
    cube[3][:, 2] = temp_D
    cube[1][2, :] = temp_L[::-1]
    cube[2][:, 0] = temp_U
    return cube

def B_inv(cube):
    # Faire pivoter la face B elle-même
    cube[5] = np.rot90(cube[5], 1)  # Rotation dans le sens des aiguilles d'une montre

    # Sauvegarder les bords adjacents
    temp_U = np.copy(cube[0][0, :])
    temp_R = np.copy(cube[3][:, 2])
    temp_D = np.copy(cube[1][2, :])
    temp_L = np.copy(cube[2][:, 0])

    # Mettre à jour les bords adjacents
    cube[0][0, :] = temp_L
    cube[3][:, 2] = temp_U[::-1]
    cube[1][2, :] = temp_R
    cube[2][:, 0] = temp_D[::-1]
    return cube


def D(cube):
    # Faire pivoter la face D elle-même
    cube[1] = np.rot90(cube[1], -1)

    # Sauvegarder les bords adjacents
    temp_F = np.copy(cube[4][2, :])
    temp_R = np.copy(cube[3][2, :])
    temp_B = np.copy(cube[5][2, :])
    temp_L = np.copy(cube[2][2, :])

    # Mettre à jour les bords adjacents
    cube[4][2, :] = temp_L
    cube[3][2, :] = temp_F
    cube[5][2, :] = temp_R
    cube[2][2, :] = temp_B
    return cube



def D_inv(cube):
    # Faire pivoter la face D elle-même
    cube[1] = np.rot90(cube[1], 1)

    # Sauvegarder les bords adjacents
    temp_F = np.copy(cube[4][2, :])
    temp_R = np.copy(cube[3][2, :])
    temp_B = np.copy(cube[5][2, :])
    temp_L = np.copy(cube[2][2, :])

    # Mettre à jour les bords adjacents
    cube[4][2, :] = temp_R
    cube[3][2, :] = temp_B
    cube[5][2, :] = temp_L
    cube[2][2, :] = temp_F
    return cube



def R(cube):
    # Faire pivoter la face R elle-même
    cube[3] = np.rot90(cube[3], -1)

    # Sauvegarder les bords adjacents
    temp_U = np.copy(cube[0][:, 2])
    temp_F = np.copy(cube[4][:, 2])
    temp_D = np.copy(cube[1][:, 2])
    temp_B = np.copy(cube[5][:, 0])

    # Mettre à jour les bords adjacents
    cube[0][:, 2] = temp_F
    cube[4][:, 2] = temp_D
    cube[1][:, 2] = temp_B[::-1]
    cube[5][:, 0] = temp_U[::-1]
    return cube



def R_inv(cube):
    # Faire pivoter la face R elle-même
    cube[3] = np.rot90(cube[3], 1)

    # Sauvegarder les bords adjacents
    temp_U = np.copy(cube[0][:, 2])
    temp_F = np.copy(cube[4][:, 2])
    temp_D = np.copy(cube[1][:, 2])
    temp_B = np.copy(cube[5][:, 0])

    # Mettre à jour les bords adjacents
    cube[0][:, 2] = temp_B[::-1]
    cube[4][:, 2] = temp_U
    cube[1][:, 2] = temp_F
    cube[5][:, 0] = temp_D[::-1]
    return cube




def test_rotation(func, initial_state):
    state = initial_state.copy()
    for _ in range(4):
        state = func(state)
    return np.array_equal(state, initial_state)

# Rubik's Cube 2x2 en une seule liste
initial_state = np.array([
    [['w', 'w', 'w'], ['w', 'w', 'w'], ['w', 'w', 'w']],  # U
    [['y', 'y', 'y'], ['y', 'y', 'y'], ['y', 'y', 'y']],  # D
    [['g', 'g', 'g'], ['g', 'g', 'g'], ['g', 'g', 'g']],  # L
    [['b', 'b', 'b'], ['b', 'b', 'b'], ['b', 'b', 'b']],  # R
    [['r', 'r', 'r'], ['r', 'r', 'r'], ['r', 'r', 'r']],  # F
    [['o', 'o', 'o'], ['o', 'o', 'o'], ['o', 'o', 'o']]   # B
])

affichage(initial_state)
#affichage(F(initial_state))


def test_rotation(func, initial_state):
    state = initial_state.copy()
    for _ in range(4):
        state = func(state)
    return np.array_equal(state, initial_state)

print("4*F     = id ?", test_rotation(F, initial_state))
print("4*F_inv = id ?", test_rotation(F_inv, initial_state))
print("4*L     = id ?", test_rotation(L, initial_state))
print("4*L_inv = id ?", test_rotation(L_inv, initial_state))
print("4*B     = id ?", test_rotation(B, initial_state))
print("4*B_inv = id ?", test_rotation(B_inv, initial_state))
print("4*R     = id ?", test_rotation(R, initial_state))
print("4*R_inv = id ?", test_rotation(R_inv, initial_state))
print("4*U     = id ?", test_rotation(U, initial_state))
print("4*U_inv = id ?", test_rotation(U_inv, initial_state))
print("4*D     = id ?", test_rotation(D, initial_state))
print("4*D_inv = id ?", test_rotation(D_inv, initial_state))
print()



def test_rotation3(func,func2, initial_state):
    state = initial_state.copy()
    for _ in range(3):
        state = func(state)
    return np.array_equal(state, func2(initial_state))

print("3*F = F_inv ?", test_rotation3(F, F_inv,initial_state))
print("3*L = L_inv ?", test_rotation3(L, L_inv,initial_state))
print("3*B = B_inv ?", test_rotation3(B, B_inv,initial_state))
print("3*R = R_inv ?", test_rotation3(R, R_inv,initial_state))
print("3*U = U_inv ?", test_rotation3(R, R_inv,initial_state))
print("3*D = D_inv ?", test_rotation3(R, R_inv,initial_state))
print()

def test_inverse(func, func_inv, initial_state):
    state = func(initial_state.copy())
    state = func_inv(state)
    return np.array_equal(state, initial_state)

print("F F_inv = id ?", test_inverse(F, F_inv, initial_state))
print("B B_inv = id ?", test_inverse(B, B_inv, initial_state))
print("R R_inv = id ?", test_inverse(R, R_inv, initial_state))
print("L L_inv = id ?", test_inverse(L, L_inv, initial_state))
print("U U_inv = id ?", test_inverse(U, U_inv, initial_state))
print("D D_inv = id ?", test_inverse(D, D_inv, initial_state))
print()



def is_solved(cube):
    # Vérifie si chaque face du cube est d'une seule couleur
    for face in cube:
        if not np.all(face == face[0,0]):
            return False
    return True

def solve(r, moves, max_depth, counter):
    counter[0] += 1  # Augmenter le compteur à chaque appel
    if is_solved(r):
        return moves
    if max_depth == 0:
        return None

    for move, func in [('F', F), ('F_inv', F_inv), ('B', B), ('B_inv', B_inv), ('R', R), ('R_inv', R_inv), ('L', L), ('L_inv', L_inv), ('U', U), ('U_inv', U_inv), ('D', D), ('D_inv', D_inv)]:
        new_r = func(r.copy())
        new_moves = moves + [move]
        solution = solve(new_r, new_moves, max_depth-1, counter)
        if solution:
            return solution
    return None


def apply_moves_to_state(moves, initial_state):
    current_state = initial_state.copy()
    for move in moves:
        if move == 'F':
            current_state = F(current_state)
        elif move == 'F_inv':
            current_state = F_inv(current_state)
        elif move == 'B':
            current_state = B(current_state)
        elif move == 'B_inv':
            current_state = B_inv(current_state)
        elif move == 'R':
            current_state = R(current_state)
        elif move == 'R_inv':
            current_state = R_inv(current_state)
        elif move == 'L':
            current_state = L(current_state)
        elif move == 'L_inv':
            current_state = L_inv(current_state)
        elif move == 'U':
            current_state = U(current_state)
        elif move == 'U_inv':
            current_state = U_inv(current_state)
        elif move == 'D':
            current_state = D(current_state)
        elif move == 'D_inv':
            current_state = D_inv(current_state)
    return current_state

# Exemple d'utilisation :
start_time = time.time()
initial_state = np.array([
    [['w', 'w', 'w'], ['w', 'w', 'w'], ['w', 'w', 'w']],  # U
    [['y', 'y', 'y'], ['y', 'y', 'y'], ['y', 'y', 'y']],  # D
    [['g', 'g', 'g'], ['g', 'g', 'g'], ['g', 'g', 'g']],  # L
    [['b', 'b', 'b'], ['b', 'b', 'b'], ['b', 'b', 'b']],  # R
    [['r', 'r', 'r'], ['r', 'r', 'r'], ['r', 'r', 'r']],  # F
    [['o', 'o', 'o'], ['o', 'o', 'o'], ['o', 'o', 'o']]   # B
])
r = R(D(L(R(U(F(initial_state))))))
affichage(r)

counter = [0]  # Compteur pour stocker le nombre de coups essayés

for k in range(1,21):
    solution = solve(r, [], k, counter)

    if solution:
        print("Solution brute trouvée:")
        print(solution)
        # Appliquer les mouvements de la solution pour vérifier
        final_state = apply_moves_to_state(solution, r)
        # Votre code pour afficher le cube ici
        break
    else:
        end_time = time.time()
        elapsed_time = end_time - start_time
        print(f"Temps écoulé : {elapsed_time:.5f} secondes")
        print("Aucune solution trouvée dans la profondeur ",k," donnée.")
    print("Nombre de coups essayés jusqu'à présent:", counter[0])
    print()
end_time = time.time()
elapsed_time = end_time - start_time
print(f"Temps écoulé : {elapsed_time:.5f} secondes")
print("Nombre total de coups essayés:", counter[0])

Retour au sommaire

01 - Capytale Récursivité ✔

La fonction de Ackermann (Correction)

Lien vers le sujet sur Capytale

La fonction de Ackermann est une fonction mathématique récursive qui est célèbre pour sa croissance extrêmement rapide.

Elle est définie de la manière suivante :

$$ A(m, n) = \begin{cases} n + 1 & \text{si } m = 0 \ A(m - 1, 1) & \text{si } m > 0 \text{ et } n = 0 \ A(m - 1, A(m, n - 1)) & \text{si } m > 0 \text{ et } n > 0 \end{cases} $$

a) Implémentez la fonction de Ackermann en Python.

def ackermann(m, n):
    if m == 0:
        return n + 1
    elif m > 0 and n == 0:
        return ackermann(m - 1, 1)
    elif m > 0 and n > 0:
        return ackermann(m - 1, ackermann(m, n - 1))
b) Tester votre implémentation avec les asserts ci-dessous :
assert ackermann(1, 1) == 3, "Test 1 échoué"
assert ackermann(2, 3) == 9, "Test 2 échoué"
assert ackermann(3, 2) == 29, "Test 3 échoué"

print("Tous les tests ont réussi.")
Tous les tests ont réussi.
c) Expliquez le fonctionnement de la récursion pour des valeurs de m = 2 et n = 3.
ackermann(2, 3) appelle ackermann(1, ackermann(2, 2))
ackermann(2, 2) appelle ackermann(1, ackermann(2, 1))
ackermann(2, 1) appelle ackermann(1, ackermann(2, 0))
ackermann(2, 0) appelle ackermann(1, 1) qui retourne 3
ackermann(1, 3) retourne 9
d) Tester ackermann(4, 1), que se passe-t-il et pourquoi ?
ackermann(4, 1)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 7, in ackermann
  File "<input>", line 7, in ackermann
  File "<input>", line 7, in ackermann
  [Previous line repeated 4987 more times]
  File "<input>", line 5, in ackermann
RecursionError: maximum recursion depth exceeded

L'erreur RecursionError: maximum recursion depth exceeded est due au fait que Python impose une limite maximale de récursion pour empêcher les programmes de consommer trop de mémoire ou de provoquer des boucles infinies. Dans votre cas, la fonction de Ackermann provoque des appels récursifs extrêmement profonds, particulièrement avec des valeurs comme ackermann(4, 1).

Python, par défaut, limite la profondeur des appels récursifs à environ 1000. Chaque appel de la fonction appelle d'autres sous-fonctions récursivement, et lorsque ce nombre dépasse la limite autorisée par Python, l'interpréteur génère cette erreur pour protéger le programme d'un dépassement de mémoire.

Retour au sommaire

02 - Capytale Les bases du SQL ✔

Devoir sur les bases du SQL (Correction)

Accéder au sujet sur Capytale


Base de données : Films de Science-fiction, auteurs et livres associés

Contexte : Vous devez travailler avec une base de données comportant trois tables : films, auteurs, et livres.

Vous allez écrire des requêtes SQL pour interagir avec ces données.

Exécuter pour commencer les 2 cellules suivantes pour définir les tables et le jeu de données:

CREATE TABLE auteurs (
    id_auteur INT PRIMARY KEY,
    nom VARCHAR(100),
    prenom VARCHAR(100),
    nationalite VARCHAR(50)
);

CREATE TABLE livres (
    id_livre INT PRIMARY KEY,
    titre VARCHAR(150),
    annee_publication INT,
    id_auteur INT,
    FOREIGN KEY (id_auteur) REFERENCES auteurs(id_auteur)
);

CREATE TABLE films (
    id_film INT PRIMARY KEY,
    titre VARCHAR(150),
    annee_sortie INT,
    id_livre INT,
    FOREIGN KEY (id_livre) REFERENCES livres(id_livre)
);
-- Insertion des auteurs
INSERT INTO auteurs (id_auteur, nom, prenom, nationalite) VALUES
(1, 'Asimov', 'Isaac', 'Américain'),
(2, 'Herbert', 'Frank', 'Américain'),
(3, 'Dick', 'Philip K.', 'Américain'),
(4, 'Verne', 'Jules', 'Français');

-- Insertion des livres
INSERT INTO livres (id_livre, titre, annee_publication, id_auteur) VALUES
(1, 'Fondation', 1951, 1),
(2, 'Dune', 1965, 2),
(3, 'Les androïdes rêvent-ils de moutons électriques ?', 1968, 3),
(4, '20 000 lieues sous les mers', 1870, 4);

-- Insertion des films
INSERT INTO films (id_film, titre, annee_sortie, id_livre) VALUES
(1, 'Fondation', 2021, 1),
(2, 'Dune', 2021, 2),
(3, 'Blade Runner', 1982, 3),
(4, '20 000 lieues sous les mers', 1954, 4);

Questions :


1) Afficher toutes les tables
SELECT *
FROM auteurs;
id_auteur nom prenom nationalite 1 Asimov Isaac Américain 2 Herbert Frank Américain 3 Dick Philip K. Américain 4 Verne Jules Français
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>id_auteur</th><th>nom</th><th>prenom</th><th>nationalite</th></tr></thead><tbody><tr><td>1</td><td>Asimov</td><td>Isaac</td><td>Américain</td></tr><tr><td>2</td><td>Herbert</td><td>Frank</td><td>Américain</td></tr><tr><td>3</td><td>Dick</td><td>Philip K.</td><td>Américain</td></tr><tr><td>4</td><td>Verne</td><td>Jules</td><td>Français</td></tr></tbody></table>
SELECT *
FROM livres;
id_livre titre annee_publication id_auteur 1 Fondation 1951 1 2 Dune 1965 2 3 Les androïdes rêvent-ils de moutons électriques ? 1968 3 4 20 000 lieues sous les mers 1870 4
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>id_livre</th><th>titre</th><th>annee_publication</th><th>id_auteur</th></tr></thead><tbody><tr><td>1</td><td>Fondation</td><td>1951</td><td>1</td></tr><tr><td>2</td><td>Dune</td><td>1965</td><td>2</td></tr><tr><td>3</td><td>Les androïdes rêvent-ils de moutons électriques ?</td><td>1968</td><td>3</td></tr><tr><td>4</td><td>20 000 lieues sous les mers</td><td>1870</td><td>4</td></tr></tbody></table>
SELECT *
FROM films;
id_film titre annee_sortie id_livre 1 Fondation 2021 1 2 Dune 2021 2 3 Blade Runner 1982 3 4 20 000 lieues sous les mers 1954 4
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>id_film</th><th>titre</th><th>annee_sortie</th><th>id_livre</th></tr></thead><tbody><tr><td>1</td><td>Fondation</td><td>2021</td><td>1</td></tr><tr><td>2</td><td>Dune</td><td>2021</td><td>2</td></tr><tr><td>3</td><td>Blade Runner</td><td>1982</td><td>3</td></tr><tr><td>4</td><td>20 000 lieues sous les mers</td><td>1954</td><td>4</td></tr></tbody></table>
2. Dans la table films, pourquoi avons-nous une clé étrangère id_livre ?

Dans la table films, la clé étrangère id_livre permet de lier chaque film à un livre spécifique dans la table livres. Elle garantit l'intégrité des données en s'assurant que chaque film fait référence à un livre existant, évitant les incohérences. De plus, elle facilite les jointures pour extraire des informations liées aux livres et aux films.

3. Affichez tous les films sortis après l'année 2000.
SELECT titre, annee_sortie
FROM films
WHERE annee_sortie > 2000;
titre annee_sortie Fondation 2021 Dune 2021
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>titre</th><th>annee_sortie</th></tr></thead><tbody><tr><td>Fondation</td><td>2021</td></tr><tr><td>Dune</td><td>2021</td></tr></tbody></table>
4.Affichez les livres dont l'année de publication est antérieure à 1900.
SELECT titre, annee_publication
FROM livres
WHERE annee_publication < 1900;
titre annee_publication 20 000 lieues sous les mers 1870
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>titre</th><th>annee_publication</th></tr></thead><tbody><tr><td>20 000 lieues sous les mers</td><td>1870</td></tr></tbody></table>
5. Affichez le nombre total de films dans la base de données.
SELECT COUNT(*) AS nombre_films
FROM films;
nombre_films 4
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>nombre_films</th></tr></thead><tbody><tr><td>4</td></tr></tbody></table>
6.Affichez tous les films avec le nom complet de l'auteur du livre sur lequel ils sont basés.
SELECT films.titre, auteurs.prenom, auteurs.nom
FROM films
JOIN livres ON films.id_livre = livres.id_livre
JOIN auteurs ON livres.id_auteur = auteurs.id_auteur;
titre prenom nom Fondation Isaac Asimov Dune Frank Herbert Blade Runner Philip K. Dick 20 000 lieues sous les mers Jules Verne
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>titre</th><th>prenom</th><th>nom</th></tr></thead><tbody><tr><td>Fondation</td><td>Isaac</td><td>Asimov</td></tr><tr><td>Dune</td><td>Frank</td><td>Herbert</td></tr><tr><td>Blade Runner</td><td>Philip K.</td><td>Dick</td></tr><tr><td>20 000 lieues sous les mers</td><td>Jules</td><td>Verne</td></tr></tbody></table>
7. Écrivez une requête SQL qui supprime le livre Fondation de la table livres.
Pourquoi cette requête cause une erreur ?
DELETE FROM livres WHERE id_livre = 1;
Error: FOREIGN KEY constraint failed

Si un film est lié à ce livre via la clé étrangère id_livre, SQL renvoit une erreur de contrainte d'intégrité référentielle, car il ne peut pas laisser un film référencer un livre supprimé.

8. Expliquez pourquoi il est préférable de séparer les auteurs, livres et films dans des tables différentes plutôt que de tout regrouper dans une seule table.

Il est préférable de séparer les auteurs, livres, et films dans des tables différentes pour respecter le principe de normalisation en base de données. Cela permet d'éviter la redondance et l'incohérence des données. Si tout était dans une seule table, chaque fois qu'un film ou un auteur change d'information, il faudrait modifier plusieurs lignes, ce qui pourrait entraîner des erreurs et des incohérences. La séparation permet une gestion plus efficace et maintient l'intégrité des données.

9. Comment insérer le film "Blade Runner 2049" (sorti en 2017) dans la table films, en vous assurant qu'il est lié au livre "Les androïdes rêvent-ils de moutons électriques ?" dans la table livres ? Écrivez la requête SQL correspondante.
Puis afficher la nouvelle table film
INSERT INTO films (id_film, titre, annee_sortie, id_livre) 
VALUES (5, 'Blade Runner 2049', 2017, 3);
SELECT *
FROM films;
id_film titre annee_sortie id_livre 1 Fondation 2021 1 2 Dune 2021 2 3 Blade Runner 1982 3 4 20 000 lieues sous les mers 1954 4 5 Blade Runner 2049 2017 3
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>id_film</th><th>titre</th><th>annee_sortie</th><th>id_livre</th></tr></thead><tbody><tr><td>1</td><td>Fondation</td><td>2021</td><td>1</td></tr><tr><td>2</td><td>Dune</td><td>2021</td><td>2</td></tr><tr><td>3</td><td>Blade Runner</td><td>1982</td><td>3</td></tr><tr><td>4</td><td>20 000 lieues sous les mers</td><td>1954</td><td>4</td></tr><tr><td>5</td><td>Blade Runner 2049</td><td>2017</td><td>3</td></tr></tbody></table>
10. Donner le schéma relationnel de chaqu'une des table

auteurs(id_auteur : INT, nom : TEXT, prenom : TEXT, nationalite : TEXT)

livres(id_livre : INT, titre : TEXT, annee_publication : INT, #id_auteur : INT)

films(id_film : INT, titre : TEXT, annee_sortie : INT, #id_livre : INT)

Retour au sommaire

03 - Capytale SQL Polynésie 2023 ✔

Bases de données relationnelles et SQL (Correction)

Accéder au sujet sur Capytale

Cet exercice porte sur les bases de données relationnelles et le langage SQL.

L'énoncé de cet exercice utilise les mots clefs du langage SQL suivants : SELECT, FROM, WHERE, JOIN ON, UPDATE, SET, INSERT INTO VALUES, COUNT, ORDER BY.

La ligue féminine de basket-ball publie les données relatives à chaque saison sur le site web de la ligue. On y retrouve des informations concernant les équipes participantes, les calendriers et les résultats des matchs, ainsi que les statistiques des joueuses. Dans cet exercice, nous allons nous intéresser à la base de données relationnelle LFP_2021_2022 permettant le stockage et la gestion des données de la saison régulière de basket-ball féminin 2021-2022.

Source : 2023 Sujet 5 - Polynésie Sujet 1 23-NSIJ1PO1 13 mars 2023 - Ex 1

Création de la table Equipe

CREATE TABLE Equipe (
    id_equipe INT PRIMARY KEY,
    nom VARCHAR(50),
    adresse VARCHAR(100),
    telephone VARCHAR(20)
);    

INSERT INTO Equipe VALUES 
(1, 'Saint-Amand', '39 avenue du Clos, 59230 Saint-Amand-les-Eaux', '03 04 05 06 07'),
(2, 'Basket Landes', '15 place Saint-Roch, 40000 Mont-De-Marsan', '05 06 07 08 09'),
(3, 'Villeneuve d’Ascq', '2 rue Breughel, 59650 Villeneuve-d’Ascq', '03 02 01 00 01'),
(4, 'Tarbe', 'Quai de l’Adour, 65000 Tarbes', '05 04 03 02 02'),
(5, 'Lyon', '451 cours d’Emile Zola, 69100 Villeurbanne', '04 05 06 07 08'),
(6, 'Bourges', '6 rue du Pré Doulet, 18000 Bourges', '02 03 04 05 06'),
(7, 'Charleville-Mézières', 'Rue de la Vieille Meuse, 08000 Charleville-Mézières', '03 05 07 09 01'),
(8, 'Landerneau', 'Kerouel, 29410 Pleyber-Christ', '02 04 06 08 00'),
(9, 'Angers', '330 rue Saint-Léonard, 49000 Angers', '02 00 08 06 04'),
(10, 'Lattes Montpellier', '157 rue de la Porte Lombarde, 34970 Lattes', '04 03 02 01 00'),
(11, 'Charnay', 'Allée des Ecoliers, 71850 Charnay-lès-Mâcon', '03 01 09 07 05'),
(12, 'Roche Vendée', 'BP 151, 85004 La Roche-Sur-Yon Cedex', '02 05 08 01 04');

SELECT * FROM Equipe;
id_equipe nom adresse telephone 1 Saint-Amand 39 avenue du Clos, 59230 Saint-Amand-les-Eaux 03 04 05 06 07 2 Basket Landes 15 place Saint-Roch, 40000 Mont-De-Marsan 05 06 07 08 09 3 Villeneuve d’Ascq 2 rue Breughel, 59650 Villeneuve-d’Ascq 03 02 01 00 01 4 Tarbe Quai de l’Adour, 65000 Tarbes 05 04 03 02 02 5 Lyon 451 cours d’Emile Zola, 69100 Villeurbanne 04 05 06 07 08 6 Bourges 6 rue du Pré Doulet, 18000 Bourges 02 03 04 05 06 7 Charleville-Mézières Rue de la Vieille Meuse, 08000 Charleville-Mézières 03 05 07 09 01 8 Landerneau Kerouel, 29410 Pleyber-Christ 02 04 06 08 00 9 Angers 330 rue Saint-Léonard, 49000 Angers 02 00 08 06 04 10 Lattes Montpellier 157 rue de la Porte Lombarde, 34970 Lattes 04 03 02 01 00 11 Charnay Allée des Ecoliers, 71850 Charnay-lès-Mâcon 03 01 09 07 05 12 Roche Vendée BP 151, 85004 La Roche-Sur-Yon Cedex 02 05 08 01 04
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>id_equipe</th><th>nom</th><th>adresse</th><th>telephone</th></tr></thead><tbody><tr><td>1</td><td>Saint-Amand</td><td>39 avenue du Clos, 59230 Saint-Amand-les-Eaux</td><td>03 04 05 06 07</td></tr><tr><td>2</td><td>Basket Landes</td><td>15 place Saint-Roch, 40000 Mont-De-Marsan</td><td>05 06 07 08 09</td></tr><tr><td>3</td><td>Villeneuve d’Ascq</td><td>2 rue Breughel, 59650 Villeneuve-d’Ascq</td><td>03 02 01 00 01</td></tr><tr><td>4</td><td>Tarbe</td><td>Quai de l’Adour, 65000 Tarbes</td><td>05 04 03 02 02</td></tr><tr><td>5</td><td>Lyon</td><td>451 cours d’Emile Zola, 69100 Villeurbanne</td><td>04 05 06 07 08</td></tr><tr><td>6</td><td>Bourges</td><td>6 rue du Pré Doulet, 18000 Bourges</td><td>02 03 04 05 06</td></tr><tr><td>7</td><td>Charleville-Mézières</td><td>Rue de la Vieille Meuse, 08000 Charleville-Mézières</td><td>03 05 07 09 01</td></tr><tr><td>8</td><td>Landerneau</td><td>Kerouel, 29410 Pleyber-Christ</td><td>02 04 06 08 00</td></tr><tr><td>9</td><td>Angers</td><td>330 rue Saint-Léonard, 49000 Angers</td><td>02 00 08 06 04</td></tr><tr><td>10</td><td>Lattes Montpellier</td><td>157 rue de la Porte Lombarde, 34970 Lattes</td><td>04 03 02 01 00</td></tr><tr><td>11</td><td>Charnay</td><td>Allée des Ecoliers, 71850 Charnay-lès-Mâcon</td><td>03 01 09 07 05</td></tr><tr><td>12</td><td>Roche Vendée</td><td>BP 151, 85004 La Roche-Sur-Yon Cedex</td><td>02 05 08 01 04</td></tr></tbody></table>

Questions

1. Pourquoi la requête suivante produit-elle une erreur ?
INSERT INTO Equipe VALUES (11, 'Toulouse', '2 rue du Nord, 40100 Dax', '05 04 03 02 01');

INSERT INTO Equipe VALUES (11, 'Toulouse', '2 rue du Nord, 40100 Dax', '05 04 03 02 01');
    
Error: UNIQUE constraint failed: Equipe.id_equipe

Réponse : id_equipe doit être unique (clé primaire). La requête essaye d’ajouter une équipe avec un id_equipe égal à 11 alors qu’il existe déjà un id_équipe égal à 11 dans la table;

2. Expliquez le choix du domaine de l’attribut `telephone`.

Réponse : Un numéro de téléphone est composé de chiffres et d’espaces (chaine de caractères)

3. Quel est le résultat de cette requête ?
SELECT nom, adresse, telephone 
FROM Equipe 
WHERE id_equipe = 5;

Réponse : Lyon 451 cours d’Emile Zola, 69100 Villeurbanne 04 05 06 07 08

SELECT nom, adresse, telephone 
FROM Equipe 
WHERE id_equipe = 5;    
nom adresse telephone Lyon 451 cours d’Emile Zola, 69100 Villeurbanne 04 05 06 07 08
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>nom</th><th>adresse</th><th>telephone</th></tr></thead><tbody><tr><td>Lyon</td><td>451 cours d’Emile Zola, 69100 Villeurbanne</td><td>04 05 06 07 08</td></tr></tbody></table>
4. Quel est le résultat de cette requête ?
SELECT COUNT(*) 
FROM Equipe;
SELECT COUNT(*) FROM Equipe;    
COUNT(*) 12
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>COUNT(*)</th></tr></thead><tbody><tr><td>12</td></tr></tbody></table>

Réponse : 12

5. Écrivez la requête SQL pour afficher les noms des équipes par ordre alphabétique.
SELECT nom 
FROM Equipe 
ORDER BY nom;    
nom Angers Basket Landes Bourges Charleville-Mézières Charnay Landerneau Lattes Montpellier Lyon Roche Vendée Saint-Amand Tarbe Villeneuve d’Ascq
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>nom</th></tr></thead><tbody><tr><td>Angers</td></tr><tr><td>Basket Landes</td></tr><tr><td>Bourges</td></tr><tr><td>Charleville-Mézières</td></tr><tr><td>Charnay</td></tr><tr><td>Landerneau</td></tr><tr><td>Lattes Montpellier</td></tr><tr><td>Lyon</td></tr><tr><td>Roche Vendée</td></tr><tr><td>Saint-Amand</td></tr><tr><td>Tarbe</td></tr><tr><td>Villeneuve d’Ascq</td></tr></tbody></table>
6. Corrigez le nom de l’équipe dont `id_equipe` est égal à 4 en `Tarbes`.
UPDATE Equipe SET nom = 'Tarbes' WHERE id_equipe = 4;    

Création de la table Joueuse

CREATE TABLE Joueuse (
    id_joueuse INT PRIMARY KEY,
    nom VARCHAR(50),
    prenom VARCHAR(50),
    date_naissance DATE,
    taille INT,
    poste INT,
    id_equipe INT,
    FOREIGN KEY (id_equipe) REFERENCES Equipe(id_equipe)
);

INSERT INTO Joueuse VALUES 
(1, 'Berkani', 'Lisa', '1997-05-19', 176, 2, 7),
(2, 'Alexander', 'Kayla', '1991-01-05', 193, 5, 5),
(3, 'Magarity', 'Regan', '1996-04-30', 192, 4, 2),
(4, 'Muzet', 'Johanna', '1997-07-08', 183, 3, 11),
(5, 'Kalu', 'Ezinne', '1992-06-26', 173, 2, 8),
(6, 'Sigmundova', 'Jodie Cornelie', '1993-04-20', 193, 5, 9),
(7, 'Dumerc', 'Céline', '1982-07-09', 162, 2, 2),
(8, 'Slonjsak', 'Iva', '1997-04-16', 183, 3, 9),
(9, 'Michel', 'Sarah', '1989-01-10', 180, 2, 6),
(10, 'Lithard', 'Pauline', '1994-02-11', 164, 1, 1);

SELECT * FROM Joueuse;
id_joueuse nom prenom date_naissance taille poste id_equipe 1 Berkani Lisa 1997-05-19 176 2 7 2 Alexander Kayla 1991-01-05 193 5 5 3 Magarity Regan 1996-04-30 192 4 2 4 Muzet Johanna 1997-07-08 183 3 11 5 Kalu Ezinne 1992-06-26 173 2 8 6 Sigmundova Jodie Cornelie 1993-04-20 193 5 9 7 Dumerc Céline 1982-07-09 162 2 2 8 Slonjsak Iva 1997-04-16 183 3 9 9 Michel Sarah 1989-01-10 180 2 6 10 Lithard Pauline 1994-02-11 164 1 1
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>id_joueuse</th><th>nom</th><th>prenom</th><th>date_naissance</th><th>taille</th><th>poste</th><th>id_equipe</th></tr></thead><tbody><tr><td>1</td><td>Berkani</td><td>Lisa</td><td>1997-05-19</td><td>176</td><td>2</td><td>7</td></tr><tr><td>2</td><td>Alexander</td><td>Kayla</td><td>1991-01-05</td><td>193</td><td>5</td><td>5</td></tr><tr><td>3</td><td>Magarity</td><td>Regan</td><td>1996-04-30</td><td>192</td><td>4</td><td>2</td></tr><tr><td>4</td><td>Muzet</td><td>Johanna</td><td>1997-07-08</td><td>183</td><td>3</td><td>11</td></tr><tr><td>5</td><td>Kalu</td><td>Ezinne</td><td>1992-06-26</td><td>173</td><td>2</td><td>8</td></tr><tr><td>6</td><td>Sigmundova</td><td>Jodie Cornelie</td><td>1993-04-20</td><td>193</td><td>5</td><td>9</td></tr><tr><td>7</td><td>Dumerc</td><td>Céline</td><td>1982-07-09</td><td>162</td><td>2</td><td>2</td></tr><tr><td>8</td><td>Slonjsak</td><td>Iva</td><td>1997-04-16</td><td>183</td><td>3</td><td>9</td></tr><tr><td>9</td><td>Michel</td><td>Sarah</td><td>1989-01-10</td><td>180</td><td>2</td><td>6</td></tr><tr><td>10</td><td>Lithard</td><td>Pauline</td><td>1994-02-11</td><td>164</td><td>1</td><td>1</td></tr></tbody></table>

Questions

1. Expliquez pourquoi l’attribut `id_equipe` a été déclaré clé étrangère.

Réponse : L’attribut id_equipe de la table Joueuse permet de lier la table Joueuse à la table Equipe, c’est donc une clé étrangère.

2. Pourquoi ne peut-on pas directement supprimer une équipe de la table `Equipe` ?

Réponse : La suppression d’une équipe de la table Equipe doit obligatoirement s’accompagner d’une modification de la table joueuse afin qu’aucun élément de l’attribut id_equipe de la table Joueuse ne pointe vers une équipe qui n’existe plus.

3. Écrivez la requête SQL pour afficher les noms et prénoms des joueuses de l’équipe d’Angers par ordre alphabétique des noms.
SELECT Joueuse.nom, Joueuse.prenom
FROM Joueuse 
JOIN Equipe ON Joueuse.id_equipe = Equipe.id_equipe 
WHERE Equipe.nom = 'Angers' 
ORDER BY Joueuse.nom;    
nom prenom Sigmundova Jodie Cornelie Slonjsak Iva
<table class="dataframe" border="1"><thead><tr style="text-align: right;"><th>nom</th><th>prenom</th></tr></thead><tbody><tr><td>Sigmundova</td><td>Jodie Cornelie</td></tr><tr><td>Slonjsak</td><td>Iva</td></tr></tbody></table>

Retour au sommaire

04 - Capytale ABR ✔

Arbres binaires de recherche (Correction)

Accéder au sujet sur Capytale

Capacités Attendue :

Exécutez la cellule suivante pour commencer.

class Noeud:
    def __init__(self, valeur, gauche=None, droit=None):
        self.valeur = valeur
        self.gauche = gauche
        self.droit = droit

racine = Noeud(40)
n20 = Noeud(20)
n60 = Noeud(60)
n10 = Noeud(10)
n30 = Noeud(30)
n50 = Noeud(50)
n70 = Noeud(76)
n5 = Noeud(5)
n15 = Noeud(15)
n25 = Noeud(25)
n35 = Noeud(35)
n45 = Noeud(45)
n75 = Noeud(75)
n80 = Noeud(80)

# Assemblage de l'arbre
racine.gauche = n20
racine.droit = n60
n20.gauche = n10
n20.droit = n30
n60.gauche = n50
n60.droit = n70
n10.gauche = n5
n10.droit = n15
n30.gauche = n25
n30.droit = n35
n50.gauche = n45
n70.gauche = n75
n70.droit = n80


def est_arbre_binaire(noeud, gauche=None, droite=None):
    if noeud is None:
        return True
    
    if gauche and noeud.valeur <= gauche.valeur:
        print(f"Problème : {noeud.valeur} <= {gauche.valeur} !!!")
        return False
    
    if droite and noeud.valeur >= droite.valeur:
        print(f"Problème : {noeud.valeur} => {droite.valeur}  !!!")
        return False
    
    return (est_arbre_binaire(noeud.gauche, gauche, noeud) and 
            est_arbre_binaire(noeud.droit, noeud, droite))
    
def verifier(racine):
    if not est_arbre_binaire(racine):
        print("L'arbre n'est pas un arbre binaire de recherche valide.")
    else:
        print("L'arbre est un arbre binaire de recherche valide.")
        
verifier(racine)

# Dessin de l'arbre
from graphviz import Digraph
from IPython.display import display, SVG

def dessiner_arbre(noeud, graph=None):
    if graph is None:
        graph = Digraph()
    
    if noeud is not None:
        graph.node(str(noeud.valeur))
        if noeud.gauche:
            graph.node(str(noeud.gauche.valeur))
            graph.edge(str(noeud.valeur), str(noeud.gauche.valeur))
            dessiner_arbre(noeud.gauche, graph)
        if noeud.droit:
            graph.node(str(noeud.droit.valeur))
            graph.edge(str(noeud.valeur), str(noeud.droit.valeur))
            dessiner_arbre(noeud.droit, graph)
    return graph

def arbre_visuel(racine) :
    graph = dessiner_arbre(racine)
    raw_data = graph.pipe(format='svg')
    raw_text = raw_data.decode('utf-8')
    display(SVG(data=raw_text))

arbre_visuel(racine)

1. La taille de l'arbre

La taille de l'arbre est le nombre total de nœuds.

Compléter la fonction suivante qui permet de calculer la taille d'un arbre.

# Fonction pour calculer la taille de l'arbre
def taille_arbre(noeud):
    if noeud is None:
        return 0
    return 1 + taille_arbre(noeud.gauche) + taille_arbre(noeud.droit)


taille = taille_arbre(racine)
print(f"La taille de l'arbre est de {taille} nœuds.")

2. La hauteur d'un arbre

La hauteur de l'arbre est le nombre d'arêtes de la racine au nœud le plus profond .

Compléter la fonction suivante qui permet de calculer la hauteur d'un arbre.

# Fonction pour calculer la hauteur de l'arbre
def hauteur_arbre(noeud):
    if noeud is None:
        return -1  # On commence à -1 pour que le nœud racine n'ajoute pas 1 à la hauteur totale
    return 1 + max(hauteur_arbre(noeud.gauche), hauteur_arbre(noeud.droit))


hauteur = hauteur_arbre(racine)
print(f"La hauteur de l'arbre est de {hauteur}.")

4. Rechercher une clé dans un arbre de recherche

La recherche dans un arbre de recherche binaire commence à la racine et descend dans l'arbre en comparant la clé à celle dans le nœud actuel. Puis on se dirige soit vers le sous-arbre gauche, soit vers le sous-arbre droit selon que la clé est plus petite ou plus grande que celle du nœud actuel.

Complétez la fonction ci-dessous, afin de rechercher une clé donnée dans une arbre.

def recherche(noeud, cle):
    if noeud is None:
        print(f"Clé {cle} non trouvée dans l'arbre.")
        return
    if cle == noeud.valeur:
        print(f"Clé {cle} trouvée dans l'arbre.")
        return 
    elif cle < noeud.valeur:
        return recherche(noeud.gauche, cle)
    else:
        return recherche(noeud.droit, cle)

recherche(racine, 76)

5. Insérer une clé.

Insérer le noeud 42 et 47 sous le noeud 45.

# Insérer un nœud dans l'arbre
n42 = Noeud(42)
n47 = Noeud(47)
n45.gauche = n42
n45.droit = n47

# Affichage des parcours
arbre_visuel(racine)
verifier(racine)

3. Parcours en largeur d'un arbre

Dans un parcours en largeur (Breadth-First Search ou BFS en anglais), vous visitez tous les nœuds d'un niveau donné avant de passer au niveau suivant.

             A
          /     \
        B         C
      /   \      /   \
     D     E    F     G
    / \   / \  / \   / \
   H   I J  K L   M N   O

Cela commence généralement à la racine. Exemple Parcours en largeur : A, B, C, D, E, F, G, H, I, J, K, L, M, N, O.

Compléter la fonction suivante pour qu'elle retourne en une chaine de caractère le parcours en largeur de l'arbre nommée racine_lettre.

# Parcours en largeur
def parcours_en_largeur(racine):
    file = [racine]  # Utilisation d'une liste comme file
    result = []
    while file:
        noeud = file.pop(0)  
        result.append(str(noeud.valeur))
        if noeud.gauche:
            file.append(noeud.gauche)
        if noeud.droit:
            file.append(noeud.droit)
    return "".join(result)


racine_lettre = Noeud('A', Noeud('B', Noeud('D', Noeud('H'), Noeud('I')), Noeud('E', Noeud('J'), Noeud('K'))), Noeud('C', Noeud('F', Noeud('L'), Noeud('M')), Noeud('G', Noeud('N'), Noeud('O'))))
print("Parcours en largeur : ", parcours_en_largeur(racine_lettre))
arbre_visuel(racine_lettre)

4. Parcours en profondeur d'un arbre

Dans un parcours en profondeur (Depth-First Search ou DFS en anglais), vous partez de la racine de l'arbre et explorez aussi loin que possible le long de chaque branche avant de revenir en arrière. Il existe trois types de parcours en profondeur :

             A
          /     \
        B         C
      /   \      /   \
     D     E    F     G
    / \   / \  / \   / \
   H   I J  K L   M N   O

Compléter les fonctions suivantes pour qu'elles retournent ces parcours en une chaine de caractère de l'arbre nommée racine_lettre.

# Parcours en profondeur
def Préfixe(noeud):
    if noeud is not None:
        return str(noeud.valeur) + Préfixe(noeud.gauche) + Préfixe(noeud.droit)
    else:
        return ""

def Infixe(noeud):
    if noeud is not None:
        return Infixe(noeud.gauche) + str(noeud.valeur) + Infixe(noeud.droit)
    else:
        return ""

def Suffixe(noeud):
    if noeud is not None:
        return Suffixe(noeud.gauche) + Suffixe(noeud.droit) + str(noeud.valeur) 
    else:
        return ""

racine_lettre = Noeud('A', Noeud('B', Noeud('D', Noeud('H'), Noeud('I')), Noeud('E', Noeud('J'), Noeud('K'))), Noeud('C', Noeud('F', Noeud('L'), Noeud('M')), Noeud('G', Noeud('N'), Noeud('O'))))
print("Parcours en Préfixe : ", Préfixe(racine_lettre))
print("Parcours en Infixe  : ", Infixe(racine_lettre))
print("Parcours en Suffixe : ", Suffixe(racine_lettre))
arbre_visuel(racine_lettre)

Retour au sommaire

01 - POO, Piles, Files, Listes, Dictionnaires

Fiche de Révision : POO, Piles, Files, Listes, Dictionnaires - Tle NSI

1. Interface, Implémentation, POO

Concepts clés :

class Voiture: # Classe structure qui permet de définir des objets qui encapsulent des données et des fonctions.
    def __init__(self, marque, modele, kilometrage):# Constructeur, et self fait référence à l'instance actuelle.
        self.marque = marque # Attribut
        self.modele = modele # Attribut
        self.kilometrage = kilometrage # Attribut interne de l'objet ou de l'instance
        
    def afficher_details(self): # Méthode
        print(f"Voiture {self.marque} {self.modele}, {self.kilometrage} km parcourus.")
        
    def conduire(self, km): # Méthode
        self.kilometrage += km

# Exemple
v1 = Voiture("Peugeot", "208", 10000) # Création d'une instance de la classe.
v1.afficher_details()
v1.conduire(150)
v1.afficher_details()

v2 = Voiture("Tesla", "Modèle 3", 60000) # Création d'une autre instance de la classe.
v2.afficher_details()
Voiture Peugeot 208, 10000 km parcourus.
Voiture Peugeot 208, 10150 km parcourus.
Voiture Tesla Modèle 3, 60000 km parcourus.

2. Piles, Files, Listes

Concepts clés :

# Exemple de pile en Python
pile = []
pile.append(10)
pile.append(20)
pile.append(30)
print(pile) # Affiche [10, 20, 30]
print(pile.pop())  # Retire 30
print(pile)        # Affiche [10, 20]
[10, 20, 30]
30
[10, 20]
# Exemple de file en Python
file = []
file.append(10)
file.append(20)
file.append(30)
print(file) # Affiche [10, 20,30]
print(file.pop(0))  # Retire 10
print(file)         # Affiche [20, 30]
[10, 20, 30]
10
[20, 30]

3. Dictionnaires

Concepts clés :

# Exemple de dictionnaire en Python
dictionnaire = {'a': 1, 'b': 2, 'c': 3}
print(dictionnaire['a'])  # Accès à la valeur associée à la clé 'a'
dictionnaire['d'] = 4    # Ajout d'une nouvelle clé-valeur
del dictionnaire['b']    # Suppression de la clé 'b'
print(dictionnaire)
1
{'a': 1, 'c': 3, 'd': 4}
# Exemple mettant en œuvre les trois types de boucle for sur un dictionnaire
dictionnaire = {'a': 1, 'b': 2, 'c': 3}

# Boucle sur les clés
print("Parcours des clés :")
for cle in dictionnaire:
    print(f"Clé : {cle}")

# Boucle sur les valeurs
print("\nParcours des valeurs :")
for valeur in dictionnaire.values():
    print(f"Valeur : {valeur}")

# Boucle sur les paires clé-valeur
print("\nParcours des paires clé-valeur :")
for cle, valeur in dictionnaire.items():
    print(f"Clé : {cle}, Valeur : {valeur}")

Retour au sommaire

02 - Arbre

Fiche de Révision : Les Arbres - Terminale NSI

Concepts de base sur les arbres

Un arbre est une structure de données hiérarchique constituée de nœuds reliés par des arêtes. Les arbres sont utilisés pour modéliser des relations de type parent-enfant.

Définitions :

Arbres binaires

Un arbre binaire est un arbre dans lequel chaque nœud peut avoir au plus deux enfants.

Un arbre binaire de recherche (ABR) est un arbre binaire avec les propriétés suivantes :

class Noeud:
    def __init__(self, valeur, gauche=None, droit=None):
        self.valeur = valeur
        self.gauche = gauche
        self.droit = droit

# Exemple d’arbre binaire
racine = Noeud(40, Noeud(20, Noeud(10), Noeud(30)), Noeud(60, Noeud(50), Noeud(70)))

# autre méthode pour implémenter l'arbre en initialisant chaque nœud étape par étape
n10 = Noeud(10)
n30 = Noeud(30)
n20 = Noeud(20, n10, n30)
n50 = Noeud(50)
n70 = Noeud(70)
n60 = Noeud(60, n50, n70)
racine = Noeud(40, n20, n60)

Définitions Métriques :

def taille_arbre(noeud):
    if noeud is None:
        return 0
    return 1 + taille_arbre(noeud.gauche) + taille_arbre(noeud.droit)

def hauteur_arbre(noeud):
    if noeud is None:
        return -1
    return 1 + max(hauteur_arbre(noeud.gauche), hauteur_arbre(noeud.droit))

print("Taille de l’arbre :", taille_arbre(racine))
print("Hauteur de l’arbre :", hauteur_arbre(racine))
Taille de l’arbre : 7
Hauteur de l’arbre : 2

Parcours des arbres

Types de parcours :

  1. Préfixe (préordre) : Racine → Gauche → Droite
  2. Infixe (inordre) : Gauche → Racine → Droite
  3. Suffixe (postordre) : Gauche → Droite → Racine
  4. Largeur d'abord : Niveau par niveau, de gauche à droite.
def parcours_prefixe(noeud):
    if noeud is not None:
        return [noeud.valeur] + parcours_prefixe(noeud.gauche) + parcours_prefixe(noeud.droit)
    else:
        return []

def parcours_infixe(noeud):
    if noeud is not None:
        return parcours_infixe(noeud.gauche) + [noeud.valeur] + parcours_infixe(noeud.droit)
    else:
        return []

def parcours_suffixe(noeud):
    if noeud is not None:
        return parcours_suffixe(noeud.gauche) + parcours_suffixe(noeud.droit) + [noeud.valeur]
    else:
        return []
    
# Résultats des parcours
print("Parcours préfixe :", parcours_prefixe(racine))
print("Parcours infixe  :", parcours_infixe(racine))
print("Parcours suffixe :", parcours_suffixe(racine))
Parcours préfixe : [40, 20, 10, 30, 60, 50, 70]
Parcours infixe  : [10, 20, 30, 40, 50, 60, 70]
Parcours suffixe : [10, 30, 20, 50, 70, 60, 40]
def parcours_largeur(racine):
    if not racine:
        return []
    
    file = [racine]
    resultat = []
    while file:
        noeud = file.pop(0)  # Récupère le premier élément de la file
        resultat.append(noeud.valeur)
        if noeud.gauche:
            file.append(noeud.gauche)
        if noeud.droit:
            file.append(noeud.droit)
    return resultat

# Exemple d'utilisation
print("Parcours largeur :",parcours_largeur(racine))
Parcours largeur : [40, 20, 60, 10, 30, 50, 70]

Arbres binaires de recherche : Recherche et insertion

Recherche dans un ABR :

def rechercher(noeud, valeur):
    if noeud is None:
        return False
    if noeud.valeur == valeur:
        return True
    if valeur < noeud.valeur:
        return rechercher(noeud.gauche, valeur)
    return rechercher(noeud.droit, valeur)

print("Recherche 30 :", rechercher(racine, 30))
print("Recherche 100 :", rechercher(racine, 100))
Recherche 30 : True
Recherche 100 : False

Insertion dans un ABR :

def inserer(noeud, valeur):
    if noeud is None:
        return Noeud(valeur)
    if valeur < noeud.valeur:
        noeud.gauche = inserer(noeud.gauche, valeur)
    else:
        noeud.droit = inserer(noeud.droit, valeur)
    return noeud

racine = inserer(racine, 35)
print("Recherche 35 après insertion :", rechercher(racine, 35))
Recherche 35 après insertion : True

Arbres équilibrés (AVL)

Les arbres AVL sont des arbres binaires de recherche auto-équilibrés qui maintiennent une hauteur logarithmique, garantissant des opérations efficaces.

Propriétés : Complexité temporelle de recherche

La recherche dans un arbre AVL est de complexité 𝑂(𝑙𝑜𝑔(𝑛)), où 𝑛 est le nombre de nœuds dans l'arbre.

Si un arbre binaire de recherche n'est pas équilibré (par exemple, une structure en ligne droite), la complexité de recherche peut se dégrader vers 𝑂(𝑛) dans le pire des cas.

Retour au sommaire

03 - Graphe

Fiche de Révision : Graphes - NSI Terminale

Concepts Essentiels des Graphes

Définitions

  1. Sommet : Un point dans le graphe (nœud).
  2. Arête : Une connexion entre deux sommets.
    Orientée : Elle a une direction.
    Non-orientée : Elle n'a pas de direction.
  3. Graphe pondéré : Les arêtes ont un poids (ou coût).
  4. Connexité : Un graphe est connexe si n'importe quelle paire de sommets peut toujours être reliée par une chaîne.
  5. Matrice d'adjacence : On représente les arêtes dans une matrice, c'est-à-dire un tableau à deux dimensions où on inscrit le poids en ligne i et colonne j de l'arête du sommets de rang i vers de rang j.
  6. Liste de successeurs : Liste des sommets accessibles depuis un sommet donné.
  7. Liste de prédécesseurs : Liste des sommets qui pointent vers un sommet donné.

Exemple

import numpy as np

class Graphe_Matrice_Adjacente:
    def __init__(self, matrice_adjacence, noms_sommets):
        self.matrice_adjacence = np.array(matrice_adjacence)
        self.noms_sommets = noms_sommets
        
    def liste_successeurs(self):
        liste_succ = {}
        n = len(self.matrice_adjacence)
        for i in range(n):
            successeurs = []
            for j in range(n):
                if self.matrice_adjacence[i][j]:
                    successeurs.append(self.noms_sommets[j])
            liste_succ[self.noms_sommets[i]] = successeurs
        return liste_succ


# Création d'un graphe avec la matrice d'adjacence donnée
matrice_adjacence = [[ 0, 7, 12,  0, 0],
                     [ 7, 0,  4,  8, 0],
                     [12, 4,  0, 13, 5],
                     [ 0, 8, 13,  0, 6],
                     [ 0, 0,  5,  6, 0] ]

noms_sommets = ['A', 'B', 'C', 'D', 'E']

# Instanciation de la classe Graphe_Matrice_Adjacente
graphe = Graphe_Matrice_Adjacente(matrice_adjacence, noms_sommets)
print("Liste des successeurs :",graphe.liste_successeurs())
Liste des successeurs : {'A': ['B', 'C'], 'B': ['A', 'C', 'D'], 'C': ['A', 'B', 'D', 'E'], 'D': ['B', 'C', 'E'], 'E': ['C', 'D']}

Algorithmes Importants sur les Graphes

DFS (Depth-First Search), traduit en français par "Parcours en profondeur d'abord", consiste à explorer un graphe en allant aussi loin que possible dans une branche avant de revenir en arrière pour explorer les voisins restants. Ce type de parcours ne tient pas compte des poids des arêtes : il se concentre sur le premier voisin non visité, qui à son tour procède de la même manière, jusqu'à ce qu'il n'y ait plus de voisins non explorés.

BFS (Breadth-First Search), traduit en français par "Parcours en largeur", explore tous les nœuds voisins d'un nœud avant de passer au niveau suivant. Ce type de parcours ignore également les poids des arêtes, mais il permet de trouver le plus court chemin entre deux nœuds en supposant que chaque arête a un poids égal à 1.

def DFS(graph,debut):
    result = []
    a_visite = [debut]
    while a_visite:
        noeux=a_visite.pop(0)
        if noeux not in result:
            result.append(noeux)
            a_visite = graph[noeux] + a_visite
    return result

def BFS(graph,debut):
    result = []
    a_visite = [debut]
    while a_visite:
        noeux = a_visite.pop(0)
        if noeux not in result:
            result.append(noeux)
            a_visite = a_visite +  graph[noeux]
    return result

# Exemple de graphe
graph_successeurs =  {'A': ['B', 'C', 'D'],
                      'B': ['A', 'E', 'F'],
                      'C': ['A', 'G', 'H'],
                      'D': ['A'],
                      'E': ['B'], 
                      'F': ['B'], 
                      'G': ['C', 'H'], 
                      'H': ['C', 'G']}

print("Ordre de visite DFS :", DFS(graph_successeurs, 'A'))
print("Ordre de visite BFS :", BFS(graph_successeurs, 'A'))
Ordre de visite DFS : ['A', 'B', 'E', 'F', 'C', 'G', 'H', 'D']
Ordre de visite BFS : ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']

Retour au sommaire

04 - SQL

Fiche de Révision : SQL - Terminale NSI

Partie 1 : Modèle Relationnel

Concepts principaux

  1. Relation : Une table dans une base de données.
  2. Attribut : Une colonne d'une table (propriété des données).
  3. Domaine : Ensemble de valeurs admissibles pour un attribut.
  4. Clé primaire : Identifiant unique d'une ligne dans une table.
  5. Clé étrangère : Attribut établissant un lien avec une clé primaire d'une autre table.

Exemple de schéma relationnel

AUTEURS(id : INT, nom : TEXT, prenom : TEXT, ann_naissance : INT, langue_ecriture : TEXT)

LIVRES(id : INT, titre : TEXT, #id_auteur : INT, ann_publi : INT, note : INT)

Les attributs soulignés sont des clés primaires, le # signifie que l'on a une clé étrangère.

Partie 2 : Requêtes SQL

1. Requêtes de sélection

2. Conditionner les résultats avec WHERE

3. Trier les résultats avec ORDER BY

4. Éviter les doublons avec DISTINCT

6. Compter

SELECT COUNT(*) 
FROM LIVRES 
WHERE note > 9;

7. Manipulations de données

Insérer des données

INSERT INTO LIVRES (id, titre, id_auteur, ann_publi, note)
VALUES (17, 'Hypérion', 11, 1989, 8);

Mettre à jour des données

UPDATE LIVRES
SET note = 10
WHERE titre = 'Dune';

Supprimer des données

DELETE FROM LIVRES
WHERE titre = 'Dune';

Attention : Sans WHERE, toutes les données seront supprimées :

DELETE FROM LIVRES;

Retour au sommaire

05 - Routage

Fiche BAC : Routage - Terminale NSI

1. Adresses IP et sous-réseaux

Concepts clés :

Exemple :

2. Tables de routage

Les routeurs permettent d'aiguiller les paquets entre les différents réseaux.

Une table de routage contient les informations nécessaires pour choisir le chemin :

Exemple : Table de routage du serveur R1

Destination Interface Passerelle
F 192.168.0.254
A 10.0.5.152
E 172.17.1.254
B 172.17.1.254 172.17.1.123
C 10.0.5.152 10.0.5.135

3. Protocole RIP (Routing Information Protocol)

Règles du RIP :

  1. Ajoute une route si elle est inconnue.
  2. Met à jour une route si une distance plus courte est trouvée.
  3. Ignore une route plus longue.
  4. Si un routeur est inactif pendant 3 minutes, la distance devient infinie (16).

4. Protocole OSPF (Open Shortest Path First)

Calcul du coût :

$coût = \frac{10^8}{débit}$$𝑑ébit$ est exprimé en bits/s

Rappel : 1 kbit = 1 000 bits ; 1Mbit = 1 000 kbits ; 1 Gbit = 1 000 Mbits

Le chemin le plus rapide pour aller de l'ordinateur 192.168.0.5 au serveur 10.7.3.8 est donc R1-R2-R4 avec un coût de 4 + 2 + 2 +10 = 18 , et non R1-R3 comme l'aurait indiqué le protocole RIP.

Retour au sommaire

06 - Récursivité et Diviser pour régner

Fiche de Révision : Récursivité et Diviser pour régner

1. La récursivité

Définition :

Une fonction récursive est une fonction qui s'appelle elle-même.

Structure d'une fonction récursive :

  1. Cas de base : Condition qui arrête la récursion.
  2. Appel récursif : Réduction du problème pour atteindre le cas de base.

Exemple : Calcul de la factorielle

def factorielle(n):
    if n == 0:  # Cas de base
        return 1
    else:
        return n * factorielle(n - 1)  # Appel récursif

print(factorielle(5))  # Résultat : 120
120

Points à vérifier pour la récursivité :

Attention :

Par défaut, Python autorise un maximum d'environ 1000 appels récursifs (ce nombre peut varier en fonction de la plateforme).

2. Diviser pour régner

Définition :

Un algorithme « Diviser pour régner » divise un problème en sous-problèmes, les résout de manière indépendante, puis combine les résultats.

Étapes principales :

  1. Diviser : Fractionner le problème en sous-problèmes plus petits.
  2. Régner : Résoudre récursivement chaque sous-problème.
  3. Combiner : Fusionner les solutions des sous-problèmes.

Exemple : Tri fusion (_Merge Sort_)

def tri_fusion(liste):
    if len(liste) <= 1:  # Cas de base
        return liste

    # Diviser
    milieu = len(liste) // 2
    gauche = tri_fusion(liste[:milieu])
    droite = tri_fusion(liste[milieu:])

    # Combiner
    return fusion(gauche, droite)

def fusion(gauche, droite):
    resultat = []
    while gauche and droite:
        if gauche[0] < droite[0]:
            resultat.append(gauche.pop(0))
        else:
            resultat.append(droite.pop(0))
    resultat = resultat + (gauche or droite)
    return resultat

# Exemple d'utilisation
print(tri_fusion([4, 3, 8, 2, 7, 1, 5]))  # Résultat : [1, 2, 3, 4, 5, 7, 8]
[1, 2, 3, 4, 5, 7, 8]

Retour au sommaire

07 - Système sur Puce et Processus Système

Fiche BAC : Système sur Puce et Processus/Système

1. Système sur Puce (SoC)

Définition :

Un Système sur Puce (System on Chip - SoC) intègre tous les composants nécessaires pour faire fonctionner un système informatique sur un seul circuit intégré.

Loi de Moore :

Exemple de SoC :

Processus :

Un processus est un programme en cours d'exécution, comprenant :

Threads :

Interblocage (Deadlock) :

Un interblocage se produit lorsque plusieurs processus attendent indéfiniment qu'une ressource détenue par un autre processus soit libérée.

Conditions pour un interblocage :

  1. Exclusion mutuelle : Les ressources ne peuvent être utilisées que par un processus à la fois.
  2. Rétention et attente : Un processus retient une ressource tout en attendant d'autres ressources.
  3. Non-préemption : Les ressources ne peuvent pas être retirées de force.
  4. Attente circulaire : Les processus forment une chaîne circulaire d'attente de ressources.

Retour au sommaire

08 - Modularité

Fiche de Révision : Modularité

1. Qu'est-ce que la modularité ?

2. Modules Python

import math
dir(math)
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'cbrt', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'exp2', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'lcm', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'nextafter', 'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc', 'ulp']
help(math.sin)
Help on built-in function sin in module math:

sin(x, /)
    Return the sine of x (measured in radians).

print(math.sin.__doc__)
Return the sine of x (measured in radians).

3. Importations en Python

  1. import module : Accès à toutes les fonctions avec la notation pointée (module.fonction()).
  2. from module import fonction : Accès direct à la fonction (fonction()).
  3. *_`from module import _`** : Importe tout (déconseillé pour éviter les conflits de noms).
  4. Alias : Utilisez as pour renommer un module ou une fonction.
import math
math.sqrt(2)
1.4142135623730951
from math import sqrt
sqrt(2)
1.4142135623730951
from math import *
sqrt(2)
1.4142135623730951
from math import sqrt as racine_carree
racine_carree(2)
1.4142135623730951

4. Créer un module

Exemple :

def addition(a, b):
    """
    Retourne la somme de a et b.
       
    """
    return a + b
help(addition)
Help on function addition in module __main__:

addition(a, b)
    Retourne la somme de a et b.

Retour au sommaire

09 - Tri par insertion, tri par selection

Fiche de Révision : Tri par insertion, tri par selection

Tri par insertion

def tri_insertion(tab):
    n = len(tab)
    for i in range(1, n):
        valeur_insertion = tab[i]
        j = i
        while j > 0 and valeur_insertion < tab[j-1]:
            tab[j] = tab[j-1]
            j = j - 1
        tab[j] = valeur_insertion
    return tab

# Exemple d'utilisation
tab = [4, 3, 5, 1]
print("Tableau trié:",tri_insertion(tab) )
Tableau trié: [1, 3, 4, 5]

Tri par sélection

def tri_selection(tab):
    for i in range(len(tab)):
        min_i = i
        for j in range(i+1, len(tab)):
            if tab[min_i] > tab[j]:
                min_i = j
        tab[i], tab[min_i] = tab[min_i], tab[i]

# Exemple d'utilisation
tab = [12, 11, 13, 5, 6]
tri_selection(tab)
print("Tableau trié:", tab)
Tableau trié: [5, 6, 11, 12, 13]

Coût des algorithmes de tris par insertion, et par sélection

Propriété :
Les algorithmes de tris par insertion, par sélection ont un coût O($n^2$), où $n$ est la taille du tableau.

Cela signifie que le temps d'exécution augmente de manière quadratrique avec le nombre d'éléments dans le tableau.

Démonstration :
Comme chaque boucle interne peut prendre jusqu'à $n$ itérations et il y a $n$ boucles externes, cela donne un total de $n×n=n^2$ opérations dans le pire cas.

Retour au sommaire

10 - Les congruences

Les congruences


Théorème de Bézout :

Si a et b sont des entiers non nuls, alors il existe des entiers relatifs x et y, et un entier d tels que ax + by = d où d est le PGCD de a et b.

Algorithme d'Euclide (Classe de 3ième) ou PGCD de a et b

PDCD(a,b) = ?

1er cas : si b = 0
PDCD(a,b) = PDCD(a,0) = a

2ième Cas : si b > 0
Posons a = qb + r où q est le quotient et r est le reste de la division euclidienne de a par b.
d = PGCD(a,b) = PGCD(b,r)

def pgcd(a, b):
    if b == 0:
        return a
    else:
        return pgcd(b, a % b)

# Exemple d'utilisation
a = 91
b = 77
print("Le PGCD de", a, "et", b, "est :", pgcd(a, b))
Le PGCD de 91 et 77 est : 7

Algorithme d'Euclide étendu ou comment trouver des coefficients x et y de Bezout et le PDCD de a et b

1er cas : si b = 0
d =a, x = 1, y = 0 est une solution de ax + by = d

2ième Cas : si b > 0
ax + by = d <=> (qb + r)x + by = d <=> b(qx + y) + rx = d <=> bx1 + ry1 = d avec x1 = qx + y et y1 = x <=> bx1 + ry1 = d avec x = y1 et y = x1 - qy1

def euclide_etendu(a, b):
    if b == 0:
        return a, 1, 0
    else:
        pgcd, x1, y1 = euclide_etendu(b, a % b)
        x = y1
        y = x1 - (a // b) * y1
        return pgcd, x, y

# Exemple d'utilisation
a = 91
b = 77
pgcd, x, y = euclide_etendu(a, b)
print(f"Une solution de {a}x + {b}y = d est {a}({x}) + {b}({y}) = {pgcd}.")
Une solution de 91x + 77y = d est 91(-5) + 77(6) = 7.

Théorème de Gauss :

Si a, b et c sont des entiers non nuls.
Si a divise le produit bc et si a et b sont premiers entre eux alors a divise c.

Définition de la Congruence :

Soient a, b, n des entiers relatifs.
On dit que a est congruent à b modulo n, noté a≡b (mod n) ou a≡b[n], si n divise la différence a−b.

Propriétés des Propriétés de Congruence :

  1. Réflexivité : Pour tout entier relatif a, a ≡ a [n].
  2. Symétrie : Si a et b sont des entiers relatifs et a ≡ b [n], alors b ≡ a [n].
  3. Transitivité : Si a, b, et c sont des entiers relatifs et a ≡ b [n] et b ≡ c [n], alors a ≡ c [n].
  4. Addition : Si a, b, c, et d sont des entiers relatifs et a ≡ b [n] et c ≡ d [n], alors a + c ≡ b + d [n].
  5. Soustraction : Si a, b, c, et d sont des entiers relatifs et a ≡ b [n] et c ≡ d [n], alors a - c ≡ b - d [n].
  6. Multiplication : Si a, b, c, et d sont des entiers relatifs et a ≡ b [n] et c ≡ d [n], alors a * c ≡ b * d [n].
  7. Puissance : Si a et b sont des entiers relatifs et a ≡ b [n], alors ak ≡ bk [n] pour tout entier k.
  8. Petit Théorème de Fermat : Si p est un nombre premier et a est un entier relatif non divisible par p, alors ap-1 ≡ 1 [p].

Critère de Divisibilité par 9

Un nombre entier est divisible par 9 si et seulement si la somme de ses chiffres est divisible par 9.

Démonstration :

Soit un nombre entier N.
N = 10nanan + 10n-1an-1+...+102a2+ 10a1 où ak sont ses chiffres.

10 ≡ 1 [9]
Donc pour tout entier k, on a : 10k ≡ 1 [9]
Donc N ≡ an + an-1 + ... + a0 [9]

Retour au sommaire

↑ Haut de la page