• Nie Znaleziono Wyników

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}