Couplage maximal dans un graphe biparti¶
Ce TP accompagne la partie Couplage maximal dans un graphe biparti du chapitre Algorithmique avancée des graphes.
Graphe biparti¶
On va considérer ici un graphe biparti donné par une liste d'arêtes sous la
forme (source,but). Voici un exemple :
[ (0,3); (1,3); (1,4); (2,3); (2,4) ]
En commençant à numéroter à 0, on peut donc supposer que le nombre de sommets est égal au plus grand entier apparaissant dans un couple plus un.
Question 1. Écrire une fonction nombre_sommets : (int * int) list -> int
qui calcule ce nombre.
Corrigé
let rec nombre_sommets l =
match l with
| [] -> 0
| (a,b)::q -> let n = nombre_sommets q in
max (1 + max a b) n
Pour tester :
assert(nombre_sommets [(0,3);(1,3);(1,4);(2,3);(2,4)] = 5);
assert(nombre_sommets [(0,8);(0,7);(2,6);(2,9);(3,8);(4,8);(4,9);(5,11)] = 12);
"OK"
On va maintenant récupérer la liste des premières composantes et la liste des secondes composantes sans répétition.
Pour cela, on commence par écrire une fonction permettant de réaliser t :: q
en omettant t en cas de répétition.
Question 2. Écrire une fonction cons_uniq : 'a -> 'a list -> 'a list
telle que cons_uniq t q renvoie t::q si t n'est pas dans la liste q et
q sinon.
Corrigé
let cons_uniq t q =
if List.mem t q
then q
else t::q
Pour tester :
assert(cons_uniq 0 [] = [0]);
assert(cons_uniq 0 [0] = [0]);
assert(cons_uniq 0 [1] = [0;1]);
"OK"
Question 3. Écrire une fonction
separe_liste : (int * int) list -> int list * int list qui prend en argument
une liste de couples d'entiers et renvoie le couple des listes sans
répétitions des premières et secondes composantes.
Corrigé
let rec separe_liste l =
match l with
| [] -> [], []
| (a,b)::q -> let l1, l2 = separe_liste q in
(cons_uniq a l1, cons_uniq b l2)
Pour tester :
assert(separe_liste [(0,3);(1,3);(1,4);(2,3);(2,4)] = ([0;1;2],[3;4]));
assert(separe_liste [(0,8);(0,7);(2,6);(2,9);(3,8);(4,8);(4,9);(5,11)] =
([0;2;3;4;5],[7;6;8;9;11]));
"OK"
Question 4. Écrire une fonction
intersection : 'a list -> 'a list -> 'a list qui prend en arguments deux
listes sans répétitions et renvoie une liste contenant les éléments présents
dans les deux listes.
Corrigé
let intersection l1 l2 =
List.filter (fun x -> List.mem x l2) l1
Pour tester :
assert(intersection [1;2;3] [0;1] = [1]);
assert(intersection [1;2;3] [1;2] = [1;2]);
assert(intersection [1;2;3] [4;5] = []);
assert(intersection [] [3;4] = []);
assert(intersection [1;2;3] [] = []);
"OK"
Question 5. En déduire une fonction biparti : (int * int) list -> bool
qui vérifie qu'une liste d'arêtes permet effectivement de représenter un graphe
biparti où les sommets sont d'un côté les premières composantes et de l'autre
les secondes.
Corrigé
let biparti l =
let l1, l2 = separe_liste l in intersection l1 l2 = []
Pour tester :
assert(biparti [(0,3);(1,3);(1,4);(2,3);(2,4)]);
assert(not (biparti [(0,3);(1,0);(1,4);(2,3);(2,4)]));
"OK"
De cette liste d'arêtes représentant un graphe biparti à \(n\) sommets, on va en
déduire le graphe lui-même en le représentant sous la forme d'un couple
(ladj, sources) où ladj est une représentation en listes d'adjacence et
sources est un tableau de booléens indiquant si un sommet appartient à la
première partie du graphe biparti, c'est-à-dire s'il est la première
composante d'un des couples d'arêtes.
Ainsi, les sommets qui n'apparaissent pas dans les arêtes sont associés implicitement à la seconde partie, ce qui ne sera pas gênant dans la suite.
Question 6. Écrire une fonction
graphe_aretes : (int * int) list -> int list array qui renvoie le tableau des
listes d'adjacence d'un graphe non orienté donné sous la forme d'une liste
d'arêtes.
Corrigé
let graphe_aretes l =
let n = nombre_sommets l in
let ladj = Array.make n [] in
List.iter (fun (a,b) ->
ladj.(a) <- b :: ladj.(a); ladj.(b) <- a :: ladj.(b)) l;
ladj
Pour tester :
assert(graphe_aretes [(0,3);(1,3);(1,4);(2,3);(2,4)]
= [|[3]; [4; 3]; [4; 3]; [2; 1; 0]; [2; 1]|]);
assert(graphe_aretes [(0,8);(0,7);(2,6);(2,9);(3,8);(4,8);(4,9);(5,11)]
= [|[7; 8]; []; [9; 6]; [8]; [9; 8]; [11]; [2];
[0]; [4; 3; 0]; [4; 2]; []; [5]|]);
"OK"
Question 7. En déduire une fonction
graphe_biparti : (int * int) list -> int list array * bool array qui renvoie
le couple (ladj, sources) représentant le graphe biparti.
Corrigé
let graphe_biparti l =
let g = graphe_aretes l in
let s1, _ = separe_liste l in
let sources = Array.make (Array.length g) false in
List.iter (fun i -> sources.(i) <- true) s1;
g, sources
Pour tester :
assert (snd(graphe_biparti [(0,3);(1,3);(1,4);(2,3);(2,4)])
= [|true; true; true; false; false|]);
assert(snd(graphe_biparti [(0,8);(0,7);(2,6);(2,9);(3,8);(4,8);(4,9);(5,11)])
= [|true; false; true; true; true; true; false; false;
false; false; false; false|]);
"OK"
Couplage maximal¶
On va programmer l'algorithme pour déterminer un couplage maximal par des bascules successives de chemins augmentants (voir la preuve du théorème de Berge dans le cours).
On va adopter deux représentations d'un couplage. La première est la représentation élémentaire comme sous-liste de la liste d'arêtes du graphe.
Par exemple [ (0,3); (1,4) ] est un couplage pour la liste d'arêtes donnée
plus haut.
Question 8. Écrire une fonction est_couplage : (int * int) list -> bool
qui vérifie si une liste d'arêtes est un couplage en vérifiant qu'il n'y a pas
d'arêtes ayant une extrémité commune.
Votre fonction devra être de complexité linéaire.
Corrigé
let est_couplage el =
let n = nombre_sommets el in
let libres = Array.make n true in
let couplage = ref true in
List.iter (fun (x,y) ->
couplage := !couplage && libres.(x) && libres.(y);
libres.(x) <- false; libres.(y) <- false) el;
!couplage
La complexité est linéaire en la longueur de la liste plus le plus grand numéro de sommet, à cause de la création du tableau.
Pour tester :
assert( est_couplage [(0,1);(2,3)] );
assert( not(est_couplage [(0,1);(1,3)]) );
"OK"
L'autre représentation est un tableau indiquant pour un sommet \(i\), soit
Some j quand on a une arête \(\{i,j\}\) dans le couplage et None sinon. En
effet, un couplage définit une fonction partielle involutive de
\(S \rightarrow S\).
Ainsi, le tableau [| Some 3; Some 4; None; Some 0; Some 1 |] permet de
représenter le couplage [ (0,3); (1,4) ] en considérant que les sommets
sont dans \(\range{0}{4}\).
Question 9. Écrire des fonctions permettant de passer d'une représentation à une autre :
couplage_liste_vers_tab : (int * int) list -> int option array;couplage_tab_vers_liste : bool array -> int option array -> (int * int) listici, on a besoin de connaître les sources pour choisir comment placer les arêtes.
Corrigé
let couplage_liste_vers_tab l =
let n = nombre_sommets l in
let couplage = Array.make n None in
List.iter (fun (a,b) ->
couplage.(a) <- Some b;
couplage.(b) <- Some a) l;
couplage
let couplage_tab_vers_liste sources couplage =
let cpl_aretes = ref [] in
let n = Array.length sources in
for i = 0 to n-1 do
if sources.(i)
then match couplage.(i) with
| None -> ()
| Some j -> cpl_aretes := (i,j) :: !cpl_aretes
done;
!cpl_aretes
Attention, couplage_liste_vers_tab renvoie un tableau dont la taille est
le nombre de sommets apparaissant dans le couplage, qui peut être
inférieur au nombre de sommets du graphe.
Pour tester :
assert( couplage_liste_vers_tab [ (0,3); (1,4) ]
= [| Some 3; Some 4; None; Some 0; Some 1 |]);
assert( couplage_tab_vers_liste
[| true; true; true; false; false |]
[| Some 3; Some 4; None; Some 0; Some 1 |]
= [ (1,4); (0,3) ]);
"OK"
On va définir ici le graphe résiduel associé à un couplage (voir le cours) de manière implicite avec une fonction permettant de décider si une arête \(x \rightarrow y\) est dans le graphe résiduel. Les arêtes hors du couplage vont des sources vers les buts, celles du couplage des buts vers les sources ; on n'ajoute pas ici les sommets \(s\) et \(t\) du cours.
Attention il peut y avoir des arêtes \(x \rightarrow z\) quand \((x, y)\) est dans le couplage.
Question 10. Écrire une fonction
arete_residuelle : bool array -> int option array -> int -> int -> bool
telle que l'appel arete_residuelle sources couplage x y permette de décider
si \(x \rightarrow y\) est dans le graphe résiduel, en supposant que
\(\{x, y\}\) est une arête du graphe.
Corrigé
let arete_residuelle sources couplage x y =
if sources.(x)
then couplage.(x) <> Some y
else couplage.(x) = Some y
Pour tester :
assert(arete_residuelle [|true;false|] [|None;None|] 0 1);
assert(not (arete_residuelle [|true;false|] [|None;None|] 1 0));
assert(arete_residuelle [|true;false|] [|Some 1; Some 0|] 1 0);
assert(not (arete_residuelle [|true;false|] [|Some 1; Some 0|] 0 1));
assert(arete_residuelle [|true;true;false;false|] [|Some 2; None; Some 0; None|] 0 3);
assert(not (arete_residuelle [|true;true;false;false|] [|Some 2; None; Some 0; None|] 0 2));
"OK"
Pour chercher un chemin augmentant, on va commencer par effectuer un parcours
en profondeur récursif dans un graphe avec une notion d'arête implicite, une
fonction comme arete_residuelle, et on va remplir un tableau de
prédécesseurs où \(x\) est le prédécesseur de \(y\) si c'est le DFS depuis \(x\)
qui a appelé le DFS depuis \(y\).
Dans toute la suite, on suppose que le prédécesseur de \(x\) est \(x\) lui-même quand on a lancé le DFS initialement depuis le sommet \(x\).
Question 11. Écrire une fonction
dfs : int list array -> (int -> int -> bool) -> int option array -> int -> unit
telle que dfs g est_arete pred x effectue un DFS depuis le sommet x
dans le graphe donné sous forme de listes d'adjacence par g, avec
une fonction est_arete permettant de
tester si une arête est à considérer dans le parcours et en remplissant le
tableau des prédécesseurs pred, qui est un int option array car la valeur
vaut None tant que le sommet n'est pas découvert.
Rappel : on a spécifié que pred.(x) valait Some x si x était un des
sommets initiaux sur lesquels on a commencé le DFS.
Corrigé
let rec dfs g est_arete pred x =
List.iter (fun y ->
if pred.(y) = None && est_arete x y
then begin
pred.(y) <- Some x;
dfs g est_arete pred y
end)
g.(x)
C'est à l'appelant d'initialiser pred.(x) à Some x avant l'appel
initial.
Question 12. Écrire une fonction
remonte : int option array -> int -> int list
telle que remonte pred x renvoie la liste des sommets allant du
point de départ d'un DFS ayant rempli pred jusqu'à x en remontant la
relation de prédécesseurs.
Corrigé
let remonte pred x =
let rec aux pred y acc =
match pred.(y) with
| None -> failwith "Impossible"
| Some x when x = y -> y::acc
| Some x -> aux pred x (y::acc)
in
aux pred x []
On considère qu'on a effectué un DFS dans le graphe résiduel associé à un couplage depuis un sommet et on cherche maintenant à écrire une fonction qui permet de déterminer s'il existe un chemin depuis ce sommet vers un sommet libre parmi les buts, c'est-à-dire les sommets qui ne sont pas des sources.
Question 13. Écrire une fonction
cherche_chemin : int option array -> bool array -> int option array -> int -> int list option
telle que cherche_chemin pred sources couplage depart renvoie
Some phi si phi est la liste des sommets visités dans un chemin du graphe
résiduel de depart à un sommet but libre pour couplage et None sinon.
Corrigé
let cherche_chemin pred sources couplage depart =
let rec aux i =
if i = Array.length sources
then None
else if not sources.(i) && couplage.(i) = None
&& pred.(i) <> None
then let phi = remonte pred i in
if List.hd phi = depart
then Some phi
else aux (i+1)
else aux (i+1)
in
aux 0
Si le DFS n'a été lancé que depuis depart, le test
List.hd phi = depart est toujours vrai ; il protège le cas où pred
proviendrait de plusieurs DFS.
Question 14. Écrire une fonction
chemin_augmentant : int list array -> bool array -> int option array -> int -> int list option
telle que chemin_augmentant g sources couplage depart renvoie Some phi où
phi est la liste des sommets d'un chemin augmentant issu de la source
libre depart (et donc arrivant nécessairement dans un sommet but
libre), et None s'il n'en existe pas.
Corrigé
let chemin_augmentant g sources couplage depart =
let n = Array.length g in
let pred = Array.make n None in
pred.(depart) <- Some depart;
dfs g (arete_residuelle sources couplage) pred depart;
cherche_chemin pred sources couplage depart
Le chemin obtenu est élémentaire car il suit l'arbre du parcours, et il alterne entre arêtes hors du couplage (des sources vers les buts) et arêtes du couplage (des buts vers les sources) : c'est bien un chemin augmentant d'après la partie du cours sur le graphe résiduel.
Question 15. Écrire une fonction
bascule_chemin : int option array -> int list -> unit
telle que bascule_chemin couplage chemin, où chemin est un chemin
augmentant pour le couplage, va faire la bascule de toutes les arêtes de
chemin : celles dans le couplage sont enlevées et celles qui n'y sont pas
sont rajoutées.
Indice : seule la parité permet de déterminer celles qui y sont dans la mesure où on a un chemin augmentant.
Corrigé
let rec bascule_chemin couplage chemin =
match chemin with
| s::t::q -> couplage.(s) <- Some t; couplage.(t) <- Some s; bascule_chemin couplage q
| [] -> ()
| [_] -> failwith "Le chemin a forcément un nombre pair de sommets"
Il suffit d'ajouter les arêtes de rang impair : chaque sommet du chemin reçoit un nouveau partenaire, ce qui écrase automatiquement les anciennes arêtes du couplage, de rang pair.
Question 16. En déduire une fonction
couplage_maximal : (int * int) list -> (int * int) list
telle que couplage_maximal aretes, où aretes est un graphe biparti donné
sous forme d'une liste d'arêtes, renvoie un couplage maximal sous la forme
d'une liste d'arêtes.
Pour cela, on va itérer sur chaque sommet source en cherchant un chemin augmentant depuis celui-ci et en le basculant. Quand on aura traité toutes les sources, on est certain qu'il n'y a plus de chemin augmentant (c'est l'objet de la dernière propriété de la partie Déterminer un chemin augmentant dans un graphe biparti du cours).
Corrigé
let couplage_maximal l =
let g, sources = graphe_biparti l in
let n = Array.length g in
let couplage = Array.make n None in
for i = 0 to n-1 do
if sources.(i) && couplage.(i) = None
then match chemin_augmentant g sources couplage i with
| None -> ()
| Some ch -> bascule_chemin couplage ch
done;
couplage_tab_vers_liste sources couplage
Il y a au plus \(|S|\) recherches de chemin, chacune en \(O(|S| + |A|)\), d'où une complexité en \(O(|S|(|S| + |A|))\).
Pour tester :
assert(couplage_maximal [(0,3);(1,3);(1,4);(2,3);(2,4)]
= [(1,4);(0,3)]);
assert(couplage_maximal [(0,8);(0,7);(2,6);(2,9);(3,8);(4,8);(4,9);(5,11)]
= [(5, 11); (4, 9); (3, 8); (2, 6); (0, 7)]);
"OK"
Remarque
Pour tester plus sérieusement couplage_maximal, on peut le comparer avec
une fonction naïve qui calcule récursivement la taille maximale d'un
couplage : pour la première arête \(\{a, b\}\) de la liste, soit on ne la
prend pas, soit on la prend et on retire toutes les arêtes touchant \(a\) ou
\(b\). Sur des milliers de petits graphes bipartis aléatoires, on vérifie
que le résultat est un couplage, formé d'arêtes du graphe, et de même
taille que celui de la fonction naïve.