from dataclasses import dataclass
from typing import Any
@dataclass
class Noeud:
"""Noeud d'un arbre binaire."""
valeur: Any = None
gauche: Any = None
droite: Any = None
ArbreBinaire
, qui référence le noeud de départ.ArbreBinaire
n'a pas de valeur.ArbreBinaire
from dataclasses import dataclass
@dataclass
class ArbreBinaire:
"""Arbre binaire."""
noeud: Noeud = None
def trouve_valeur_dans_arbre_binaire(arbre, valeur):
"""Trouve une valeur dans l'arbre binaire."""
noeud = arbre.noeud
while noeud != None and noeud.valeur != valeur:
if valeur < noeud.valeur:
noeud = noeud.gauche
else:
noeud = noeud.droite
return noeud
def insere_noeud_dans_arbre_binaire(arbre, valeur):
"""Insère un nouveau noeud dans un arbre binaire."""
if arbre.noeud == None:
arbre.noeud = Noeud(valeur=valeur)
return arbre.noeud
noeud = arbre.noeud
while noeud.valeur != valeur:
if valeur < noeud.valeur:
if noeud.gauche == None:
noeud.gauche = Noeud(valeur=valeur)
noeud = noeud.gauche
else:
if noeud.droite == None:
noeud.droite = Noeud(valeur=valeur)
noeud = noeud.droite
return noeud
Dans un arbre rouge-noir, la recherche et l'insertion sont en
list
.from dataclasses import dataclass, field
from typing import Any, List
@dataclass
class Noeud:
"""Noeud d'un arbre n-aire."""
valeur: Any = None
descendants: List = field(default_factory=list)
Graphe orienté |
Graphe orienté |
Graphe orienté |
Graphe simple |
Chemin |
Chemin non simple | Chemin simple |
Circuit | Cycle dans le graphe sous-jacent |
Circuit non élémentaire | Circuit élémentaire |
DAG |
Graphe simple | Fermeture transitive |
Graphe non connexe | Graphe connexe |
Graphe non fortement connexe | Graphe fortement connexe |
G = [
[1], # 0 -> 1
[3, 4], # 1 -> 3, 1 -> 4
[4], # 2 -> 4
[0, 2], # 3 -> 0, 3 -> 2
[] # aucun
]
sommets | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
sommet 0 : | 0 | 1 | 0 | 0 | 0 |
sommet 1 : | 0 | 0 | 0 | 1 | 1 |
sommet 2 : | 0 | 0 | 0 | 0 | 1 |
sommet 3 : | 1 | 0 | 1 | 0 | 0 |
sommet 4 : | 0 | 0 | 0 | 0 | 0 |
M = [
[0, 1, 0, 0, 0],
[0, 0, 0, 1, 1],
[0, 0, 0, 0, 1],
[1, 0, 1, 0, 0],
[0, 0, 0, 0, 0]
]
M = [
[None, "a", None, None, None],
[None, None, None, "b", "c"],
[None, None, None, None, "e"],
[ "f", None, "g", None, None],
[None, None, None, None, None]
]
arêtes | a | b | c | d | e | f |
---|---|---|---|---|---|---|
sommet 0 : | 1 | 0 | 0 | 0 | 1 | 0 |
sommet 1 : | 1 | 1 | 1 | 0 | 0 | 0 |
sommet 2 : | 0 | 0 | 0 | 1 | 0 | 1 |
sommet 3 : | 0 | 1 | 0 | 0 | 1 | 1 |
sommet 4 : | 0 | 0 | 1 | 1 | 0 | 0 |
M = [
[1, 0, 0, 0, 1, 0],
[1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 1],
[0, 1, 0, 0, 1, 1],
[0, 0, 1, 1, 0, 0]
]
Ordre de visite : 0, 1, 4, 10, 11, 5, 6, 2, 7, 12, 13, 14, 3, 8, 9.
Ordre de visite : 0, 1, 3, 2, 4.
Ordre de visite : 0, 3, 2, 7, 12, 11, 6, 13, 14, 9, 10, 8, 4, 1, 5, 11, 15, 16, 17, 18, 19.
def parcours_en_profondeur(m, f):
"""Applique la fonction f à chaque sommet du graphe.
m - matrice d'adjacence.
f - fonction prenant un sommet en argument.
"""
def parcours_successeurs(m, s, f, marque):
"""Traitement récursif."""
if s not in marque:
marque.append(s)
f(s)
suivants = successeurs(m, s)
for suivant in suivants:
parcours_successeurs(m, suivant, f, marque)
marque = []
for s in range(len(m)):
parcours_successeurs(m, s, f, marque)
Ordre de visite : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14.
Ordre de visite : 0, 1, 3, 4, 2.
Ordre de visite : 0, 3, 4, 2, 8, 9, 1, 5, 11, 7, 6, 12, 13, 14, 10, 15, 16, 18, 17, 19.
def parcours_en_largeur(m, f):
"""Applique la fonction f à chaque sommet du graphe.
m - matrice d'adjacence.
f - fonction prenant un sommet en argument.
"""
marque = [] # On ne souhaite pas traiter plusieurs fois un sommet
queue = [] # On utilise une queue pour traiter d'abord les plus proches
for s in range(len(m)): # Visite chaque sommet pour les graphes non-connexes
if s not in marque: # Evite de traiter 2 fois un sommet
marque.append(s) # Marque le sommet courant à traiter
queue.append(s) # Empile dans la queue des sommets à traiter
while len(queue) != 0: # Tant que la queue est non vide
s_i = queue.pop(0) # On prend le 1er sommet
f(s_i) # On traite s_i
suivants = successeurs(m, s_i) # On prend les successeurs
for suivant in suivants: # On parcourt les successeurs
if suivant not in marque: # Les successeurs non marqués
marque.append(suivant) # sont marqués
queue.append(suivant) # et empilés.
def cherche_cycle_en_profondeur(m, s, marque, chemin):
"""Recherche en profondeur un cycle.
m - matrice d'adjacence.
s - sommet à visiter.
marque - liste de sommets marqués.
chemin - chemin jusqu'à s.
"""
cs = chemin + [s] # On construit le nouveau chemin cs
if s in chemin: # Si un cycle est identifié dans le chemin,
return cs # on le renvoie.
if s not in marque: # Parcours en profondeur
marque.append(s) # On marque le sommet s
suivants = successeurs(m, s) # On prend les successeurs de s
for suivant in suivants: # On vérifie les sous-chemins
cycle = cherche_cycle_en_profondeur(m, suivant, marque, cs)
if cycle != None: # Si un cycle a été identifié dans
return cycle # un sous-chemin, on le renvoie.
return None # Pas de cycle identifié
def identifie_cycle(m):
"""Retourne le premier cycle identifié ou None."""
marque = []
for s in range(len(m)):
cycle = identifie_cycle_dans_chemin(m, s, marque, [])
if cycle != None:
return cycle
return None
cycle = identifie_cycle(G)
print(cycle)
[0, 1, 3, 0]
G = [
[ # 0 -> 1 (poids = 42)
[1, 42]
],
[ # 1 -> 3 (poids = 21), 1 -> 4 (poids = -56.7)
[3, 21],
[4, -56.7]
],
[ # 2 -> 4 (poids = 7)
[4, 7]
],
[ # 3 -> 0 (poids = 3.14), 3 -> 2 (poids = -8)
[0, 3.14],
[2, -8]
],
[] # aucun
]
sommets | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
sommet 0 : | 42 | ||||
sommet 1 : | 21 | -56.7 | |||
sommet 2 : | 7 | ||||
sommet 3 : | 3.14 | -8 | |||
sommet 4 : |
M = [
[None, 42, None, None, None],
[None, None, None, 21, -56.7],
[None, None, None, None, 7 ],
[3.14, None, -8, None, None],
[None, None, None, None, None]
]
@dataclass
class Sommet:
"""Sommet d'un graphe."""
label: int = 0
@dataclass
class Arc:
"""Arc d'un graphe orienté"""
origine: int = 0
but: int = 0
poids: float = 0.
@dataclass
class GraphePondere:
"""Graphe orienté pondéré."""
sommets: List = field(default=list)
arcs: List = field(default=list)
s0 = Sommet(0)
s1 = Sommet(1)
s2 = Sommet(2)
s3 = Sommet(3)
s4 = Sommet(4)
a0 = Arc(origine=0, but=1, poids=42)
a1 = Arc(origine=1, but=3, poids=21)
a2 = Arc(origine=1, but=4, poids=-56.7)
a3 = Arc(origine=2, but=4, poids=7)
a4 = Arc(origine=3, but=2, poids=-8)
a5 = Arc(origine=3, but=0, poids=3.14)
G = GraphePondere(sommets=[s0, s1, s2, s3, s4],
arcs=[a0, a1, a2, a3, a4, a5])
def adjacents(m, s):
"""Renvoie les arcs adjacents à s dans m."""
adj = []
for j in range(len(m[s])):
if m[s][j] != None:
adj.append(Arc(origine=s, but=j, poids=m[s][j]))
return adj
def recalcule_bellman_ford(m, s, dist_a, arc_vers, queue):
"""Recalcule la distance minimale en considérant les successeurs de s.
m - matrice d'adjacence pondérée.
s - sommet dans les successeurs sont considérés.
dist_a - liste des distances minimales aux autres sommets.
arc_vers - liste des arcs conservés pour aller à un sommet donné.
queue - queue pour le parcours en largeur.
"""
adj = adjacents(m, s)
for arc in adj:
w = arc.but
if dist_a[w] == None or dist_a[w] > dist_a[s] + arc.poids:
dist_a[w] = dist_a[s] + arc.poids
arc_vers[w] = arc
if w not in queue:
queue.append(w)
def bellman_ford_impl(m, s):
"""Implémentation de Bellman-Ford sans gestion de cycles négatifs.
m - matrice d'adjacence pondérée.
s - sommet de départ.
Renvoie la liste des distances aux autres sommets et la liste des
arcs constituant les plus courts chemins.
"""
dist_a = [None for _ in range(len(m))] # distances à l'infini
dist_a[s] = 0 # distance à lui-même
arc_vers = [None for _ in range(len(m))] # résultat
queue = [s]
while len(queue) != 0:
v = queue.pop(0)
recalcule_bellman_ford(m, v, dist_a, arc_vers, queue)
return dist_a, arc_vers
def bellman_ford(m, s):
"""Renvoie une matrice d'adjacence correspondant au shortest path
tree (SPT) et la liste des distances minimales."""
dist_a, arc_vers = bellman_ford_impl(m, s)
spt = [[None for _ in range(len(m))] for _ in range(len(m))]
for arc in arc_vers:
if arc != None:
spt[arc.origine][arc.but] = arc.poids
return spt, dist_a
Note : Nous omettons la recherche de circuit négatif pour simplifier l'implémentation.
Graphe pondéré | Arbre de distances minimales pour |
Coûts = [0, 42, 55, 63, -14.7]
Graphe pondéré | Arbre de distances minimales pour |
Coûts = [0, 4, 9, 4, 2, 6, 8, 12, 13, 12, 3, 5, 14, 13, 14]
Tâches | Durée (jours) | Prédécesseurs |
---|---|---|
T1 : Architecturer Solution | 2 | |
T2 : Configurer Serveurs | 1 | |
T3 : Configurer BD | 3 | |
T4 : Plan de Tests | 2 | |
T5 : Coder Front End | 5 | T1 |
T6 : Coder Backend | 7 | T1, T2, T3 |
T7 : Tests | 3 | T4, T5, T6 |
NA = None # Non Applicable
M = [
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[NA, 0, 0, 0, NA, NA, NA, NA, NA, NA, NA, 0, NA, NA, NA, NA], # 0
[NA, NA, NA, NA, 2, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA], # 1
[NA, NA, NA, NA, NA, 1, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA], # 2
[NA, NA, NA, NA, NA, NA, 3, NA, NA, NA, NA, NA, NA, NA, NA, NA], # 3
[NA, NA, NA, NA, NA, NA, NA, 0, 0, NA, NA, NA, NA, NA, NA, NA], # 4
[NA, NA, NA, NA, NA, NA, NA, NA, 0, NA, NA, NA, NA, NA, NA, NA], # 5
[NA, NA, NA, NA, NA, NA, NA, NA, 0, NA, NA, NA, NA, NA, NA, NA], # 6
[NA, NA, NA, NA, NA, NA, NA, NA, NA, 5, NA, NA, NA, NA, NA, NA], # 7
[NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 7, NA, NA, NA, NA, NA], # 8
[NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 0, NA, NA], # 9
[NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 0, NA, NA], # 10
[NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 2, NA, NA, NA], # 11
[NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 0, NA, NA], # 12
[NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 3, NA], # 13
[NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 0], # 14
[NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA] # 15
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
]
début
à fin
.début
à fin
.2
devient donc un poids de -2
par exemple.def oppose_poids(m):
"""Renvoie une matrice d'adjacence dont les poids sont opposés."""
resultat = []
for i in range(len(m)):
ligne = []
for j in range(len(m)):
if m[i][j] == None:
ligne.append(None)
else:
ligne.append(-m[i][j])
resultat.append(ligne)
return resultat
def chemin_vers(s, dist_a, arc_vers):
"""Renvoie le chemin vers le sommet s."""
if dist_a[s] == None:
return None
chemin = []
arc = arc_vers[s]
while arc != None:
predecesseur = arc.origine
chemin.insert(0, predecesseur)
arc = arc_vers[predecesseur]
return chemin
def chemin_critique(m):
"""Renvoie le chemin critique en utilisant PERT.
Utilise Bellman-Ford sur l'opposé de la matrice d'adjacence.
La matrice d'adjacence doit représenter un DAG.
Par convention, le début est supposé être le 1er sommer, et
la fin est supoosée être le dernier sommet.
"""
# Construit une matrice d'adjacence avec les poids opposés
m_p = oppose_poids(m)
# Calcule l'arbre des plus courts chemins
dist_a, arc_vers = bellman_ford_impl(m_p, 0)
# Le chemin vers la fin est le chemin critique
fin = len(m) - 1
chemin = chemin_vers(fin, dist_a, arc_vers)
return chemin
c = chemin_critique(M)
print(c)
[0, 3, 6, 8, 10, 13, 14]
Cours très important pour l'examen final mais également fondamental car il s'agit d'une introduction au domaine passionant de la recherche opérationnelle. Soyez très attentifs et faites les TPs avec sérieux. L'examen final contient en général des questions sur les algorithmes de tri, de calcul matriciel et de graphes. Note d'implémentation : les graphes sont générés avec GraphViz via le script bin/misc/graph.py.
En pratique, on abrège par "arbre binaire", plutôt que "arbre de recherche binaire".
Quel est le chemin le plus rapide entre Saumur et Paris ?
Par quel chemin faire transiter des paquets entre des noeuds de calcul pour optimiser le flux global d'informations ?
Comment représenter les relations entre des utilisateurs ?
Il y a beaucoup de définitions dans cette partie, mais rien de complexe en réalité. Regardez bien les exemples qui illustrent chaque définition.
Vertices est le pluriel de vertex en anglais. Le terme digraph est très souvent employé, même en français.
Il s'agit du graphe sous-jacent à celui présenté précédemment.
3 composantes connexes de G (graphe non-orienté).
5 composantes fortement connexes du graphe orienté.
Dans la première partie de ce cours, nous nous sommes concentrés sur les arbres de recherche, et plus particulièrement sur les arbres de recherche binaires. Dans les arbres de recherche, un noeud doit avoir une valeur plus grande que les noeuds enfants à gauche et plus petite que les enfants à droite. La notion d'arbre en théorie des graphes est plus générale et décrit simplement la structure d'arbre. Cette notion n'intègre pas de relation d'ordre entre les parents et les enfants. Cette distinction est importante à comprendre.
Dans un vrai projet, les durées d'exécution peuvent être imprécises : données sous forme d'intervalle, de loi de probabilité, etc. De nouvelles contraintes peuvent éventuellement surgir en cours de projet.
Pour trouver tous les chemins critiques, il faudrait modifier notre implémentation de Bellman-Ford pour qu'il créé un graphe des plus courts chemins, plutôt qu'un arbre des plus courts chemins.
On prend l'opposé des poids qui ne sont pas None.
Cet algorithme est utilisable de manière générale également avec les résultats de Bellman-Ford. On obtient ainsi la liste des sommets par lesquels on passe pour atteindre le sommet de fin en passant par des tâches critiques.
On utilise tout simplement les fonctions précédentes. Il n'y a aucune difficulté ici.