Graphes (partie 2)

Comme pour les arbres, il existe différentes façons de parcourir un graphe. On peut le parcourir en largeur ou en profondeur. Le parcours d’un graphe consiste à visiter tous ses nœuds.

Parcours en largeur

Nous allons utiliser des nœuds avec un attribut « visite » à « Faux » par défaut et qui sera mis à « Vrai » lorsqu’il aura été visité. Des nœuds sont dit adjacents ou voisins s’ils sont réliés par une arrête. Voici donc un algorithme du parcours en largeur :

VARIABLES
f : file
nœud : nœud de départ (origine)
x : nœud actuellement visité
y : nœud

DEBUT
nœud.visite = Vrai
f.enfile(nœud)
tant que f est non vide:
    x = f.defile()
    afficher x
    pour chaque y adjacent à x:
        si y.visite est Faux:
            y.visite = Vrai
            f.enfile(y)
FIN

1) Appliquez cet algorithme au graphe ci-dessous en partant de A. On notera sur une même ligne le nœud actuellement visité et le contenu de la file f. Dans le cas de plusieurs voisins, on les ajoutera dans l’ordre alphabétique.

Graphe non-orienté avec 9 sommets

2) Faire un tableau des distances entre A et les autres nœuds.

3) Qui a-t-il de remarquable entre ce tableau et le parcours en largeur ?

4) Appliquez à nouveau cet algorithme au graphe ci-dessous en prenant A comme nœud initial.

Arbre avec 8 sommets

Parcours en profondeur

Voici maintenant un algorithme qui propose un parcours en profondeur :

VARIABLES
nœud : nœud de départ (origine)

DEBUT
profondeur(nœud):
    nœud.visite = True
    afficher nœud
    pour chaque y adjacent à nœud:
        si y.visite est Faux:
            profondeur(y)

FIN

5) Quelle est la particularité de ce programme par rapport au parcours en largeur ?

6) Appliquez cet algorithme au graphe ci-dessous en partant de A.

Graphe non-orienté avec 9 sommets

7) Appliquez à nouveau cet algorithme au graphe ci-dessous en prenant A comme nœud initial.

Arbre avec 8 sommets

Chemin

Il est souvent nécessaire de trouver un chemin entre deux nœuds d’un graphe. Voici un algorithme qui permet de faire cette recherche :

VARIABLES
graphe : le graphe
start : nœud de départ (origine)
end : nœud d'arrivée
chaine : chemin initialement vide

DEBUT
trouve-chaine(graphe, start, end, chaine)
    ajouter start à chaine
    si start = end:
        retourner chaine
    pour chaque y adjacent à start:
        si y n'appartient pas à chaine:
            chemin = trouve-chaine(graphe, y, end, chaine)
            si chemin est non-vide
                retourner chemin
    retourner None
FIN

8) Appliquez rigoureusement cet algorithme au graphe ci-dessous entre A et C.

Graphe non-orienté avec 9 sommets

9) Appliquez à nouveau cet algorithme au graphe ci-dessous entre A et C.

Arbre avec 8 sommets

Cycle

Un cycle est une suite d’arrêtes (donc une chaine) dont les deux extrémités sont identiques.

Il est parfois intéressant de savoir si un graphe contient un cycle. Par exemple un graphe qui contient un cycle ne peut pas être un arbre.

Voici donc un algorithme qui permet de détecter s’il y a au moins un cycle dans un graphe :

VARIABLES
graphe : le graphe
d : le nœud de départ
p : pile

DEBUT
trouve-cycle(graphe, d)
    p.empile(d)
    tant que p n'est pas vide:
        x = p.depile()
        pour chaque y adjacent à x:
            si y.visite est Faux:
                p.empile(y)
        si x.visite est Vrai:
            retourner Vrai
        sinon:
            x.visite = Vrai
    retourner Faux
FIN

10) Appliquez cet algorithme au graphe en haut de page en prenant A comme nœud initial.

11) Appliquez à nouveau cet algorithme au graphe ci-dessous en prenant A comme nœud initial.

Arbre avec 8 sommets

implémentations : programmes de référence

Programme 1 —Parcours en profondeur

def parcours(g, vus, s):
     """parcours en profondeur depuis le sommet s"""
     if s not in vus:
          vus.add(s)
          for v in g.voisins(s):
               parcours(g, vus, v)

def existe_chemin(g, u, v):
     """existe-t-il un chemin de u à v ?"""
     vus = set()
     parcours(g, vus, u)
     return v in vus

Programme 2 — Parcours en profondeur avec une pile

def parcours(g, vus, s):
     """parcours en profondeur depuis le sommet s"""
     pile = Pile()
     pile.empiler(s)
     while not pile.est_vide():
          s = pile.depiler()
          if s in vus:
               continue
          vus.add(s)
          for v in g.voisins(s):
               pile.empiler(v)

Programme 3 — Détecter la présence d’un cycle dans un graphe

BLANC, GRIS, NOIR = 1, 2, 3
def parcours_cy(g, couleur, s):
     """parcours en profondeur depuis le sommet s"""
     if couleur[s] == GRIS:
          return True
     if couleur[s] == NOIR:
          return False
     couleur[s] = GRIS
     for v in g.voisins(s):
          if parcours_cy(g, couleur, v):
               return True
          couleur[s] = NOIR
     return False

def cycle(g):
     couleur = {}
     for s in g.sommets():
          couleur[s] = BLANC
     for s in g.sommets():
          if parcours_cy(g, couleur, s):
               return True
     return False

Programme 4 — Parcours en largeur

def parcours_largeur(g, source):
     """parcours en largeur depuis le sommet source"""
     dist = {source: 0}
     courant = {source}
     suivant = set()
     while len(courant) > 0:
          s = courant.pop()
          for v in g.voisins(s):
               if v not in dist:
                    suivant.add(v)
                    dist[v] = dist[s] + 1
          if len(courant) == 0:
               courant, suivant = suivant, set()
     return dist

def distance(g, u, v):
     """distance de u à v (et None si pas de chemin)"""
     dist = parcours_largeur(g, u)
     return dist[v] if v in dist else None

Exercices

Exercice 1

Dérouler à la main le parcours en profondeur sur le graphe suivant

pour différentes valeurs du sommet de départ. Donner à chaque fois la valeur finale de l’ensemble vus, c’est-à-dire l’ensemble des sommets atteints par le
parcours.


Exercice 2

Dérouler à la main le parcours en largeur sur le graphe suivant


pour différentes valeurs du sommet de départ. Donner à chaque fois la valeur finale du dictionnaire dist, c’est-à-dire la distance à la source de chaque
sommet atteint par le parcours.


Exercice 3

On peut se servir d’un parcours en profondeur pour déterminer si un graphe non orienté est connexe, c’est-à-dire si tous ses sommets
sont reliés entre eux par des chemins. Pour cela, il suffit de faire un parcours en profondeur à partir d’un sommet quelconque, puis de vérifier
que tous les sommets ont été atteints par ce parcours. Écrire une fonction est_connexe() qui réalise cet algorithme. On pourra se servir de la méthode
sommets() de la classe Graphe et de la fonction parcours du programme de référence 1 voir plus haut .


Exercice 4

Dans cet exercice, on se propose d’utiliser le parcours en profondeur pour construire un chemin entre deux sommets, lorsque c’est possible.
On le fait avec deux fonctions, comme dans le programme de référence 1 voir plus haut.

def parcours_ch(g, vus, org, s):
    """parcours depuis le sommet s, en venant de org"""
    …
def chemin(g, u, v):
    """un chemin de u à v, le cas échéant, None sinon"""
    …

L’idée est que l’attribut vus n’est plus un ensemble mais un dictionnaire, qui associe à chaque sommet visité le sommet qui a permis de l’atteindre
pendant le parcours en profondeur. La fonction parcours_ch prend un argument supplémentaire, org (pour origine), qui est justement le sommet qui
a permis d’atteindre s, en empruntant l’arc org → s. Écrire le code de la fonction parcours_ch ; il est très semblable à celui de la fonction parcours
du programme de référence n°1 . Écrire ensuite le code de la fonction chemin qui renvoie un chemin entre les sommets u et v, le cas échéant, et None s’il n’existe pas de chemin entre ces deux sommets. Pour cela, lancer un parcours en profondeur à partir du sommet u, en donnant à org la valeur None, puis, si le sommet v a été atteint, construire le chemin dans un tableau en« remontant » le dictionnaire vus de v jusqu’à u.



Exercice 5

Dans cet exercice, on se propose d’utiliser le parcours en largeur pour construire un chemin de longueur minimale entre deux sommets.
On le fait avec deux fonctions, comme dans le programme de référence n°4.

def parcours_largeur_ch(g, source):
    """parcours depuis le sommet source"""
    …

def chemin(g, u, v):
    """un chemin de u à v, le cas échéant, None sinon"""
    …

L’idée est qu’un dictionnaire vus remplace le dictionnaire dist. Ce dictionnaire vus associe à chaque sommet visité le sommet qui a permis
de l’atteindre pendant le parcours en largeur. Écrire le code de la fonction parcours_largeur_ch ; il est très semblable à celui de la fonction
parcours_largeur du programme de référence 4. Pour le sommet source, on lui associe la valeur None dans le dictionnaire vus. Écrire ensuite le code de la fonction chemin qui renvoie un chemin réalisant la distance entre les sommets u et v, le cas échéant, et None s’il n’existe pas de chemin entre ces deux
sommets. Pour cela, lancer un parcours en largeur à partir du sommet u puis, si le sommet v a été atteint, construire le chemin dans un tableau
en« remontant » le dictionnaire vus de v jusqu’à u.


Exercice 6

Les nœuds d’un arbre binaire peuvent être vus comme les sommets d’un graphe non orienté. En particulier, on peut donc parcourir les nœuds d’un arbre avec un parcours en profondeur ou en largeur. Pour le parcours en profondeur, il s’agit d’un parcours que nous avons déjà présenté, à savoir le parcours préfixe . Dans cet exercice, on se propose de parcourir les nœuds d’un arbre binaire avec un parcours en largeur.
Écrire une fonction largeur(a) qui reçoit un arbre binaire a en argument et imprime les valeurs de ses différents nœuds dans un ordre donné par un
parcours en largeur, c’est-à-dire la valeur contenue dans la racine, puis les valeurs contenues dans les nœuds immédiatement en-dessous (profondeur 1),
puis celles contenues dans les nœuds encore en-dessous (profondeur 2), etc.
Adapter pour cela le code du programme de référence 4.

Corrections

Exercice 1

sommet de départvaleur finale de vus
0 {0, 1, 3, 2}
1{1, 2}
2{2}
3{3, 1, 2}

Exercice 2

sommet de départvaleur finale de dist
0{0 → 0, 1 → 1, 3 → 1, 2 → 2}
1{1 → 0, 2 → 1}
2{2 → 0}
3{3 → 0, 1 → 1, 2 → 2}

Exercice 3

On suit l’algorithme proposé, en faisant uniquement attention au cas pathologique d’un graphe qui ne contiendrait aucun
sommet.

def est_connexe(g):
    """le graphe est-il connexe ?
    (uniquement pour un graphe non orienté)"""
    ts = g.sommets()
    if len(ts) == 0:
        return True
    s = ts.pop()
    vus = set()
    parcours(g, vus, s)
    for s in ts:
        if s not in vus:
            return False
    return True

Exercice 4

Pour parcours_ch, l’ajout dans un ensemble devient un ajout dans un dictionnaire.

def parcours_ch(g, vus, org, s):
    """parcours depuis le sommet s, en venant de org"""
    if s not in vus:
        vus[s] = org
        for v in g.voisins(s):
            parcours_ch(g, vus, s, v)

Pour chemin, on prend soin de tester si v a été atteint par le parcours. Dans le cas contraire, on renvoie None. Sinon, on construit le chemin avec une
boucle while.

def chemin(g, u, v):
    """un chemin de u à v, le cas échéant, None sinon"""
    vus = {}
    parcours_ch(g, vus, None, u)
    if v not in vus:
        return None
    ch = []
    s = v
    while s is not None:
        ch.append(s)
        s = vus[s]
    ch.reverse()
    return ch

De fait, le chemin est construit à l’envers. On prend soin de le renverser avec la méthode reverse avant de le renvoyer.

Exercice 5

On garde la structure du programme 4, le dictionnaire vus prenant la place du dictionnaire dist.

def parcours_largeur_ch(g, source):
    """parcours depuis le sommet source"""
    vus = {source: None}
    courant = {source}
    suivant = set()
    while len(courant) > 0:
        s = courant.pop()
        for v in g.voisins(s):
            if v not in vus:
                suivant.add(v)
                vus[v] = s
        if len(courant) == 0:
            courant, suivant = suivant, set()
    return vus

La seconde partie, à savoir la reconstruction du chemin, n’est pas différente de celle effectuée dans l’exercice précédent pour un parcours en profondeur.

def chemin(g, u, v):
    """un chemin de u à v, le cas échéant, None sinon"""
    vus = parcours_largeur_ch(g, u)
    if v not in vus:
        return None
    ch = []
    s = v
    while s is not None:
        ch.append(s)
        s = vus[s]
    ch.reverse()
    return ch

Exercice 6

La subtilité consiste ici à correctement traiter les arbres vides. On a le choix de le faire avant ou après l’insertion dans les ensembles.
On choisit ici de le faire après, ce qui évite de faire un cas particulier pour un arbre qui serait intégralement vide.

def largeur(a):
    courant = []
    suivant = []
    courant.append(a)
    while len(courant) > 0:
        n = courant.pop()
        if n is None:
            continue
        print(n.valeur)
        suivant.append(n.gauche)
        suivant.append(n.droit)
        if len(courant) == 0:
            courant, suivant = suivant, []

Graphes (partie 1)

Activité : « Qui suit qui ? »

1. La mise en situation

Dessiner sur votre feuille 5 prénoms (Alice, Bob, Charles, Driss, Elsa) éparpillés de manière aléatoire. Voici leurs relations sur Instagram :

Alice suit Bob et Elsa.

Bob suit Alice.

Charles suit Alice et Driss.

Driss ne suit personne.

Elsa suit Bob et Charles.

Consigne : Représenter ces relations par des flèches sur leur cahier.

2. Pourquoi pas une liste ?

Nous avons vu les listes chaînées où chaque nœud a un seul suivant. Pourquoi est-ce que notre classe Cellule habituelle ne suffit pas pour modéliser cette situation ? »

3. Formalisation

Introduisez le vocabulaire de base en utilisant le dessin de correction :

Les personnes sont des ……………………………………….

Les flèches sont des ……………………………………………

L’ensemble s’appelle …………………………………………..

4. Comment coder ça ?

Si on devait créer une classe Sommet, à quoi ressemblerait son constructeur pour qu’on puisse retrouver ses abonnés ? »

class Sommet:
    def __init__(self, nom):
        ………………………………………….
        ………………………………………….

# Exemple de création
a = Sommet("Alice")
b = Sommet("Bob")
a.voisins.append(b) # Alice suit Bob

Exercice de clôture : Le Réseau des Développeurs

On considère le mini-graphe suivant représentant trois serveurs informatiques connectés :

Le serveur A est connecté au serveur B.

Le serveur B est connecté au serveur C.

Le serveur C est connecté au serveur A (c’est une boucle).

  1. En utilisant la classe Sommet définie précédemment, écrivez les instructions Python permettant de créer ces trois serveurs et d’établir leurs connexions.

Si on exécute l’instruction print(a.voisins[0].voisins[0].nom), qu’est-ce qui va s’afficher à l’écran ?

Définition

Un graphe est un objet mathématique très simple avec lequel on peut faire des choses extrêmement compliquées. Il est constitué de sommets (ou nœuds) reliés par des arêtes (ou liens). Si les arêtes sont fléchées, on parle de graphe orienté sinon on parle de graphe non-orienté. Dans un graphe orienté, on appelle parents d’un nœud, les nœuds qui pointent vers lui. On appelle fils d’un nœud, les nœuds vers lesquels il pointe.

Un graphe peut représenter un réseau social, des liens d’amitié, un réseau routier.

1) Pour le graphe ci-dessous, dire s’il est orienté ou non, donner le nombre de sommets et le nombre d’arêtes.

Graphe avec six sommets et des flèches

C’est un graphe orienté (il y a des flèches). Il a 6 sommets et 8 arêtes.

2) Pour le graphe ci-dessous, dire s’il est orienté ou non, donner le nombre de sommets et le nombre d’arêtes.

Graphe avec cinq sommets sans flèches

C’est un graphe non-orienté (il n’y a pas de flèches). Il a 5 sommets et 6 arêtes.

On utilisera parfois des graphes pondérés pour représenter par exemple des distances sur une carte. Dans un tel graphe, les arêtes sont associées à un poids :

Graphe avec cinq sommets, sans flèches et avec des valeurs sur les arêtes

Propriétés

Les propriété les plus communes pour parler d’un graphe sont les suivantes :

  • une chaîne (ou chemin) est une suite d’arêtes consécutives ;
  • la longueur d’une chaîne est le nombre d’arêtes qui la compose ;
  • la distance entre deux sommets est la longueur de la chaîne la plus courte entre ces deux sommets ;
  • un cycle est une chaîne fermée composée d’arêtes distinctes. C’est à dire qui commence et termine par le même sommet et qui n’empreinte pas deux fois la même arête.

3) Pour le graphe ci-dessous, trouvez deux cycles. Donnez également la distance entre A et D.

Graphe avec cinq sommets sans flèches

Voici les trois cycles possibles : DCBD, DCEBD et CEBC. La distance entre A et B est 2.

Représentation

Nous avons utilisé intuituvement une représentation visuelle d’un graphe. Mais nous aurions pû utiliser une description textuelle d’un graphe. Par exemple pour celui de la question 2 :

  • A est relié à E ;
  • B est relié à C, D et E ;
  • C est relié à B, D et E ;
  • D est relié à C et B ;
  • E est relié à A, B et C.

4) Dessinez le graphe correspondant à la description ci-dessous :KevinMattéoTomEmmaClaireDavid

  • Tom est ami avec Emma, Claire et Mattéo ;
  • Claire est amie avec Tom, Emma et David ;
  • Mattéo est ami avec Tom et Kevin ;
  • Emma est amie avec Tom et Claire ;
  • David est ami avec Claire ;
  • Kevin est ami avec Mattéo.

5) Dessinez le graphe correspondant à la description ci-dessous :

  • A est à 12 km de B ;
  • B est à 12 km de A, 9 km de C et 25 km de D ;
  • C est à 9 km de B ;
  • D est à 25 km de B et 6 km de E ;
  • E est à 6 km de D.

Implémentations 1

On parle d’implémentation lorsqu’il s’agit de « représenter » un graphe dans un système informatique. Même si nous ne verrons pas directement comment on implémente un graphe en Python nous allons parler des deux principales implémentations d’un graphe.

Matrice d’adjacence

Une matrice n’est qu’un tableau à double entrée :

Matrice 3x4

Nous avons ci-dessus une matrice à 3 lignes et 4 colonnes. En mathématiques on utilise les matrices pour effectuer des calculs, nous les utiliserons ici simplement comme des tableaux à deux dimensions.

Une matrice d’adjacence est une matrice carrée (même nombre de lignes que de colonnes) où chaque ligne et chaque colonne représentent un sommet. Pour un graphe non-orienté on mettra un « 1 » sur la case [i][j] s’il y a une arête entre le sommet i et le sommet j, et un « 0 » sinon. Pour un graphe orienté on mettra un « 1 » sur la case [i][j] s’il y a une arête du sommet i vers le sommet j, et un « 0 » sinon. Pour un graphe pondéré on mettra sur la case [i][j] le poids du lien entre le sommet i et le sommet j.

6) Donnez la matrice d’adjacence du graphe ci-dessous.

Graphe avec cinq sommets sans flèches

7) Donnez la matrice d’adjacence du graphe ci-dessous.

Graphe avec six sommets et des flèches

8) Donnez la matrice d’adjacence du graphe ci-dessous.

Graphe avec cinq sommets, sans flèches et avec des valeurs sur les arêtes

Listes d’adjacence

Pour un graphe non orienté, une liste d’adjacence associe à chaque sommet la liste de ses voisins. Par exemple, pour le graphe ci-dessous

Graphe avec cinq sommets sans flèches

La liste d’adjacence serait :

  • A → [B, C, E] ;
  • B → [A] ;
  • C → [A, D] ;
  • D → [C, E] ;
  • E → [A, D].

Pour un graphe orienté, une liste d’adjacence associe à chaque sommet la liste de ses successeurs. C’est à dire les sommets vers lesquels il pointe. Il est également possible de faire une liste d’adjacence des prédécesseurs, ou même les deux pour des raisons de symétrie.

9) Donnez la liste d’adjacence du graphe ci-dessous.

Graphe avec cinq sommets sans flèches

10) Donnez la liste d’adjacence des successeurs du graphe ci-dessous.

Graphe avec six sommets et des flèches

Choix d’implémentation

On utilisera une matrice d’adjacence dans le cas d’un graphe avec beaucoup de liens (on parle de graphe dense) et une liste d’adjacence dans le cas contraire. Cela permettra de gagner de la mémoire. Le choix peut aussi dépendre de l’utilité du graphe. En fonction de l’algorithme utilisé, on pourra préférer une implémentation à l’autre.

Implémentations 2

Nous avons vu dans ci-dessus, deux façons de représenter un graphe : par matrice d’adjacence ou par liste d’adjacence. Nous allons utiliser ces deux implémentations ici.

Matrice d’adjacence

Nous utiliserons le même graphe que le précédent :

1) Créez en Python la matrice d’adjacence de ce graphe.

2) (Dificile) Proposez une fonction matrice_largeur permettant de parcourir en largeur ce graphe. On utilisera une liste pour enregistrer les nœuds visités.

3) (Plus dificile) Proposez une fonction matrice_profondeur permettant de parcourir en profondeur ce graphe.

Liste d’adjacence

Nous utiliserons ici le même graphe que précédement.

4) Créez en Python les listes d’adjacence de ce graphe. (On utilisera un tableau à deux dimensions)

5) (Dificile) Proposez une fonction listes_largeur permettant de parcourir en largeur ce graphe. On utilisera une liste pour enregistrer les nœuds visités.

6) (Plus difficile) Proposez une fonction listes_profondeur permettant de parcourir en profondeur ce graphe.

programmes de référence

programme 1 : Graphe représenté par une matrice d’adjacence

class Graphe:
    """un graphe représenté par une matrice d’adjacence,
    où les sommets sont les entiers 0,1,...,n-1"""

    def __init__(self, n):
        self.n = n
        self.adj = [[False] * n for _ in range(n)]

    def ajouter_arc(self, s1, s2):
        self.adj[s1][s2] = True

    def arc(self, s1, s2):
        return self.adj[s1][s2]

    def voisins(self, s):
        v = []
        for i in range(self.n):
            if self.adj[s][i]:
                v.append(i)
        return v

programme 2 : Graphe représenté par un dictionnaire d’adjacence

class Graphe:
    """un graphe comme un dictionnaire d’adjacence"""

    def __init__(self):
        self.adj = {}

    def ajouter_sommet(self, s):
        if s not in self.adj:
            self.adj[s] = set()

    def ajouter_arc(self, s1, s2):
        self.ajouter_sommet(s1)
        self.ajouter_sommet(s2)
        self.adj[s1].add(s2)

    def arc(self, s1, s2):
        return s2 in self.adj[s1]

    def sommets(self):
        return list(self.adj)

    def voisins(self, s):
        return self.adj[s]

programme 3 : Coloriage glouton d’un graphe

def mex(voisins, couleur):
    """la plus petite couleur non utilisée par les voisins"""
    n = len(voisins)
    dispo = [True] * (n + 1)
    for v in voisins:
        if v in couleur and couleur[v] <= n:
            dispo[couleur[v]] = False
    for c in range(n + 1):
        if dispo[c]:
            return c

assert False # on n’arrivera jamais ici

def coloriage(g):
    """colorie graphe g avec un algorithme glouton"""
    couleur = {}
    n = 0
    for s in g.sommets():
        c = mex(g.voisins(s), couleur)
        couleur[s] = c
        n = max(n, c + 1)
    return couleur, n

Exercices

Exercice 1

Dessiner tous les graphes non orientés ayant exactement trois sommets.


Exercice 2

Combien y a-t-il de graphes orientés ayant exactement trois sommets ? On ne demande pas de les dessiner tous, mais seulement de les dénombrer.


Exercice 3

Ajouter à la classe Graphe du programme 1 (matrice d’adjacence) une méthode afficher pour afficher le graphe sous la forme suivante
0 -> 1 3
1 -> 2 3
2 -> 3
3 -> 1
c’est-à-dire une ligne par sommet, avec pour chacun la liste de ses voisins.


Exercice 4

Ajouter à la classe Graphe du programme 2 (dictionnaire d’adjacence) une méthode afficher pour afficher le graphe sous la forme suivante
0 {1, 3}
1 {2, 3}
3 {1}
2 {3}
c’est-à-dire une ligne par sommet, avec pour chacun l’ensemble de ses voisins.
L’ordre des sommets n’est pas important. L’ensemble des voisins peut être affiché directement avec print.


Exercice 5

Ajouter à la classe Graphe du programme 2 une méthode
nb_sommets() qui donne le nombre de sommets du graphe.

Exercice 6

Ajouter à la classe Graphe du programme 1 une méthode degre(s) qui donne le nombre d’arcs issus du sommet s. On appelle cela le degré du sommet s.


Exercice 7

En utilisant l’exercice précédent, ajouter à la classe Graphe du programme 36 une méthode nb_arcs() qui donne le nombre total d’arcs du graphe.


Exercice 8

Ajouter à la classe Graphe du programme 2 une méthode degre(s) qui donne le nombre d’arcs issus du sommet s. On appelle cela le degré du sommet s.


Exercice 9

En utilisant l’exercice précédent, ajouter à la classe Graphe du programme 2 une méthode nb_arcs() qui donne le nombre total d’arcs du graphe.


Exercice 10

Ajouter aux classes Graphe des programmes 1 et 2 une méthode supprimer_arc(s1, s2) pour supprimer l’arc entre les sommets s1 et s2. S’il n’y a pas d’arc entre ces sommets, la méthode n’a aucun effet. On supprime un élément d’un ensemble avec sa méthode remove.


Exercice 11

Donner une coloration du graphe des régions ci-dessous qui n’utilise que quatre couleurs.

Corrections

Exercice 1

Il y en a 8 au total

Exercice 2

Il y a six arcs possibles (0 → 1, 1 → 0, 1 → 2, 2 → 1, 2 → 0, 0 → 2), chacun pouvant être présent ou absent, soit 26 = 64
possibilités au total.

Exercice 3

def afficher(self):
     for s in range(self.n):
     print(s, "->", end="")
     for v in range(self.n):
          if self.adj[s][v]:
               print("", v, end="")
     print()

Exercice 4

On fait un cas particulier pour l’ensemble vide, histoire qu’il ne soit pas affiché comme set() mais comme {}.

def afficher(self):
     for s in self.adj:
          if len(self.adj[s]) == 0:
               print(s, "{}")
          else:
               print(s, self.adj[s])

Exercice 5

Le nombre de sommets est égal au nombre d’entrées dans le dictionnaire :

def nb_sommets(self):
     return len(self.adj)

Exercice 6

Le code est très semblable à celui de la méthode voisins :

def degre(self, s):
     d = 0
     for i in range(self.n):
          if self.adj[s][i]:
               d += 1
     return d

Exercice 7

Il suffit de faire la somme de tous les degrés.

def nb_arcs(self):
     n = 0
     for s in range(self.n):
          n += self.degre(s)
     return n

Exercice 8

C’est exactement le cardinal de l’ensemble adj[s] :

def degre(self, s):
     return len(self.adj[s])

Exercice 9

Comme dans l’exercice 7, on fait la somme de tous les degrés.

def nb_arcs(self):
     n = 0
     for s in self.adj:
          n += self.degre(s)
     return n

Exercice 10

Pour la matrice d’adjacence, on affecte le booléen False dans la matrice :

def supprimer_arc(self, s1, s2):
     self.adj[s1][s2] = False

Pour le dictionnaire d’adjacence, on supprime s2 de l’ensemble des voisins de s1, si l’arc existe :

def supprimer_arc(self, s1, s2):
     if self.arc(s1, s2):
          self.adj[s1].remove(s2)

Exercice 11

Auvergne-Rhône-Alpes 0
Bourgogne-Franche-Comté 1
Bretagne 1
Centre-Val de Loire 2
Corse 0
Grand Est 0
Hauts-de-France 1
Île-de-France 3
Normandie 0
Nouvelle-Aquitaine 1
Occitanie 2
Pays de la Loire 3
Provence-Alpes-Côte dAzur 1
Guadeloupe 0
Martinique 0
Guyane 0
La Réunion 0
Mayotte 0

Création Musicale

Les vieux grincheux vous le répéteront encore et encore, l’état actuel de la musique est cataclysmique. Ils pourront vous citer des exemples à la pelle et se lamenter que des morceaux aussi médiocres soient autant écoutés sur Spotify et autres plateformes de consommation musicale. Pour renforcer leur argument ils vous citeront un grand nombre de chef d’œuvre obscures ayant marqués leurs jeunes années, et vous diront « ça, ça c’était de la musique ». Contrairement à ce que ces passéistes peuvent croire, le problème ne date pas d’aujourd’hui.

L’essentiel de la musique populaire n’est pas composé par les artistes qui l’interprètent, mais par des petits génies ayant un doigt sur le pouls de la culture et sachant adapter leurs petites recettes à l’humeur générale. Même les bons groupes, après un deux ou trois albums novateurs et un nombre substantiel de fans accumulés, troquent l’ébullition et la créativité débridée de leurs premières années pour un pilote automatique leur permettant répliquer leur recette encore et encore pour leur public de futur nostalgiques/passéistes. Ceci dit, qu’on les apprécie ou non,  tous ces producteurs, compositeurs et interprètes, tous ces groupes et artistes solos, ont passé des années à développer la maitrise de leur champ de compétence.

Avec les capacités croissantes des ordinateurs, de nombreux logiciels sont apparus pour faciliter la vie des musiciens, et réduire les coûts d’enregistrement. Il n’a jamais été aussi abordable de créer et d’enregistrer de la musique. Avec l’apparition de FrutyLoops, GarageBand et autres, la production musicale est devenue un jeu d’enfant. Les absences de culture, et de compétences sont effacée par l’ordinateur prenant le relai (coupage, collage, variation de vitesse, correction de pitch pour les enregistrements, arpégiateurs, …)

après avoir composé ses premiers titres sur ordinateur portable dans sa chambre, Tinashe doit se sentir comme à la maison !

Les IA destinées à la production musicales comme SUNO, UDIO, ElevenLabs s’inscrivent en continuité de ce mouvement. Les utilisateurs ont besoin d’encore moins de compétence et de temps pour obtenir des résultats d’un niveau professionnel. C’est aussi en rupture de l’évolution des techniques de « création » musicale, car la machine ne se contente plus d’assister le créatif, elle le remplace partiellement ou totalement. En appuyant sur un bouton l’IA peut créer un morceau de A à Z à votre place. Il sera bien fichu mais très générique. Vous pouvez personnaliser ça en donnant des indications sur ce que vous voulez obtenir en termes d’accompagnement : style de musique, instruments, tempo, atmosphère, clé, mode ; ou en termes de paroles : indications générales pour le générateur automatique de paroles, ou texte à compléter ou entier.

L’intelligence artificielle en analysant des millions de titres, a appris à deviner quelle est la note la plus appropriée après une séquence, ou quel genre de refrain mettre après un couplet, quel rythme donner à une suite de syllabe. Il le fait en suivant ce qui a été le plus populaire jusqu’ici. Les morceaux générés sont la synthèse d’une recette magique jamais énoncée mais utilisée inconsciemment par des millions de musiciens. Chaque nouvelle génération est unique mais avec un grand nombre de paramètres, les variations sont limitées, et on va avoir des sorties très ressemblantes.   

Il y a bien des raisons de faire de la musique : faire danser les gens, contribuer à la beauté du monde, vous exprimer en partageant vos sentiments ou vos idées, faire de l’argent, trouver sa place dans un milieu, être validé, admiré, devenir célèbre… avec la démocratisation des logiciels de création musicale, l’offre semble devenir infinie, par contre la quantité de temps disponible pour écouter que ce soit au niveau individuel comme collectif est beaucoup plus limité.

Même si on aime le confort des mélodies qui vont bien ensemble, les auditeurs cherchent quelque chose de plus, quelque chose qui leur parle personnellement, et en miroir, quelque chose qui leur permettra de rejoindre une grande famille de gens comme eux. Être unique c’est polarisant, ça passe ou ça casse et en général ça casse. On a du mal à gérer ce qui est trop éloigné de nous. Par contre quand ça passe, c’est un coup de cœur, ça crée un lien potentiellement durable. Les morceaux génériques peuvent marcher auprès du public mais seulement si on y met les grands moyens : artistes célèbres pour les interpréter, vidéoclip onéreux, campagne de pub moderne (placement de produit, création artificielle de tendance avec des bots, vidéos virales, …) N’ayant pas ces moyens on va travailler pour avoir des productions les plus uniques possible et les plus personnelles. Et même si ça semble contreproductif, de s’investir dans un projet qui parlera à 0,1% des gens, le monde est grand et donc 0,1% de 8,3 milliards ça fait quand même 8,3 millions de fans potentiels !

Activité suivie (sur 3,5 +2,5 séances de 60 minutes)

Objectif : à moins que vous vouliez produire les morceaux en votre nom propre le but est en plusieurs parties :

  • de créer un artiste virtuel unique (chanteur/rappeur, poète, songwriter), et générer deux titres aussi spécifiques et uniques que possibles.
  • Présenter à l’oral votre production pendant une dizaine de minutes
    • La personna de votre artiste virtuel (qui est il ? quelles sont ses valeurs ? son style de vie, ses marques, son histoire,
    • Au moins deux morceaux, en quoi sont-ils uniques ? Parler de votre connexion personnelle avec ceux-ci.
    • Parler brièvement de votre technique pour créer vos morceaux.
  • Vous passerez en fin de séquence faire une présentation orale s’appuyant sur un PDF ou un powerpoint contenant la préparation de la présentation : image, texte, liens vers les chansons. Deux notes : une pour la préparation, y compris la création de vos morceaux, et une pour la prestation orale.

Pour cette activité vous aurez besoin de créer un compte ChatGPT (généralement vous en avez déjà un, mais on ne sait jamais) et un compte SUNO (qui vous donnera 5 générations de morceaux par jour). Il vous faudra connaitre vos mots de passe afin de pouvoir utiliser ces deux applications sur n’importe quel ordinateur et sur vos téléphones portables. Vous aurez aussi besoin d’une clé USB (avec 500Mo de libre) que vous garderez dans votre trousse jusqu’à la fin de l’année.

Rôle de ChatGPT pour l’activité :

  • Découverte de l’art du prompt : plus vous êtes clairs dans vos questions plus les réponses seront intéressantes.
  • Si vous avez besoin d’explication, et d’idées sur n’importe quel sujet.
  • Si vous voulez mettre une idée sous forme de texte de chansons.
  • Il vous servira à générer des images pour illustrer votre morceau (pochette d’album, concept art pour un clip, description visuelle du monde évoqué par le morceau), comme votre artiste virtuel
  • Rétro engenieering de chansons : si vous avez un titre que vous aimez beaucoup et que vous voudriez que SUNO s’en inspire, vous pouvez lui demander de générer un prompt lié au morceau total ou uniquement à la partie de celui-ci que vous voulez émuler.

Séance 1 :

  • Présentation de l’activité
  • Création des comptes SUNO et ChatGPT
  • Pour chatGPT : expérimenter des prompts pour créer des images, du discours, des textes de chanson
  • Retro engenieering d’une chanson populaire
  • Génération de votre première chanson avec SUNO avec le texte généré avec ChatGPt et un prompt obtenu par retro engenieering.
  • Ecoute et évaluation de cette chanson, est ce que le texte sonne bien ? est ce qu’il doit être changé par endroit ? Musicalement qu’est ce qui vous convient ? Qu’est ce qui pose problème ? avec ChatGPT obtenez un prompt mieux adapté. Génération d’une nouvelle version. Et d’une nouvelle etc jusqu’à épuisement des crédits. En affinant votre production.
  • Attention : si vous avez une certaine latitude au niveau de la vulgarité de votre texte, ceux-ci ne doivent pas contenir de propos racistes, misogynes, homophobes, antisémites… pas plus que d’incitation à l’utilisation de drogues.  
  • Si vous avez encore du temps, vous pourrez générer une illustration d’album pour votre titre.
  • L’image générée, le texte et le titres musical seront à conserver et inclus dans l’annexe de votre document à rendre en fin de séquence.

Séance 2

  • On va générer une nouvelle chanson en suivant la méthode de la séance précédente, mais en rendant votre démarche plus personnelle (vous pourrez utiliser un texte écrit de A à Z chez vous ou en classe, si vous n’êtes pas à l’aise dans votre expression, vous pouvez vous concentrer sur les idées, sur l’ambiance, pui collaborer plus finement avec ChatGPT pour construire quelque chose de personnel), Avec un rendu qui sera aussi inclus dans le document powerpoint à rendre. Document dont vous commencerez la rédaction.
  • Vous commencerez à travailler sur la personna de votre artiste virtuel, en générant photo, biographie,…
  • (durant cette séance vous ferez au moins une des activités IA disponibles sur le site)

Séance 3

  • La même chose que la séance 2, en continuant d’affiner votre approche et de faire un travail de plus en plus personnel. On finalise le power point. On commence à s’entrainer à la présentation.
  • Si vous avez un artiste virtuel, il sera interessant de vous débrouiller pour que sa voix soit aussi constante que possible. Pour cela il faudra être attentif aux prompt et ne pas hésiter à utiliser la fonction persona de SUNO.

Séance 4

  • On commence par faire passer les volontaires. Discussion sur ce qui va et ne va pas dans :
    • La prestation orale
    • Les morceaux
    • La persona
  • Pour chaque prestation orale, sur leur cahier où une feuille à conserver précieusement, vous noterez le nom de la personne faisant la présentation des notes sur 100 des trois items cité audessus ainsi qu’un petit texte indiquant ce que vous avez préféré (ou ce qui vous a marqué positivement) et ce que vous avez le moins apprécié (Votre feuille de note est susceptible d’être ramassée).
  • Après le passage des volontaires, les élèves peuvent peaufiner leur projet, créer d’autres titres, améliorer leur powerpoint. D’ailleurs ce fichier sera à rendre en fin de séance sur pronote.

Séance 5 et 6

  • Les élèves qui ne sont pas encore passé le feront dans l’ordre alphabétique. Si l’élève refuse de passer, ou s’il a une absence non justifiée durant la séance durant laquelle il doit passer, il passera la suivante en perdant automatiquement un certain nombre de points (sans doute 5).
  • Après ça, vous remplirez un questionnaire en ligne et vous marquerez les notes mises à vos camarades.

C’est terminé … mais ça n’est que le début…

Pour les 6 élèves les plus motivés, vous vous partagerez les élèves de la classe de manière relativement équitable (mais personne ne doit être laissé derrière). Vous allez devenir patron d’un label virtuel en charge de la promotion de ces artistes. Vous obtiendrez d’eux leurs illustrations ainsi que leurs morceaux. Vous allez créer une chaine musicale sur Youtube, et vous posterez au moins un morceau des élèves que vous aurez choisi (vous pouvez créer une vidéo avec le logiciel gratuit ClipChamp ou avec des plateformes qui permettrons de visualiser les paroles : ). Il vous faudra créer un logo pour votre label, et un petit texte expliquant votre philosophie.

Une fois que c’est fait, à travers vos amis, vos relations, vos réseaux vous allez promouvoir votre label et vos artistes (ces derniers pourront aussi promouvoir leurs productions) Il y aura un petit concours de vues pour savoir qui a le mieux assuré la promotion de ses artistes.

Pour tout le monde : quelque mois plus tard vous allez créer une page WEB présentant votre artiste (ou vos artistes si vous êtes un patron de label) qui sera mise en ligne.

Pour ceux qui sont très content de ce qu’ils ont fait et on envie de poursuivre dans cette voie vous pouvez utiliser un service de mise en ligne comme DistroKid qui vous permettra d’être présent sur la majeure partie des plateformes de streaming. C’est fastidieux mais ça n’est pas bien cher.

Bonus :

Emerveillé par les possibilité de SUNO, en parallèles à la création de l’activité, j’ai passé trois mois à créer des morceaux. En dehors de « je t’ai dans la peau » et « 4 saisons » j’ai écris 100% des paroles et j’ai « prompté » les habillages sonores en m’inspirant de mes morceaux préférés.

Algorithmes de parcours de graphes 

Algorithme de parcours

Un parcours de graphe est un algorithme consistant à explorer tous les sommets d’un graphe de proche en proche à partir d’un sommet initial donné. Cela généralise les parcours des arbres binaires que vous avez déjà vus.

Ces parcours sont notamment utilisés pour rechercher un plus court chemin (routage sur Internet, applications de navigation dans un réseau de métros ou de trains) ou pour trouver la sortie d’un labyrinthe.

1. Parcours de graphes, en version intuitive⚓︎

Parcours en largeur

Dans un parcours en largeur, on progresse par « cercles concentriques » à partir du sommet de départ. On visite d’abord tous les sommets à une distance de 1 unité du point départ, puis tous les sommets à une distance de 2 unités, etc. (en considérant que la distance est le nombre d’arêtes).

Par exemple, avec le graphe suivant,

parcoursLargeur

le parcours en largeur au départ de A donne la liste suivante des sommets :

A ; B ; D ; C ; E ; F ; G ; H

(avec la convention que, quand il y a plusieurs sommets à la même distance, on les écrit dans l’ordre alphabétique).

Exercice 1

Effectuer le parcours en largeur du graphe représenté ci-dessous, en partant du sommet A.

Solution

Le résultat du parcours en largeur est : A ; B ; C ; E ; D ; G ; F.

Exercice 2

Effectuer le parcours en largeur du graphe représenté ci-dessous, en partant du sommet D.

Solution

Le résultat du parcours en largeur est : D ; F ; A ; B ; C ; K ; E ; H ; G.

Parcours en profondeur

Dans un parcours en profondeur, l’idée est de suivre un chemin le plus loin possible. Si on arrive au bout d’un chemin, on revient à la bifurcation précédente pour explorer un nouveau chemin.

Par exemple, avec le graphe suivant :

un parcours en profondeur au départ du sommet A donne :

A ; B ; E ; F ; C ; K ; H ; G ; D ; Q ; M.

Exercice

Le mécanisme d’un parcours en profondeur est encore plus explicite dans le cas des graphes orientés.

Effectuer le parcours en profondeur du graphe représenté ci-dessous, en partant du sommet A.

Solution

Le résultat du parcours en profondeur est : A ; E ; G ; F ; C ; B ; D.

2. Parcours de graphes, en version rigoureuse⚓︎

Algorithme de parcours

Les deux types de parcours (en largeur et en profondeur) reposent sur le même principe de base :

  • Le sommet de départ dep étant donné, on crée une structure V qui contiendra au départ uniquement ce sommet dep .
  • Tant que V n’est pas vide :
    • on choisit un sommet s existant dans V ;
    • on visite s, c’est-à-dire qu’on l’affiche ;
    • on ajoute à V tous les voisins de s pas encore découverts.

La structure V sert donc à mémoriser les sommets qui ont été découverts mais pas encore visités.

  • Si on choisit une file (FIFO), on visitera les sommets dans l’ordre d’arrivée, donc les plus proches du sommet précédent. On obtient donc un parcours en largeur (  Breadth First Search, ou BFS).
  • Si on choisit une pile (LIFO), on visitera d’abord les derniers sommets arrivés, donc on parcourt le graphe en visitant à chaque étape un voisin du précédent. On obtient donc un parcours en profondeur (  Depth First Search, ou DFS).

2.1. Le parcours en largeur⚓︎

Exemple de parcours en largeur

Voici une illustration du parcours en largeur d’un graphe avec B comme sommet de départ :

Codes couleur :

  • vert : les sommets non encore traités.
  • rouge : le sommet en cours de traitement.
  • orange : la file d’attente des sommets qui seront bientôt traités. On y rajoute à chaque fois les voisins du sommet en cours de traitement, uniquement si ils n’ont pas encore été découverts.
  • noir : les sommets traités.

Le résultat du parcours en largeur du graphe est donc : B ; A ; D ; E ; C ; F ; G ; H

Remarque : quand on ajoute plusieurs voisins d’un seul coup dans la file, on fait ici le choix de les ajouter dans l’ordre alphabétique.

Un outil en ligne

Un outil en ligne, permet de visualiser le résultat du parcours en profondeur (DFS : depth first search) d’un graphe.

Un graphe est donné en exemple, mais vous pouvez le modifier ou construire le vôtre :

Dans les menus déroulants, bien choisir Algorithm : BFS et Example graph : directedGraph

2.2. Codage d’un parcours en largeur en Python⚓︎

Remarque : ce code est donné uniquement à titre illustratif : il n’est pas nécessaire de l’apprendre par cœur. Par ailleurs, dans une partie cachée, il y a le codage des primitives des files et des graphes.

def parcours_largeur(G,s):
    f = creer_file_vide()
    enfiler(f,s)
    tab_decouverts = [s]
    while not est_vide(f):
        s = defiler(f)
        print(s, end=" - ")
        for v in liste_voisins(G,s) :
            if v not in tab_decouverts :
                enfiler(f,v)
                tab_decouverts.append(v)
    return None

# modélisation du graphe par un dictionnaire
graphe = {"B" : ["A", "D", "E"], "A" : ["B", "C"], "C" : ["A", "D"], "D":["B", "C", "E"],
"E" : ["B", "F", "G"], "F" : ["E", "G"], "G" : ["E", "F", "H"], "H":["G"]}
# parcours du graphe
parcours_largeur(graphe, "B")

2.3. Le parcours en profondeur⚓︎

Exemple de parcours en profondeur

Rappel : dans un parcours en profondeur, chaque nouveau voisin découvert est ajouté au sommet d’une pile.

On prend comme exemple le même graphe que dans l’exemple précédent, avec un départ du sommet B. On convient que, quand il y a plusieurs sommets à empiler en même temps, on les empile dans l’ordre alphabétique.

  • B est visité et on empile les voisins de B : la pile contient alors A (bas de la pile), D, E (sommet de la pile).
  • E est dépilé (il est visité) et on empile ses voisins qui ne sont pas déjà dans la pile : la pile contient alors A (bas de la pile), D, F, G (sommet de la pile).
  • G est dépilé (il est visité) et on empile H, son seul voisin pas encore découvert : la pile contient alors A (bas de la pile), D, F, H (sommet de la pile).
  • H est dépilé et il n’y a pas de nouveau voisins à empiler : la pile contient alors A (bas de la pile), D, F (sommet de la pile).
  • et ainsi de suite

Au final, on obtient l’ordre de visite suivant : B ; E ; G ; H ; F ; D ; C ; A.

2.4. Codage d’un parcours en profondeur en Python⚓︎

Remarque : ce code est donné uniquement à titre illustratif : il n’est pas nécessaire de l’apprendre par cœur.

def parcours_profondeur(G,s):
    p = creer_pile_vide()
    empiler(p,s)
    tab_decouverts = [s]
    while not est_vide(p):
        s = depiler(p)
        print(s, end=" - ")
        for v in liste_voisins(G,s) :
            if v not in tab_decouverts :
                empiler(p,v)
                tab_decouverts.append(v)
    return None

# modélisation du graphe par un dictionnaire
graphe = {"B" : ["A", "D", "E"], "A" : ["B", "C"], "C" : ["A", "D"], "D":["B", "C", "E"],
"E" : ["B", "F", "G"], "F" : ["E", "G"], "G" : ["E", "F", "H"], "H":["G"]}
# parcours du graphe
parcours_profondeur(graphe, "B")

2.5. Applications⚓︎

Graphe connexe ou pas ?

On donne ci-dessous le code d’un graphe non orienté graphe1.

En comparant les résultats des appels des fonctions liste_sommets et parcours depuis le sommet A (soit en largeur, soit en profondeur), déterminer si ce graphe est connexe ou pas.

# modélisation d'un graphe par un dictionnaire
graphe1 = {"A": ["C", "H", "I"],"B": ["J", "L"],"C": ["A", "D", "G", "I"],"D": ["C", "E"],
    "E": ["D", "H"], "F": ["G", "H"], "G": ["C", "F"], "H": ["A", "E", "F"], "I": ["A", "C"],
    "J": ["B", "K", "L"], "K": ["J"], "L": ["B", "J"]}
# ce graphe non orienté est-il connexe ?

Solution : On observe que le parcours du graphe depuis le sommet A fournit moins de sommets que la liste des sommets. Ce graphe n’est donc pas connexe.

Sommet atteignable ou pas ?

On donne ci-dessous le code d’un graphe connexe orienté graphe2.

En effectuant un parcours depuis le sommet A (soit en largeur, soit en profondeur), déterminer s’il est possible d’atteindre le sommet B depuis ce sommet A.

Ce même sommet B est-il atteignable depuis le sommet N ?

# modélisation d'un graphe par un dictionnaire
graphe2 = {
    "A": ["G", "K", "L"],  "B": ["L", "N"], "C": ["A", "D", "I"], "D": ["C"], "E": ["D", "F", "G"],
    "F": ["H"], "G": ["D"], "H": ["E", "G"], "I": ["A", "G", "H"], "J": ["B", "M", "N"],
    "K": ["I"], "L": [], "M": ["B"], "N": ["M"]
}
# dans ce graphe orienté, est-il possible d'aller de A vers B ? et de N vers B ?

Solution

On observe que le sommet B n’apparait pas dans le parcours du graphe depuis le sommet A : B n’est donc pas atteignable depuis A.

Mais B est atteignable depuis le sommet N, comme le montre un parcours depuis N.

3. TP: graphe et labyrinthe⚓︎

Télécharger le carnet Jupyter, à ouvrir sur Basthon.

Il faudra aussi charger dans Basthon les trois modules Python suivants (Ouvrir… puis Installer le module) : laby2d.pylaby3d.py et solution.py.

4. Algorithme de recherche d’un plus court chemin : algorithme de Dijkstra⚓︎

Vous avez déjà effectué la recherche d’un plus court chemin dans un graphe pondéré, quand il s’agissait de trouver le chemin emprunté pour aller d’un routeur A à un routeur B selon le protocole OSPF.

On peut systématiser la détermination du plus court chemin d’un point à un autre en appliquant l’algorithme de Dijkstra (mis au point par Edsger Dijkstra en 1959), comme expliqué dans la vidéo suivante :

Application de l’algorithme de Dijkstra

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

Solution

EABCDFChoix
0E(0)
.30 vE40 vE10 vED(10)
.20 vD40 vE.80 vDA(20)
..60 vA30 vA.80 vDC(30)
..50 vC..80 vDB(50)
.....70 vBF(70)

Le meilleur trajet est donc E-D-A-C-B-F, pour une distance totale de 70.
Attention ce trajet correspond à la colonne choix (dans l’ordre) mais ce n’est pas toujours le cas.

Deuxième application de l’algorithme de Dijkstra

Déterminer le plus court chemin entre A et G dans le graphe pondéré ci-dessous (en version papier).

grapheDijkstra

Solution

ABCDEFGHChoix
0A(0)
.10 vA8 vA5 vAD(5)
.10 vA8 vA.20 vDC(8)
.9 vC..15 vC20 vD12 vCB(9)
....19 vB15 vD20 vD12 vCH(12)
....17 vH13 vH17 vH.F(13)
....17 vH.16 vF.G(16)

Le meilleur trajet est donc A – C – H – F – G, pour une distance totale de 16.

L’algorithme de Dijkstra, ici exécuté de manière manuelle, est bien sûr programmable. Mais cela dépasse le niveau attendu en fin de Terminale.

5. Exercices⚓︎

programmes ressources

Graphe représenté par une matrice d’adjacence

class Graphe:
    """un graphe représenté par une matrice d’adjacence,
    où les sommets sont les entiers 0,1,...,n-1"""

    def __init__(self, n):
        self.n = n
        self.adj = [[False] * n for _ in range(n)]

    def ajouter_arc(self, s1, s2):
        self.adj[s1][s2] = True
        def arc(self, s1, s2):
        return self.adj[s1][s2]

    def voisins(self, s):
        v = []
        for i in range(self.n):
            if self.adj[s][i]:
                v.append(i)
        return v

Graphe représenté par un dictionnaire d’adjacence

class Graphe:
    """un graphe comme un dictionnaire d’adjacence"""

    def __init__(self):
        self.adj = {}

    def ajouter_sommet(self, s):
        if s not in self.adj:
            self.adj[s] = set()

    def ajouter_arc(self, s1, s2):
        self.ajouter_sommet(s1)
        self.ajouter_sommet(s2)
        self.adj[s1].add(s2)

    def arc(self, s1, s2):
        return s2 in self.adj[s1]

    def sommets(self):
        return list (self.adj)

    def voisins(self, s):
        return self.adj[s]

Exercice 1 Dessiner tous les graphes non orientés ayant exactement trois sommets.  

Solution : 8 au total :

Exercice 2 Combien y a-t-il de graphes orientés ayant exactement trois sommets ? On ne demande pas de les dessiner tous, mais seulement de les dénombrer.  

Solution : Il y a six arcs possibles (0 → 1, 1 → 0, 1 → 2, 2 → 1, 2 → 0, 0 → 2), chacun pouvant être présent ou absent, soit  possibilités au total.

Exercice 3 Ajouter à la classe Graphe du programme matrice d’adjacence donné avant les consignes des exercices, une méthode afficher pour afficher le graphe sous la forme suivante

0 -> 1 3

1 -> 2 3

2 -> 3

3 -> 1

c’est-à-dire une ligne par sommet, avec pour chacun la liste de ses voisins.

def afficher(self):
    for s in range(self.n):
        print(s, "->", end="")
    for v in range(self.n):
        if self.adj[s][v]:
            print("", v, end="")
    print()

Exercice 04 Ajouter à la classe Graphe du programme dictionnaire d’adjacence donné avant les consignes des exercices une méthode afficher pour afficher le graphe sous la forme suivante

0 {1, 3}

1 {2, 3}

3 {1}

2 {3}

c’est-à-dire une ligne par sommet, avec pour chacun l’ensemble de ses voisins.

L’ordre des sommets n’est pas important. L’ensemble des voisins peut être affiché directement avec print.  

Correction : On fait un cas particulier pour l’ensemble vide, histoire qu’il ne soit pas affiché comme set() mais comme {}.

def afficher(self):
    for s in self.adj:
        if len(self.adj[s]) == 0:
            print(s, "{}")
        else:
            print(s, self.adj[s])

Exercice 05 Ajouter à la classe Graphe du programme dictionnaire d’adjacence donné avant les consignes des exercices une méthode nb_sommets() qui donne le nombre de sommets du graphe.

Correction : Le nombre de sommets est égal au nombre d’entrées dans le dictionnaire :

def nb_sommets(self):
    return len(self.adj)

Exercice 06 Ajouter à la classe Graphe du programme matrice d’adjacence donné avant les consignes des exercices une méthode degre(s) qui donne le nombre d’arcs issus du sommet s. On appelle cela le degré du sommet s.  

Correction : Le code est très semblable à celui de la méthode voisins :

def degre(self, s):
    d = 0
    for i in range(self.n):
        if self.adj[s][i]:
            d += 1
    return d

Exercice 07 En utilisant l’exercice précédent, ajouter à la classe Graphe du programme matrice d’adjacence donné avant les consignes des exercices une méthode nb_arcs() qui donne le nombre total d’arcs du graphe.  

Correction : Il suffit de faire la somme de tous les degrés.

def nb_arcs(self):
    n = 0
    for s in range(self.n):
        n += self.degre(s)
    return n

Exercice 08 Ajouter à la classe Graphe du programme dictionnaire d’adjacence donné avant les consignes des exercices une méthode degre(s) qui donne le nombre d’arcs issus du sommet s. On appelle cela le degré du sommet s.  

Correction : C’est exactement le cardinal de l’ensemble adj[s] :

def degre(self, s):
    return len(self.adj[s])

Exercice 09 En utilisant l’exercice précédent, ajouter à la classe Graphe du programme dictionnaire d’adjacence donné avant les consignes des exercices une méthode nb_arcs() qui donne le nombre total d’arcs du graphe.  

Correction : comme il y a deux exercice, on fait la somme de tous les degrés.

def nb_arcs(self):
    n = 0
    for s in self.adj:
        n += self.degre(s)
    return n

Exercice 10 Ajouter aux classes Graphe des programmes dictionnaire et matrice d’adjacence une méthode supprimer_arc(s1, s2) pour supprimer l’arc entre les sommets s1 et s2. S’il n’y a pas d’arc entre ces sommets, la méthode n’a aucun effet. On supprime un élément d’un ensemble avec sa méthode remove.

Correction : Pour la matrice d’adjacence, on affecte le booléen False dans la matrice :


def supprimer_arc(self, s1, s2):
    self.adj[s1][s2] = False

def supprimer_arc(self, s1, s2):
    if self.arc(s1, s2):
        self.adj[s1].remove(s2)

Pour le dictionnaire d’adjacence, on supprime s2 de l’ensemble des voisins de s1, si l’arc existe :

def supprimer_arc(self, s1, s2):
    if self.arc(s1, s2):
        self.adj[s1].remove(s2)

Exercice 11 Donner une coloration du graphe des régions ci-dessus qui n’utilise que quatre couleurs.  

Correction : Il suffit de reprendre les couleurs obtenues avec l’algorithme glouton (figure 11.2 page 204) et de faire seulement deux changements :

Bretagne 1 (au lieu de 0)

Normandie 0 (au lieu de 4)

Exercice 12 : coloriage de graphes

écrivons un programme qui colorie un graphe avec un algorithme glouton. Colorier un graphe veut dire associer une couleur à chacun de ses sommets, de façon à ce que deux sommets reliés par un arc n’aient pas la même couleur. Colorier un graphe avec un minimum de couleurs est un problème difficile, mais si on accepte l’idée d’utiliser éventuellement plus de couleurs que nécessaire, alors on peut colorier un graphe efficacement. Le principe d’un coloriage glouton est simple. On parcourt les sommets dans un ordre arbitraire et, pour chaque sommet, on lui attribue la première couleur qui n’est pas utilisée par ses voisins.

def mex(voisins, couleur):
    """la plus petite couleur non utilisée par les voisins"""
    n = len(voisins)
    dispo = [True] * (n + 1)
    for v in voisins:
        if v in couleur and couleur[v] &lt;= n:
            dispo[couleur[v]] = False
    for c in range(n + 1):
        if dispo[c]:
            return c
        assert False # on n’arrivera jamais ici

def coloriage(g):
    """colorie graphe g avec un algorithme glouton"""
    couleur = {}
    n = 0
    for s in g.sommets():
        c = mex(g.voisins(s), couleur)
        couleur[s] = c
        n = max(n, c + 1)
    return couleur, n

programmes de référence :

Programme 39 — Parcours en profondeur

def parcours(g, vus, s):
    """parcours en profondeur depuis le sommet s"""
    if s not in vus:
        vus.add(s)
    for v in g.voisins(s):
        parcours(g, vus, v)

def existe_chemin(g, u, v):
    """existe-t-il un chemin de u à v ?"""
    vus = set()
    parcours(g, vus, u)

def parcours(g, vus, s):
    """parcours en profondeur depuis le sommet s"""
    if s not in vus:
        vus.add(s)
    for v in g.voisins(s):
        parcours(g, vus, v)

def existe_chemin(g, u, v):
    """existe-t-il un chemin de u à v ?"""
    vus = set()
    parcours(g, vus, u)
    return v in vus

Programme 40 — Parcours en profondeur avec une pile

def parcours(g, vus, s):
    """parcours en profondeur depuis le sommet s"""
    pile = Pile()
    pile.empiler(s)
    while not pile.est_vide():
    s = pile.depiler()
    if s in vus:
        continue
    vus.add(s)

for v in g.voisins(s):
    pile.empiler(v)

Programme 41 — Détecter la présence d’un cycle dans un graphe

BLANC, GRIS, NOIR = 1, 2, 3

def parcours_cy(g, couleur, s):
    """parcours en profondeur depuis le sommet s"""
    if couleur[s] == GRIS:
        return True
    if couleur[s] == NOIR:
        return False
    couleur[s] = GRIS
    for v in g.voisins(s):
        if parcours_cy(g, couleur, v):
            return True
    couleur[s] = NOIR
    return False

def cycle(g):
    couleur = {}
    for s in g.sommets():
        couleur[s] = BLANC
    for s in g.sommets():
        if parcours_cy(g, couleur, s):
            return True
    return False

Programme 42 — Parcours en largeur

def parcours_largeur(g, source):
    """parcours en largeur depuis le sommet source"""
    dist = {source: 0}
    courant = {source}
    suivant = set()
    while len(courant) > 0:
        s = courant.pop()
    for v in g.voisins(s):
        if v not in dist:
            suivant.add(v)
    dist[v] = dist[s] + 1
    if len(courant) == 0:
        courant, suivant = suivant, set()
    return dist

def distance(g, u, v):
    """distance de u à v (et None si pas de chemin)"""
    dist = parcours_largeur(g, u)
    return dist[v] if v in dist else None

Exercice 104 Dérouler à la main le parcours en profondeur sur le graphe suivant

pour différentes valeurs du sommet de départ. Donner à chaque fois la valeur finale de l’ensemble vus, c’est-à-dire l’ensemble des sommets atteints par le parcours.

Correction :

Exercice 105 Dérouler à la main le parcours en largeur sur le graphe suivant

pour différentes valeurs du sommet de départ. Donner à chaque fois la valeur finale du dictionnaire dist, c’est-à-dire la distance à la source de chaque sommet atteint par le parcours.

Correction :

Exercice 106 On peut se servir d’un parcours en profondeur pour déterminer si un graphe non orienté est connexe, c’est-à-dire si tous ses sommets sont reliés entre eux par des chemins. Pour cela, il suffit de faire un parcours en profondeur à partir d’un sommet quelconque, puis de vérifier que tous les sommets ont été atteints par ce parcours. Écrire une fonction est_connexe() qui réalise cet algorithme. On pourra se servir de la méthode sommets() de la classe Graphe et de la fonction parcours du programme Parcours en profondeur.

Correction : On suit l’algorithme proposé, en faisant uniquement attention au cas pathologique d’un graphe qui ne contiendrait aucun sommet.

def est_connexe(g):
    """le graphe est-il connexe ?
    (uniquement pour un graphe non orienté)"""
    ts = g.sommets()
    if len(ts) == 0:
        return True
    s = ts.pop()
    vus = set()
    parcours(g, vus, s)
    for s in ts:
        if s not in vus:
            return False
    return True

Exercice 107 Dans cet exercice, on se propose d’utiliser le parcours en profondeur pour construire un chemin entre deux sommets, lorsque c’est possible. On le fait avec deux fonctions, comme dans le programme Parcours en profondeur.

def parcours_ch(g, vus, org, s):

«  » »parcours depuis le sommet s, en venant de org » » »

def chemin(g, u, v):

«  » »un chemin de u à v, le cas échéant, None sinon » » »

L’idée est que l’attribut vus n’est plus un ensemble mais un dictionnaire, qui associe à chaque sommet visité le sommet qui a permis de l’atteindre pendant le parcours en profondeur. La fonction parcours_ch prend un argument supplémentaire, org (pour origine), qui est justement le sommet qui a permis d’atteindre s, en empruntant l’arc org s. Écrire le code de la fonction parcours_ch ; il est très semblable à celui de la fonction parcours du programme 39. Écrire ensuite le code de la fonction chemin qui renvoie un chemin entre les sommets u et v, le cas échéant, et None s’il n’existe pas de chemin entre ces deux sommets. Pour cela, lancer un parcours en profondeur à partir du sommet u, en donnant à org la valeur None, puis, si le sommet v a été atteint, construire le chemin dans un tableau en« remontant » le dictionnaire vus de v jusqu’à u.

Correction : Pour parcours_ch, l’ajout dans un ensemble devient

un ajout dans un dictionnaire.

def parcours_ch(g, vus, org, s):
    """parcours depuis le sommet s, en venant de org"""
    if s not in vus:
        vus[s] = org
        for v in g.voisins(s):
            parcours_ch(g, vus, s, v)

Pour chemin, on prend soin de tester si v a été atteint par le parcours. Dans le cas contraire, on renvoie None. Sinon, on construit le chemin avec une boucle while.

def chemin(g, u, v):
    """un chemin de u à v, le cas échéant, None sinon"""
    vus = {}
    parcours_ch(g, vus, None, u)
    if v not in vus:
        return None
    ch = []
    s = v
    while s is not None:
        ch.append(s)
        s = vus[s]
    ch.reverse()
    return ch

De fait, le chemin est construit à l’envers. On prend soin de le renverser avec la méthode reverse avant de le renvoyer.

Exercice 108 Dans cet exercice, on se propose d’utiliser le parcours en largeur pour construire un chemin de longueur minimale entre deux sommets. On le fait avec deux fonctions, comme dans le programme Parcours en largeur.

def parcours_largeur_ch(g, source):

«  » »parcours depuis le sommet source » » »

def chemin(g, u, v):

«  » »un chemin de u à v, le cas échéant, None sinon » » »

L’idée est qu’un dictionnaire vus remplace le dictionnaire dist. Ce dictionnaire vus associe à chaque sommet visité le sommet qui a permis de l’atteindre pendant le parcours en largeur. Écrire le code de la fonction parcours_largeur_ch ; il est très semblable à celui de la fonction parcours_largeur du programme 42. Pour le sommet source, on lui associe la valeur None dans le dictionnaire vus. Écrire ensuite le code de la fonction chemin qui renvoie un chemin réalisant la distance entre les sommets u et v, le cas échéant, et None s’il n’existe pas de chemin entre ces deux sommets. Pour cela, lancer un parcours en largeur à partir du sommet u puis, si le sommet v a été atteint, construire le chemin dans un tableau en« remontant » le dictionnaire vus de v jusqu’à u.

Correction : On garde la structure du programme 42, le dictionnaire vus prenant la place du dictionnaire dist.

def parcours_largeur_ch(g, source):
    """parcours depuis le sommet source"""
    vus = {source: None}
    courant = {source}
    suivant = set()
    while len(courant) > 0:
        s = courant.pop()
        for v in g.voisins(s):
            if v not in vus:
                suivant.add(v)
                vus[v] = s
        if len(courant) == 0:
                courant, suivant = suivant, set()
    return vus


La seconde partie, à savoir la reconstruction du chemin, n’est pas différente de celle effectuée dans l’exercice précédent pour un parcours en profondeur.

def chemin(g, u, v):
    """un chemin de u à v, le cas échéant, None sinon"""
    vus = parcours_largeur_ch(g, u)
    if v not in vus:
        return None
    ch = []
    s = v
    while s is not None:
        ch.append(s)
        s = vus[s]
    ch.reverse()
    return ch

Exercice 109 Les nœuds d’un arbre binaire peuvent être vus comme les sommets d’un graphe non orienté. En particulier, on peut donc parcourir largeur les nœuds d’un arbre avec un parcours en profondeur ou en largeur. Pour le parcours en profondeur, il s’agit d’un parcours que nous avons déjà présenté, à savoir le parcours préfixe . Dans cet exercice, on se propose de parcourir les nœuds d’un arbre binaire avec un parcours en largeur.

Écrire une fonction largeur(a) qui reçoit un arbre binaire a en argument et imprime les valeurs de ses différents nœuds dans un ordre donné par un parcours en largeur, c’est-à-dire la valeur contenue dans la racine, puis les valeurs contenues dans les nœuds immédiatement en-dessous (profondeur 1), puis celles contenues dans les nœuds encore en-dessous (profondeur 2), etc.

Adapter pour cela le code du programme Parcours en largeur.

Correction

La subtilité consiste ici à correctement traiter les arbres vides. On a le choix de le faire avant ou après l’insertion dans les ensembles.

On choisit ici de le faire après, ce qui évite de faire un cas particulier pour un arbre qui serait intégralement vide.

def largeur(a):
    courant = []
    suivant = []
    courant.append(a)
    while len(courant) > 0:
        n = courant.pop()
        if n is None:
            continue
        print(n.valeur)
        suivant.append(n.gauche)
        suivant.append(n.droit)
        if len(courant) == 0:
            courant, suivant = suivant, []