Zadanie: Przemytnicy
2.13. Maksymalne skojarzenie w grafie dwudzielnym
2.13.1. Dwudzielność grafu
Graf G = (V, E) nazywamy dwudzielnym, gdy zbiór jego wierzchołków V można podzielić na dwa rozł¸aczne zbiory V1 oraz V2, V1∪ V2 = V , w taki sposób, aby wszystkie krawędzie grafu (u, v) ∈ E prowadziły między wierzchołkami z różnych zbiorów (u ∈ V1, v∈ V2 lub v ∈ V1, u∈ V2). Podział taki jest możliwy dla każdego grafu niezawieraj¸acego cyklu o nieparzys-tej długości. Wiele algorytmów skonstruowanych z myśl¸a o grafach dwudzielnych, przed przyst¸apieniem do rozwi¸azania właściwego zagadnienia, musi wyznaczyć podział (V1, V2). Na listingu 2.50 przedstawiona jest implementacja funkcji bool Graph<V,E>::BiPart(vector
<bool>&), służ¸aca do wyznaczania tego podziału. Funkcja przyjmuje jako parametr ref-erencję do wektora zmiennych logicznych, a zwraca prawdę, jeśli graf, dla którego została wywołana jest dwudzielny. Wtedy również przekazany przez referencję wektor wypełniany jest wartościami logicznymi — k-ta z tych wartości reprezentuje przynależność k-tego wierzchołka grafu do zbioru V1. Działanie funkcji opiera się na algorytmie sortowania topologicznego grafu
0 1
Rysunek 2.16: (a) Graf dwudzielny o dziewięciu wierzchołkach i dwunastu krawędziach. (b) Podzi-ał zbioru wierzchołków grafu z rysunku (a) na dwa rozł¸aczne zbiory {0, 1, 2, 3, 8} oraz {4, 5, 6, 7}, taki że między dowolnymi dwoma wierzchołkami w tym samym zbiorze nie istnieje żadna krawędź.
— posortowane wierzchołki s¸a przetwarzane w kolejności wyznaczonego porz¸adku i zachłan-nie umieszczane w jednym ze zbiorów V1 lub V2. Złożoność czasowa algorytmu wynosi zatem O(n + m). Ze względu na fakt, iż grafy dla których wyznaczany jest podział przy użyciu funkcji bool Graph<V,E>::BiPart(vector <bool>&) s¸a nieskierowane, może pojawić się obawa o poprawność algorytmu — wykorzystywane jest bowiem sortowanie topologiczne dzi-ałaj¸ace poprawnie dla grafów skierowanych. Istotn¸a obserwacj¸a pozwalaj¸ac¸a na uzasadnienie poprawności algorytmu jest fakt, iż faza rozdzielaj¸aca wierzchołki grafu na dwa zbiory V1 i V2 korzysta z tylko jednej własności porz¸adku wyznaczonego przez sortowanie topologiczne — jeśli kolejno przetwarzany wierzchołek nie został jeszcze przydzielony do żadnego ze zbiorów, to jest on pierwszym wierzchołkiem analizowanym w obrębie swojej spójnej składowej, a zatem można go przydzielić do dowolnego ze zbiorów (algorytm przydziela go do zbioru V2).
Listing 2.50: Implementacja funkcji bool Graph<V,E>::BiPart(vector<bool>&) 01 bool BiPart(vector<char> &v) {
// Inicjalizacja zmiennych 02 v.resize(SIZE(g), 2);
// Wykonaj sortowanie topologiczne grafu. W grafie mogą występować cykle, ale // nie stanowi to problemu
03 VI r = TopoSortV();
// Dla każdego wierzchołka w grafie 04 FOREACH(x, r) {
// Jeśli wierzchołek nie był jeszcze odwiedzony, to przydzielany jest on do // pierwszego zbioru wierzchołków
05 if (v[*x] == 2) v[*x] = 0;
// Przetwórz każdego sąsiada aktualnego wierzchołka - jeśli nie był on jeszcze // odwiedzony, to przydziel go do innego zbioru niż wierzchołek x, a jeśli // był odwiedzony i jest w tym samym zbiorze co x, to graf nie jest
// dwudzielny
Przedstawiona funkcja bool Graph<V,E>::BiPart(vector<bool>&)będzie wykorzy-stywana przez algorytmy wyznaczania maksymalnego skojarzenia w grafach dwudzielnych.
Oryginalne algorytmy, pochodz¸ace z biblioteczki algorytmicznej, nie wymagaj¸a implementacji tej funkcji, gdyż wychodz¸a one z założenia, że wierzchołki w grafie o numerach 0 . . .n2 − 1 należ¸a do zbioru V1, podczas gdy wierzchołki n2 . . . n− 1 — do zbioru V2. Takie założenie jest wygodne w przypadku większości zadań, gdyż podczas konstrukcji grafu zazwyczaj z góry wiadomo, które wierzchołki należ¸a do którego zbioru. Odpowiednia numeracja wierzchołków pozwala na skrócenie kodu implementowanego rozwi¸azania zadania.
Listing 2.51: Podział wierzchołków grafu z rysunku 2.16.a wyznaczony przez funkcję bool Graph<V,E>::BiPart(vector<bool>&l)
Wierzcholek 0 nalezy do V0 Wierzcholek 1 nalezy do V0 Wierzcholek 2 nalezy do V0 Wierzcholek 3 nalezy do V0 Wierzcholek 4 nalezy do V1 Wierzcholek 5 nalezy do V1 Wierzcholek 6 nalezy do V1 Wierzcholek 7 nalezy do V1 Wierzcholek 8 nalezy do V0
Listing 2.52: Kod źródłowy programu użytego do wyznaczenia wyniku z listingu 2.51. Pełny kod źródłowy programu znajduje się w pliku bipart str.cpp
01 struct Ve { 02 int rev;
03};
// Wzbogacenie struktury wierzchołków wymagane przez funkcję Bipart 04 struct Vs {
05 int t, s;
06};
07 intmain() { 08 int n, m, b, e;
09 cin >> n >> m;
// Skonstruuj graf o n wierzchołkach i m krawędziach 10 Graph<Vs, Ve> g(n);
11 REP(x, m) {
12 cin >> b >> e;
13 g.EdgeU(b, e);
14 }
15 vector<char> l;
// Wypisz wynik wyznaczony przez funkjcę Bipart 16 if (g.BiPart(l)) {
17 REP(x, SIZE(l)) cout << "Wierzcholek " << x <<
18 " nalezy do V" << ((int) l[x]) << endl;
19 }
20 return 0;
21}
2.13.2. Maksymalne skojarzenie w grafie dwudzielnym w czasie O(n∗(n+m)) Bardzo ważnym pojęciem, w kontekście maksymalnego skojarzenia, jest ścieżka naprzemien-na, która pełni podobn¸a rolę co ścieżka powiększaj¸aca w przypadku maksymalnego przepły-wu. Niech dany będzie graf G = (V, E) oraz zbiór E0 ⊆ E, stanowi¸acy skojarzenie w grafie G (niekoniecznie maksymalne). Ścieżk¸a naprzemienn¸a S nazywamy ścieżkę postaci s1 → s2 → . . . → sk, tak¸a że s1 oraz sk nie s¸a wierzchołkami skojarzonymi, oraz krawędzie (s2∗l, s2∗l+1) ∈ E0, l ∈ {1, 2, . . . ,k2 − 1}. Przykład ścieżki naprzemiennej o długości 5 przed-stawiony jest na rysunku 2.17.d.
Pierwsz¸a, jak¸a zaprezentujemy funkcj¸a służ¸ac¸a do wyznaczania maksymalnego skojarzenia w grafie dwudzielnym, jest boolGraph<V,E>::BipMatching(), której kod źródłowy przed-stawiony jest na listingu 2.53. W przypadku, gdy graf nie jest dwudzielny, funkcja ta zwraca fałsz. W przeciwnym razie, dla każdego wierzchołka v w grafie wyznaczana jest wartość zmiennej int m — numer wierzchołka skojarzonego z v. W przypadku wierzchołków niesko-jarzonych, wyznaczona wartość to −1. Funkcja wymaga wzbogacenia struktury wierzchołków o dodatkowe pola int moraz int t.
Metoda działania algorytmu polega na znajdowaniu kolejnych ścieżek naprzemiennych (podobnie jak w przypadku maksymalnego przepływu i ścieżki poszerzaj¸acej, skojarzenie jest maksymalne, gdy w grafie nie ma więcej ścieżek naprzemiennych). W każdym poje-dynczym przebiegu algorytmu, przy użyciu przeszukiwania grafu w gł¸ab wyznaczana jest ścieżka naprzemienna, a następnie zamieniana jest przynależność do skojarzenia wszystkich krawędzi leż¸acych na tej ścieżce. Pojedyncze przeszukiwanie zwiększa wielkość wyznaczonego skojarzenia o 1. Ponieważ wielkość maksymalnego skojarzenia w grafie jest ograniczona z góry przez n2, zatem złożoność algorytmu to O(n ∗ (n + m)).
Listing 2.53: Implementacja funkcji boolGraph<V,E>::BipMatching() // Funkcja realizująca wyszukiwanie ścieżki naprzemiennej w grafie przy użyciu // przeszukiwania w głąb
01 bool MDfs(intx) {
// Jeśli wierzchołek nie został jeszcze odwiedzony...
02 if (!g[x].t) { 03 g[x].t = 1;
// Dla każdej krawędzi wychodzącej z wierzchołka, jeśli koniec krawędzi nie // jest skojarzony lub istnieje możliwość wyznaczenia rekurencyjnie ścieżki // naprzemiennej...
04 FOREACH(it, g[x]) if (g[it->v].m == -1 || MDfs(g[it->v].m)) { // Skojarz wierzchołki wzdłuż aktualnie przetwarzanej krawędzi
05 g[g[it->v].m = x].m = it->v;
06 return true;
07 }
08 }
09 return false;
10}
// Funkcja wyznacza maksymalne skojarzenie w grafie dwudzielnym. Umieszcza ona w // polu m każdego wierzchołka numer wierzchołka z nim skojarzonego (lub -1 // dla wierzchołków nieskojarzonych).
11 bool BipMatching() {
Listing 2.53: (c.d. listingu z poprzedniej strony) 12 vector<char> l;
// Jeśli graf nie jest dwudzielny, zwróć fałsz 13 if (!BiPart(l)) return 0;
// Inicjalizacja zmiennych 14 int n = SIZE(g), p = 1;
15 FOREACH(it, g) it->m = -1;
// Dopóki istnieje ścieżka naprzemienna...
16 while (p) {
17 p = 0;
18 FOREACH(it, g) it->t = 0;
// Wykonaj przeszukiwanie w głąb w celu znalezienia ścieżki naprzemiennej 19 REP(i, n) if (l[i] && g[i].m == -1) p |= MDfs(i);
20 }
21 return 1;
22}
Listing 2.54: Maksymalne skojarzenie wyznaczone przez funkcję bool Graph<V,E>::BipMatching()dla grafu z rysunku 2.17.a
Wierzcholek 0 skojarzono z 3 Wierzcholek 1 skojarzono z 2 Wierzcholek 4 skojarzono z 5
Listing 2.55: Kod źródłowy programu użytego do wyznaczenia wyniku z listingu 2.54. Pełny kod źródłowy programu znajduje się w pliku bipmatch.cpp
01 struct Ve { 02 int rev;
03};
// Wzbogacenie wierzchołków grafu wymagane przez funkcję BipMatching 04 struct Vs {
05 int m, t;
06};
07 intmain() {
08 int n, m, s, b, e;
// Skonstruuj odpowiedni graf na podstawie danych wejściowych
09 cin >> n >> m;
10 Graph<Vs, Ve> g(n);
11 REP(x, m) {
12 cin >> b >> e;
13 g.EdgeU(b, e);
14 }
// Wyznacz maksymalne skojarzenie oraz wypisz wynik
15 if (g.BipMatching()) REP(x, SIZE(g.g)) if(g.g[x].m > x)
16 cout << "Wierzcholek " << x << " skojarzono z " << g.g[x].m << endl;
17 return 0;
18}
2.13.3. Maksymalne skojarzenie w grafie dwudzielnym w czasie O((n + m) ∗√n) Przedstawiona w tym rozdziale implementacja algorytmu Hopcrofta-Karpa wyznacza maksy-malne skojarzenie w grafie dwudzielnym, sprowadzaj¸ac problem do wyznaczania maksymal-nego jednostkowego przepływu w grafie. Niech zbiory V1 oraz V2 stanowi¸a podział dwudziel-ny zbioru wierzchołków grafu G = (V, E), dla którego wyznaczane jest maksymalne sko-jarzenie. Nasz algorytm modyfikuje graf G, dodaj¸ac do niego dwa specjalne wierzchołki — źródło oraz ujście. Wierzchołek-źródło ł¸aczony jest ze wszystkimi wierzchołkami ze zbioru V1, natomiast wszystkie wierzchołki ze zbioru V2 ł¸aczone s¸a z ujściem. Przykładowa kon-strukcja tego typu została przedstawiona na rysunku 2.18. Po wyznaczeniu maksymalnego przepływu między źródłem a ujściem, krawędzie oryginalnego grafu G, przez które reali-zowany jest jednostkowy przepływ, należ¸a do wyznaczonego maksymalnego skojarzenia. Do wyznaczenia maksymalnego przepływu można wykorzystać dowolny algorytm, jednak jeżeli zastosujemy algorytm do wyznaczania jednostkowego maksymalnego przepływu realizowany przez funkcję int Graph<V,E>::UnitFlow(int,int), to otrzymamy algorytm o złożoności czasowej O((n + m) ∗√n).
Opisany algorytm realizowany jest przez funkcjęVI Graph<V,E>::Hopcroft()z listin-gu 2.56. Funkcja ta zwraca jako wynik wektor liczb całkowitych o długości n. Dla każdego wierzchołka v odpowiadaj¸aca mu liczba reprezentuje numer wierzchołka, z którym v został skojarzony. W przypadku, gdy wierzchołek v nie został skojarzony, odpowiadaj¸ac¸a liczb¸a jest
−1.
Listing 2.56: Implementacja funkcjiVI Graph<V,E>::Hopcroft() // UWAGA: Na skutek działania algorytmu graf ulega modyfikacji
01VI Hopcroft() {
// Inicjalizacja zmiennych 02 intn = SIZE(g);
03 VI res(n, -1);
04 vector<char> l;
// Jeśli graf nie jest dwudzielny, to algorytm zwraca puste skojarzenie 05 if (!BiPart(l)) return res;
// Do grafu dodawane są dwa wierzchołki, jeden z nich jest łączony ze wszystkimi // wierzchołkami z pierwszego zbioru wyznaczonego przez funkcję BiPart,
// natomiast drugi z wierzchołkami z drugiego zbioru.
06 g.resize(n + 2);
07 REP(i, n) if (!l[i]) EdgeD(n, i);
08 else EdgeD(i, n + 1);
// Wyznaczany jest przepływ jednostkowy w zmodyfikowanym grafie 09 UnitFlow(n, n + 1);
// Skojarzenie jest rekonstruowane na podstawie wyniku wyliczonego przez // algorytm wyznaczający przepływ jednostkowy
10 REP(i, n) if (l[i] && g[i][0].v != n + 1) 11 res[res[g[i][0].v] = i] = g[i][0].v;
12 returnres;
13}
Listing 2.57: Wynik wygenerowany przez funkcjęVI Graph<V,E>::Hopcroft()dla grafu z ry-sunku 2.18.a
Wierzcholek 0 skojarzono z 3 Wierzcholek 1 skojarzono z 2 Wierzcholek 4 skojarzono z 5
Listing 2.58: Kod źródłowy programu użytego do wyznaczenia wyniku z listingu 2.58. Pełny kod źródłowy programu znajduje się w pliku hopcroft str.cpp
01 struct Ve { };
// Wzbogacenie wierzchołków grafu wymagane przez funkcję Hopcroft 02 struct Vs {
03 int t, s;
04};
05 intmain() { 06 int n, m, b, e;
// Skonstruj graf o odpowiednim rozmiarze oraz dodaj do niego wymagane krawędzie
07 cin >> n >> m;
08 Graph<Vs, Ve> g(n);
09 REP(x, m) {
10 cin >> b >> e;
11 g.EdgeD(b, e);
12 }
// Wykonaj algorytm Hopcrofta oraz wypisz wynik 13 VI res = g.Hopcroft();
14 REP(x, SIZE(res)) if(res[x] > x)
15 cout << "Wierzcholek " << x << " skojarzono z " << res[x] << endl;
16 return 0;
17}