Graphes sans circuit et ordonnancement de projets
Cours · algorithmique (école d'ingénieur), chapitre 7 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun
Objectifs du chapitre
- Définir et manipuler des listes d'entiers en Java.
- Représenter un graphe par un tableau de listes d'arcs, calculer son graphe symétrique en temps linéaire.
- Calculer une numérotation isotone (ordre topologique) d'un graphe sans circuit.
- Ordonnancer un projet : dates de début au plus tôt, durée minimum, chemin critique.
- Reconnaître un problème de chemin optimal dans un graphe sans circuit derrière des énoncés variés (réseau de bus, plus longue sous-séquence croissante).
Jusqu'ici, nos sous-problèmes étaient rangés dans des tableaux : l'objet venait après l'objet , la case après les cases . Dans ce chapitre, la structure des sous-problèmes est donnée par un graphe : la tâche d'un projet ne peut commencer qu'après certaines autres tâches, ses prédécesseurs. La programmation dynamique s'applique toujours, à condition de calculer les valeurs dans un ordre où les prédécesseurs passent avant leurs successeurs.
7.1 Listes d'entiers
Une liste d'entiers est :
- soit la liste vide, notée ;
- soit un couple où est un entier, le premier élément de la liste, et est une liste, le reste de la liste (la liste privée de son premier élément).
La définition est récursive : la liste que nous écrivons est, pour l'ordinateur, le couple . En Java, la liste vide est représentée par null et un couple par un objet de la classe Liste :
static class Liste { int p; Liste r; // premier et reste
Liste(int p, Liste r) { this.p = p; this.r = r; }
}
static boolean vide(Liste l) { return l == null; }
static int premier(Liste l) { return l.p; } // l non vide
static Liste reste(Liste l) { return l.r; } // l non vide
La liste se construit par new Liste(0, new Liste(2, new Liste(1, new Liste(2, null)))). Ajouter un élément en tête d'une liste est en temps constant : l = new Liste(x, l). Le parcours d'une liste s'écrit avec une boucle for :
for (Liste l = L; !vide(l); l = reste(l))
System.out.print(premier(l) + " ");
Les fonctions sur les listes s'écrivent naturellement de façon récursive, en suivant la définition : un cas pour la liste vide (base), un cas pour la liste non vide (hérédité).
static int longueur(Liste l) {
if (vide(l)) return 0; // la liste vide a 0 élément
return 1 + longueur(reste(l)); // le premier, plus ceux du reste
}
7.2 Graphes orientés
Un graphe orienté est formé d'un ensemble de sommets, ici , et d'un ensemble d'arcs (ou flèches) ; un arc relie le sommet au sommet . On note le nombre d'arcs et l'on appelle taille du graphe la valeur .
- est un successeur de , et un prédécesseur de , s'il existe un arc ; on note l'ensemble des prédécesseurs de .
- Le degré entrant d'un sommet est le nombre d'arcs qui arrivent sur lui ; une source est un sommet de degré entrant nul.
- Un circuit est un chemin qui revient à son point de départ. Un graphe sans circuit est souvent appelé DAG (directed acyclic graph).
7.2.1 Représentation par un tableau de listes d'arcs
On représente un graphe à sommets par un tableau de listes : est la liste des successeurs de , c'est-à-dire la liste des arcs qui sortent de . L'ordre des éléments d'une liste est quelconque.
Sa représentation : , , , , , , , .
static Liste[] graphe() { // G[i] = liste des successeurs de i
Liste[] G = new Liste[8];
G[0] = new Liste(1, new Liste(2, new Liste(3, null)));
G[1] = new Liste(3, new Liste(5, null));
G[2] = new Liste(4, new Liste(5, new Liste(6, new Liste(7, null))));
G[3] = new Liste(7, null);
G[4] = new Liste(6, new Liste(7, null));
G[5] = new Liste(6, new Liste(7, null));
G[6] = new Liste(7, null);
G[7] = null;
return G;
}
static void afficher(Liste[] G) {
for (int i = 0; i < G.length; i++) {
System.out.print(i + " :");
for (Liste l = G[i]; !vide(l); l = reste(l))
System.out.print(" " + premier(l));
System.out.println();
}
}
Cette représentation occupe une place proportionnelle à : une case de tableau par sommet, une cellule de liste par arc. Elle permet de parcourir les successeurs d'un sommet en un temps proportionnel à leur nombre.
7.2.2 Le graphe symétrique
Les récurrences de ce chapitre s'expriment avec les prédécesseurs d'un sommet, alors que donne ses successeurs. On construit donc le graphe symétrique.
Le graphe symétrique de a les mêmes sommets et les arcs inversés : est un arc de si et seulement si est un arc de . Ainsi est la liste des prédécesseurs de dans : .
static Liste[] symetrique(Liste[] G) { int n = G.length;
Liste[] Gp = new Liste[n]; // n listes vides
for (int i = 0; i < n; i++)
for (Liste l = G[i]; !vide(l); l = reste(l)) { int j = premier(l);
Gp[j] = new Liste(i, Gp[j]); // arc j --> i ajouté à G'
}
return Gp;
}
Chaque sommet est touché une fois (boucle externe), chaque arc une fois (boucle interne), et le corps de boucle est en temps constant puisque l'ajout se fait en tête de liste. Le temps de calcul est de la forme : la complexité est , linéaire en la taille du graphe. On ne peut pas faire mieux, puisqu'il faut au moins lire le graphe.
Graphe symétrique G' :
0 :
1 : 0
2 : 0
3 : 1 0
4 : 2
5 : 2 1
6 : 5 4 2
7 : 6 5 4 3 2
Attention
Les listes de sortent « à l'envers » : comme on ajoute en tête, contient alors que les arcs ont été rencontrés dans l'ordre . L'ordre des éléments d'une liste d'arcs n'a pas d'importance pour le graphe… mais il peut en avoir pour la solution retournée lorsqu'il y a plusieurs solutions optimales (voir section 7.6).
7.3 Numérotation isotone
Une numérotation des sommets d'un graphe est isotone si pour tout arc on a : les arcs vont toujours d'un numéro plus petit vers un numéro plus grand.
La suite du cours est sur Adloun
Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.
Lire la suite du coursEn accès libre, sans inscription.