Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz  

Autor artykułu: mgr Jerzy Wałaszek

©2026 mgr Jerzy Wałaszek

Maksymalne skojarzenia w grafach dwudzielnych

SPIS TREŚCI
Tematy pomocnicze

Problem

Dla danego grafu dwudzielnego należy znaleźć maksymalne skojarzenie.

Rozwiązanie

Sieci przepływowe służą zwykle do rozwiązywania problemów związanymi z przepływami – komunikacja, transport, ruch w sieciach teleinformatycznych itp. Jednakże po przyjęciu pewnych założeń można je stosować do rozwiązywania zadziwiająco dużej klasy problemów, które na pierwszy rzut oka nie mają wiele wspólnego z przepływami. Jednym z takich problemów jest znajdowanie maksymalnego skojarzenia w grafie dwudzielnym

Przypomnijmy:

Graf dwudzielny (ang. bipartite graph lub bigraph) jest grafem, którego wierzchołki można pokolorować za pomocą dwóch kolorów, tak aby żadne dwa sąsiednie nie były tego samego koloru. Innymi słowy, graf dwudzielny zawiera wierzchołki należące do dwóch rozłącznych klas. Wierzchołki należące do tej samej klasy nie łączą się krawędzią.

obrazek

Skojarzeniem nazywamy znalezienie zbioru krawędzi grafu, które w grafie dwudzielnym łączą pary wierzchołków. Przy czym wierzchołki tworzące jedną parę nie mogą już należeć do innych par. Dwudzielność grafu gwarantuje, iż będą to zawsze wierzchołki należące do różnych klas.

obrazek

Skojarzenie nazywamy maksymalnym, jeśli zawiera maksymalną, możliwą liczbę krawędzi. Powyżej przedstawiamy skojarzenie w grafie dwudzielnym (wierzchołki skojarzone są zielonymi krawędziami), jednakże nie jest to skojarzenie maksymalne, chociaż w tym układzie nie można już dodać żadnej krawędzi. Skojarzenie maksymalne uzyskamy reorganizując nieco krawędzie zielone. Wynik mamy poniżej:

obrazek

Algorytm znajdowania maksymalnych skojarzeń w grafach dwudzielnych opisaliśmy dokładnie przy okazji problemu kojarzenia małżeństw. Rozwiązanie wykorzystywało metodę BFS do znajdowania naprzemiennych ścieżek rozszerzających. Do tego samego wyniku możemy dojść stosując algorytm Forda-Fulkersona, który znajduje maksymalny przepływ w sieci. W tym celu musimy nieco zmodyfikować graf dwudzielny dodając węzeł źródła S oraz węzeł ujścia T:

obrazek

Węzeł źródła S łączymy ze wszystkimi węzłami jednej klasy, a węzły należące do drugiej klasy łączymy z ujściem T. Przepustowość wszystkich krawędzi ustalamy na 1. W efekcie otrzymujemy powyższą sieć przepływową. Wyznaczamy w niej maksymalny przepływ przy pomocy algorytmu Forda-Fulkersona. Ponieważ do węzłów niebieskich (umowne oznaczenie węzłów w pierwszej klasie) dochodzą ze źródła krawędzie o przepustowości 1, to przepływ wypływający z tych węzłów nie może przekroczyć 1. Jeśli przepływ będzie miał wartość 1, to popłynie tylko jedną krawędzią grafu-tą, która kojarzy wierzchołek niebieski z czerwonym. W efekcie wyznaczone przepływy w krawędziach grafu wskażą nam skojarzenia wierzchołków. Jeśli w danej krawędzi występuje przepływ o wartości 1, to krawędź ta kojarzy dwa wierzchołki w grafie dwudzielnym. Jeśli przepływ w krawędzi jest zerowy, to krawędź nie jest wykorzystywana do kojarzenia wierzchołków.

Dodatkową korzyścią jest wyznaczenie wierzchołków nieskojarzonych – wystarczy zbadać przepływ zerowy w krawędziach połączonych ze źródłem ( wierzchołki nieskojarzone w klasie pierwszej) oraz w krawędziach połączonych z ujściem (wierzchołki nieskojarzone w klasie drugiej). Jeśli skojarzenie jest zupełne (wszystkie wierzchołki klasy pierwszej skojarzone z wierzchołkami klasy drugiej), to oczywiście we wszystkich tych krawędziach będzie przepływ o wartości 1.

Wartość wyznaczonego przez algorytm maksymalnego przepływu w sieci informuje nas o liczbie skojarzonych wierzchołków – pozwala od razu stwierdzić, czy w danym grafie możliwe jest skojarzenie zupełne.

Z rozważań tych wyłania się ogólny algorytm znajdowania maksymalnych skojarzeń w grafach dwudzielnych przy pomocy algorytmu wyznaczania maksymalnego przepływu w sieci przepływowej:

Krok 1: Pokoloruj wierzchołki grafu na czerwono i niebiesko, tak aby wierzchołki z tej samej klasy posiadały ten sam kolor.
Krok 2: Potraktuj graf jako graf skierowany-wszystkie krawędzie są skierowane od wierzchołka niebieskiego do czerwonego.
Krok 3: Na podstawie grafu zbuduj macierz przepustowości krawędzi C[n+2] × [n+2] ustawiając przepustowość krawędzi na 1 – macierz powinna zawierać dodatkowo dane dla źródła S i ujścia T. W wierszu C[S][ ] należy ustawić 1 w kolumnach odpowiadających wierzchołkom niebieskim, 0 dla pozostałych wierzchołków. W kolumnie C[ ][T] należy ustawić 1 dla wierszy odpowiadających wierzchołkom czerwonym i 0 dla pozostałych wierzchołków.
Krok 4: Zastosuj algorytm Karpa-Edmondsa do wyznaczenia maksymalnego przepływu w sieci.
Krok 5: Dla każdego niebieskiego wierzchołka grafu wyprowadź tę krawędź, której przepływ jest równy 1.
Krok 6: Koniec

Jest to algorytm zbudowany z kilku mniejszych algorytmów. Aby wykorzystać go do napisania programu, musimy rozwiązać kilka problemów:

Kolorowanie wierzchołków grafu rozwiązuje algorytm badający dwudzielność grafu. W wyniku jego działania otrzymujemy informację o tym, czy graf jest dwudzielny. Dodatkowo dostajemy wypełnioną n elementową tablicę Color, która przechowuje kolory wierzchołków grafu.

Tworzenie grafu skierowanego można rozwiązać na etapie wprowadzania danych wymagając, aby krawędzie grafu dwudzielnego były zdefiniowane parami wierzchołków (niebieski, czerwony) – takie podejście uwalnia nas również od algorytmu kolorowania. Jeśli tak się nie stało i mamy na wejściu graf nieskierowany, to po prostu usuwamy z niego wszystkie krawędzie wychodzące z wierzchołków czerwonych, które znaleźliśmy w poprzednim kroku.

Macierz przepustowości C definiuje sieć przepływową. Ustawiamy przepustowość każdej krawędzi na 1. Musimy również uwzględnić węzeł źródła S oraz węzeł ujścia T. Dlatego wymiar macierzy jest o 2 większy od n, czyli liczby wierzchołków w grafie. Umawiamy się, iż źródło ma numer n+1, a ujście ma numer n+2. Wierzchołków tych wcale nie musimy dodawać do grafu - wystarczy, iż znajdą się w macierzy przepustowości.

Algorytm znajdowania maksymalnego skojarzenia w nieskierowanym grafie dwudzielnym – wersja z macierzą przepustowości

Wejście:

n : liczba wierzchołków w grafie; n ∈ C.
graf
 : zadany w dowolnie wybrany sposób, algorytm tego nie precyzuje.

Wyjście:

Macierz przepływów F o wymiarze n = 2×n+2 lub informacja, że graf nie jest dwudzielny. Macierz zawiera informację o maksymalnym skojarzeniu. Wierzchołki i,j są skojarzone ze sobą, jeśli C[i,j] = 1 i F[i,j] = 1. Źródło S jest wierzchołkiem o numerze n. Ujście jest wierzchołkiem o numerze n = 1.
fmax – zawiera informację o liczbie skojarzonych par wierzchołków.

Elementy pomocnicze:

Q : kolejka przechowująca wierzchołki grafu.
i,v,u : wierzchołki; i,v,u ∈ C.
Color : n elementowa tablica zawierająca kolory wierzchołków, w przypadku grafu dwudzielnego:
  0 - kolor szary,
  1 - kolor czerwony,
-1 - kolor niebieski.
fmax : wartość maksymalnego przepływu w sieci; fmax ∈ C.
P : tablica n elementowa przechowująca ścieżki (poprzedniki) wyszukiwane przez BFS.
CFP : tablica n elementowa przechowująca wartości cf(p) dla ścieżki kończącej się w danym węźle sieci.
cp : przechowuje wartość przepustowości rezydualnej; cp ∈ C.

Lista kroków:

K01: Utwórz n elementową
     tablicę Color
K02: Ustaw wszystkie elementy ; 0 oznacza kolor szary
     tablicy Color na 0
K03: Utwórz pustą kolejkę Q
K04: Dla i = 0, 1, …, n-1: ; przechodzimy przez kolejne wierzchołki grafu
     wykonuj kroki K05…K15
K05:   Jeśli Color[i] ≠ 0, ; szukamy wierzchołka szarego, który będzie wierzchołkiem startowym
       to następny obieg pętli K04
K06:   Color[i] ← 1 ; wierzchołek startowy kolorujemy na czerwono
K07:   Q.push(i) ; numer wierzchołka umieszczamy w kolejce
K08:   Dopóki Q.empty() = false: ; rozpoczynamy przejście BFS
       wykonuj kroki K09…K15
K09:     v ← Q.front() ; pobieramy element z początku kolejki
K10:     Q.pop() ; pobrany element usuwamy z kolejki
K11:     Dla każdego sąsiada u wierzchołka v: ; przetwarzamy sąsiadów wierzchołka v
         wykonuj kroki K12…K15
K12:       Jeśli Color[u] = Color[v], ; sąsiad ma ten sam kolor, więc graf nie może być dwudzielny
           to zakończ z komunikatem o błędzie
K13:       Jeśli Color[u] ≠ 0, ; szukamy wierzchołka niepokolorowanego,
           to następny obieg pętli K11 ; czyli jeszcze nieodwiedzonego
K14:       Color[u] ← -Color[v] ; kolorujemy sąsiada na kolor przeciwny
K15:       Q.push(u) ; sąsiada umieszczamy w kolejce
K16: Utwórz macierz C ; tworzymy macierz przepustowości 
     o rozmiarze n+2×n+2
K17: Ustaw wszystkie elementy
     macierzy C na 0
K18: Dla i = 0, 1, …, n-1: ; przechodzimy przez kolejne wierzchołki grafu
     wykonuj kroki K19…K23
K19:   Jeśli Color[i] = 1, ; sprawdzamy kolor wierzchołka
       to idź do kroku K23
K20:   Dla każdego sąsiada u wierzchołka i: ; krawędzie wierzchołków niebieskich
       wykonuj: C[i, u] ← 1 ; otrzymują przepustowość 1
K21    C[n, i] ← 1 ; dodajemy krawędź od źródła do wierzchołka i
K22:   Następny obieg pętli K18
K23:   C[i, n+1] ← 1 ; dla wierzchołków czerwonych dodajemy tylko krawędź do ujścia
K24: fmax ← 0 ; zerujemy wartość przepływu sieciowego
K25: Utwórz macierz F ; zerujemy macierz przepływów
     o wymiarze n = 2×n+2
     i wyzeruj ją
K26: Dla i = 0, 1, …, n+1:
     wykonuj: P[i] ← -1
K27: P[n] ← -2 ; to zapobiegnie wybieraniu źródła przez BFS
K28: CFP[n] ← ∞ ; w CFP przechowujemy przepustowość rezydualną ścieżki
K29: Dopóki Q.empty() = false, ; zerujemy kolejkę
     wykonuj Q.pop()
K30: Q.push(s) ; w kolejce umieszczamy numer źródła
K31: Dopóki Q.empty() = false, 
     wykonuj kroki K32…K39
K32:   v = Q.front(); Q.pop() ; pobieramy numer wierzchołka z kolejki
K33:   Dla u = 0, 1, …, n+1: ; rozpoczynamy BFS
       wykonuj kroki K34…K39
K34:     cp ← C[v][u] - F[v][u] ; wyznaczamy przepustowość rezydualną kanału (v, u)
K35:     Jeśli (cp = 0) obrazek (P[u] ≠ -1), ; omijamy przy braku krawędzi lub odwiedzonym sąsiedzie
         to następny obieg pętli K33
K36:     P[u] ← v ; zapamiętujemy poprzednik na ścieżce
K37:     CPF[u] ← min(CPF[v], cp) ; obliczamy przepustowość rezydualną do węzła u
K38:     Jeśli u = n+1, ; ścieżka rozszerzająca znaleziona!
         to idź do kroku K41
K39:     Q.push(u)
K40: Idź do kroku K48 ; brak ścieżki, idziemy do końcówki algorytmu
K41: fmax ← fmax+CFP[n+1] ; zwiększamy przepływ sieciowy
K42: Dopóki un, ; cofamy się po ścieżce rozszerzającej od ujścia t do źródła s 
     wykonuj kroki K43…K46
K43:   v ← P[u] ; (v, u) jest krawędzią ścieżki rozszerzającej
K44:   F[v, u] ← F[v, u] + CFP[n+1] ; w kierunku zgodnym ze ścieżką zwiększamy przepływ
K45:   F[u, v] ← F[u, v] - CFP[n+1] ; w kierunku przeciwnym zmniejszamy przepływ
K46:   u ← v ; przechodzimy do następnej krawędzi ścieżki
K47: Idź do kroku K26
K48: Pisz fmax ; wypisujemy liczbę skojarzonych par wierzchołków
K49: Jeśli fmax = 0, 
     to zakończ
K50: Dla v = 0, 1, …, n-1: ; wypisujemy pary skojarzonych wierzchołków
     wykonuj krok K51
K51:   Dla u = 0, 1, …, n-1:
       wykonuj:
         Jeśli (C[v, u] = 1) obrazek (F[v, u] = 1),
         to pisz v, u
K52: Zakończ

Przykładowe programy

Uwaga:

Zanim uruchomisz program, przeczytaj wstęp do tego artykułu, w którym wyjaśniamy funkcje tych programów oraz sposób korzystania z nich.

Program odczytuje definicję grafu nieskierowanego. Jeśli graf jest grafem dwudzielnym, to wyznacza dla niego maksymalne skojarzenie.
Dane przykładowe (przekopiuj do schowka i wklej do okna konsoli):
obrazek 10 17
0 2
0 3
0 4
0 8
1 2
1 4
1 8
2 5
2 7
3 7
3 9
4 5
4 9
5 6
6 7
6 9
8 9
Pascal
// Maksymalne skojarzenia
// Data: 26.10.2014
// (C)2014 mgr Jerzy Wałaszek
//---------------------------

program maxmatch;

// Definicja typu
// elementów listy
//----------------
type
  PSLel = ^SLel;
  SLel = record
    next  : PSLel;
    Data: integer;
  end;

// Definicja typu
// obiektowego queue
//------------------
  queue = object
    private
      head : PSLel;
      tail : PSLel;

    public
      constructor init;
      destructor destroy;
      function empty : boolean;
      function front : integer;
      procedure push(v : integer);
      procedure pop;
  end;

//---------------------
// Metody obiektu queue
//---------------------

// Konstruktor
//------------
constructor queue.init;
begin
  head := nil;
  tail := nil;
end;

// Destruktor
//-----------
destructor queue.destroy;
begin
  while not empty do pop;
end;

// Sprawdza,
// czy kolejka jest pusta
//-----------------------
function queue.empty : boolean;
begin
  if head = nil then
    empty := true
  else
    empty := false;
end;

// Zwraca początek kolejki.
// Wartość specjalna to -MAXINT
//-----------------------------
function queue.front : integer;
begin
  if head = nil then
    front := -MAXINT
  else
    front := head^.data;
end;

// Zapisuje do kolejki
//--------------------
procedure queue.push(v : integer);
var
  p : PSLel;
begin
  new(p);
  p^.next := nil;
  p^.Data:= v;
  if tail = nil then
    head := p
  else
    tail^.next := p;
  tail := p;
end;

// Usuwa z kolejki
//----------------
procedure queue.pop;
var
  p : PSLel;
begin
  if head <> nil then
  begin
    p := head;
    head := head^.next;
    if head = nil then
      tail := nil;
    dispose(p);
  end;
end;

//---------------
// Program główny
//---------------

var
  // Kolejka
  Q : queue;
  // Tablica kolorów
  // wierzchołków
  Color: array of integer;
  // Tablica list
  // sąsiedztwa
  graf : array of PSLel;
  // Macierz przepustowości
  C : array of array of integer;
  // Macierz przepływów netto
  F : array of array of integer;
  // Tablica poprzedników
  P : array of integer;
  // Tablica przepustowości
  // rezydualnych
  CFP : array of integer;
  // Zmienne proste algorytmu
  n,m,fmax,cp,v,u,i,j : integer;
  // Do wychodzenia
  // z zagnieżdżonych pętli
  esc : boolean;
  // Wskaźniki
  // elementów listy
  pr, rr : PSLel;

// Funkcja zwraca true,
// jeśli graf jest dwudzielny.
// Ustala kolory wierzchołków
// w tablicy Color
//----------------------------
function isBipartite : boolean;
begin
  // Przechodzimy przez
  // kolejne wierzchołki
  for i := 0 to n-1 do
    // Szukamy wierzchołka
    // szarego
    if Color[i] = 0 then
    begin
      // Wierzchołek startowy
      // kolorujemy na czerwono
      Color[i] := 1;
      // i umieszczamy w kolejce
      Q.push(i);
      // Przejście BFS
      while not Q.empty do
      begin
        // Pobieramy wierzchołek
        // z kolejki
        v := Q.front;
        // Pobrany wierzchołek
        // usuwamy z kolejki
        Q.pop;
        // Przeglądamy sąsiadów
        // wierzchołka v
        pr := graf [v];
        while pr <> nil do
        begin
          // pobieramy z listy
          // sąsiedztwa numer sąsiada
          u := pr^.data;
          if Color[u] = Color[v] then
            Exit(false);
          // Jeśli wierzchołek
          // nie jest odwiedzony,
          if Color[u] = 0 then
          begin
            // kolorujemy go
            // na kolor przeciwny
            Color[u] := -Color[v];
            // i umieszczamy w kolejce
            Q.push(u);
          end;
          // Następny sąsiad
          pr := pr^.next;
        end;
      end;
    end;
  isBipartite := true;
end;

begin
  // Inicjujemy kolejkę
  Q.init;
  // Ze standardowego wejścia
  // odczytujemy:
  // n - liczbę wierzchołków
  //     w grafie sieci
  // m - liczbę krawędzi
  read(n, m);
  // Tworzymy dane dynamiczne
  //-------------------------
  // Tablica kolorów
  // wierzchołków
  SetLength(Color, n);
  // Tablica list sąsiedztwa
  SetLength(graf, n);
  for i := 0 to n-1 do
  begin
    graf[i] := nil;
    Color[i] := 0;
  end;
  // Macierz przepustowości
  // krawędzi
  SetLength(C, n+2);
  // Macierz przepływów netto
  SetLength(F, n+2);
  for i := 0 to n+1 do
  begin
    SetLength(C[i], n+2);
    SetLength(F[i], n+2);
    for j := 0 to n+1 do
    begin
      C[i][j] := 0;
      F[i][j] := 0;
    end;
  end;
  // Tablica poprzedników
  SetLength(P, n+2);
  // Tablica przepustowości
  // rezydualnych
  SetLength(CFP, n+2);
  // Odczytujemy definicje
  // krawędzi i dodajemy je
  // do list sąsiedztwa
  for i := 1 to m do
  begin
    read(v, u);
    new(pr);
    pr^.Data := u;
    pr^.next := graf[v];
    graf[v] := pr;
    new(pr);
    pr^.Data := v;
    pr^.next := graf[u];
    graf[u] := pr;
  end;
  if isBipartite then
  begin
    // Ustawiamy macierz
    // przepustowości
    for i := 0 to n-1 do
      if Color[i] = -1 then
      begin
        // Przeglądamy listę
        // sąsiadów wierzchołka
        // niebieskiego
        pr := graf[i];
        while pr <> nil do
        begin
          // Przepustowość krawędzi
          // do wierzchołka czerwonego
          C[i][pr^.data] := 1;
          // Następny sąsiad
          pr := pr^.next;
        end;
        // Przepustowość krawędzi
        // od źródła
        C[n][i] := 1;
      end
      else
        // Przepustowość krawędzi
        // do ujścia
        C[i][n+1] := 1;

    //*****************************
    //** Tutaj rozpoczyna się    **
    //** algorytm Edmondsa-Karpa **
    //*****************************
    fmax := 0;
    while true do
    begin
      for i := 0 to n+1 do
        P[i] := -1;
      P[n] := -2;
      CFP[n] := MAXINT;
      while not Q.empty do
        Q.pop;
      Q.push(n);
      esc := false;
      while not Q.empty do
      begin
        v := Q.front; Q.pop;
        for u := 0 to n+1 do
        begin
          cp := C[v][u] - F[v][u];
          if (cp <> 0) and
             (P[u] = -1) then
          begin
            P[u] := v;
            if CFP[v] > cp then
              CFP[u] := cp
            else
              CFP[u] := CFP[v];
            if u = n+1 then
            begin
              inc(fmax, CFP[n+1]);
              i := u;
              while i <> n do
              begin
                v := P[i];
                inc(F[v][i],CFP[n+1]);
                dec(F[i][v],CFP[n+1]);
                i := v;
              end;
              esc := true; break;
            end;
            Q.push(u);
          end;
        end;
        if esc then break;
      end;
      if not esc then break;
    end;
    // Wyświetlamy wyniki
    writeln;
    writeln('NUMBER OF MATCHES = ',
            fmax);
    writeln;
    if fmax > 0 then
      for v := 0 to n-1 do
        for u := 0 to n-1 do
          if (C[v][u] = 1) and
             (F[v][u] = 1) then
            writeln(v, '-', u);
  end
  else
  begin
    writeln;
    writeln('NOT A BIBARTITE GRAPH');
  end;
  writeln;
  // Usuwamy dane dynamiczne
  //------------------------
  // Tablica kolorów wierzchołków
  SetLength(Color, 0);
  for i := 0 to n-1 do
  begin
    pr := graf[i];
    while pr <> nil do
    begin
      rr := pr;
      pr := pr^.next;
      dispose(rr);
    end;
  end;
  // Tablica list sąsiedztwa
  SetLength(graf, 0);
  for i := 0 to n+1 do
  begin
    SetLength(C[i], 0);
    SetLength(F[i], 0);
  end;
  // Macierz przepustowości
  // krawędzi
  SetLength(C, 0);
  // Macierz przepływów netto
  SetLength(F, 0);
  // Tablica poprzedników
  SetLength(P, 0);
  // Tablica przepustowości
  // rezydualnych
  SetLength(CFP, 0);
end.
C++
// Maksymalne skojarzenia
// Data: 26.10.2014
// (C)2014 mgr Jerzy Wałaszek
//---------------------------

#include <iostream>

using namespace std;

const int MAXINT = 2147483647;

// Definicja typu
// elementów listy
//----------------
struct SLel
{
  SLel * next;
  int data;
};

// Definicja typu
// obiektowego queue
//------------------
class queue
{
  private:
    SLel * head;
    SLel * tail;

  public:
    // konstruktor
    queue();
    // destruktor
    ~queue();
    bool empty(void);
    int  front(void);
    void push(int v);
    void pop(void);
};

//---------------------
// Metody obiektu queue
//---------------------

// Konstruktor
//------------
queue::queue()
{
  head = tail = nullptr;
}

// Destruktor
//-----------
queue::~queue()
{
  while(head) pop();
}

// Sprawdza,
// czy kolejka jest pusta
//-----------------------
bool queue::empty(void)
{
  return !head;
}

// Zwraca początek kolejki.
// Wartość specjalna to -MAXINT
//-----------------------------
int queue::front(void)
{
  if(head)
    return head->data;
  else
    return -MAXINT;
}

// Zapisuje do kolejki
//--------------------
void queue::push(int v)
{
  SLel * p = new SLel;
  p->next = nullptr;
  p->data = v;
  if(tail)
    tail->next = p;
  else
    head = p;
  tail = p;
}

// Usuwa z kolejki
//----------------
void queue::pop(void)
{
  if(head)
  {
    SLel * p = head;
    head = head->next;
    if(!head)
      tail = nullptr;
    delete p;
  }
}

//---------------
// Program główny
//---------------

// Kolejka
queue Q;
// Tablica kolorów
// wierzchołków
int *Color;
// Tablica list sąsiedztwa
SLel **graf;
// Macierz przepustowości
int **C;
// Macierz przepływów netto
int **F;
// Tablica poprzedników
int *P;
// Tablica przepustowości
// rezydualnych
int *CFP;
// Zmienne proste algorytmu
int n,m,xfmax,cp,v,u,i,j;
// Do wychodzenia
// z zagnieżdżonych pętli
bool esc;
// Wskaźniki elementów listy
SLel *pr, *rr;

// Funkcja zwraca true,
// jeśli graf jest dwudzielny.
// Ustala kolory wierzchołków
// w tablicy Color
//----------------------------
bool isBipartite()
{
  // Przechodzimy przez
  // kolejne wierzchołki
  for(int i = 0; i < n; i++)
    // Szukamy wierzchołka szarego
    if(!Color[i])
    {
      // Wierzchołek startowy
      // kolorujemy na czerwono
      Color[i] = 1;
      // i umieszczamy w kolejce
      Q.push(i);
      // Przejście BFS
      while(!Q.empty())
      {
        // Pobieramy wierzchołek
        // z kolejki
        v = Q.front();
        // Pobrany wierzchołek
        // usuwamy z kolejki
        Q.pop();
        // Przeglądamy sąsiadów
        // wierzchołka v
        for(pr = graf[v];
            pr; pr = pr->next)
        {
          // pobieramy z listy
          // sąsiedztwa numer
          // sąsiada
          u = pr->data;
          if(Color[u] == Color[v])
            return false;
          // Jeśli wierzchołek nie
          // jest odwiedzony,
          if(!Color[u])
          {
            // kolorujemy go
            // na kolor przeciwny
            Color[u] = -Color[v];
            // i umieszczamy
            // w kolejce
            Q.push(u);
          }
        }
      }
    }
  return true;
}

int main()
{
  // Ze standardowego
  // wejścia odczytujemy
  // n - liczbę wierzchołków
  //     w grafie sieci
  // m - liczbę krawędzi
  cin >> n >> m;
  // Tworzymy dane dynamiczne
  // Tablica kolorów wierzchołków
  Color = new int [n];
  // Tablica list sąsiedztwa
  graf = new SLel * [n];
  for(i = 0; i < n; i++)
  {
    graf[i] = nullptr;
    Color[i] = 0;
  }
  // Macierz przepustowości
  //krawędzi
  C = new int * [n+2];
  // Macierz przepływów netto
  F = new int * [n+2];
  for(i = 0; i <= n+1; i++)
  {
    C[i] = new int [n+2];
    F[i] = new int [n+2];
    for(j = 0; j <= n+1; j++)
      F[i][j] = C[i][j] = 0;
  }
  // Tablica poprzedników
  P = new int [n+2];
  // Tablica przepustowości
  // rezydualnych
  CFP = new int [n+2];
  // Odczytujemy definicje
  // krawędzi i dodajemy
  // je do list sąsiedztwa
  for(i = 0; i < m; i++)
  {
    cin >> v >> u;
    pr = new SLel;
    pr->data = u;
    pr->next = graf[v];
    graf[v]  = pr;
    pr = new SLel;
    pr->data = v;
    pr->next = graf[u];
    graf[u]  = pr;
  }
  if(isBipartite())
  {
    // Ustawiamy macierz
    // przepustowości
    for(i = 0; i < n; i++)
      if(Color[i] == -1)
      {
        // Przeglądamy listę
        // sąsiadów wierzchołka
        // niebieskiego
        for(pr = graf[i];
            pr; pr = pr->next)
          // Przepustowość krawędzi
          // do wierzchołka
          // czerwonego
          C[i][pr->data] = 1;
        // Przepustowość krawędzi
        // od źródła
        C[n][i] = 1;
      }
      else
        // Przepustowość krawędzi
        // do ujścia
        C[i][n+1] = 1;

    //*****************************
    //** Tutaj rozpoczyna się    **
    //** algorytm Edmondsa-Karpa **
    //*****************************

    xfmax = 0;
    while(true)
    {
      for(i = 0; i <= n+1; i++)
        P[i] = -1;

      P[n] = -2;
      CFP[n] = MAXINT;
      while(!Q.empty()) Q.pop();
      Q.push(n);
      esc = false;
      while(!Q.empty())
      {
        v = Q.front(); Q.pop();
        for(u = 0; u <= n+1; u++)
        {
          cp = C[v][u] - F[v][u];
          if(cp && (P[u] == -1))
          {
            P[u] = v;
            if(CFP[v] > cp)
              CFP[u] = cp;
            else
              CFP[u] = CFP[v];
            if(u == n+1)
            {
              xfmax += CFP[n+1];
              i = u;
              while(i != n)
              {
                v = P[i];
                F[v][i] += CFP[n+1];
                F[i][v] -= CFP[n+1];
                i = v;
              }
              esc = true; break;
            }
            Q.push(u);
          }
        }
        if(esc) break;
      }
      if(!esc) break;
    }

    // Wyświetlamy wyniki

    cout << endl
         << "NUMBER OF MATCHES = "
         << xfmax << endl << endl;
    if(xfmax > 0)
      for(v = 0; v < n; v++)
        for(u = 0; u < n; u++)
          if((C[v][u] == 1) &&
             (F[v][u] == 1))
            cout << v << "-"
                 << u << endl;
  }
  else
    cout << endl
         << "NOT A BIBARTITE GRAPH"
         << endl;
  cout << endl;
  // Usuwamy dane dynamiczne
  // Tablica kolorów wierzchołków
  delete [] Color;
  // Tablica list sąsiedztwa
  for(i = 0; i < n; i++)
  {
    pr = graf[i];
    while(pr)
    {
      rr = pr;
      pr = pr->next;
      delete rr;
    }
  }
  delete [] graf;
  for(i = 0; i <= n+1; i++)
  {
    delete [] C[i];
    delete [] F[i];
  }
  // Macierz przepustowości
  // krawędzi
  delete [] C;
  // Macierz przepływów netto
  delete [] F;
  // Tablica poprzedników
  delete [] P;
  // Tablica przepustowości
  // rezydualnych
  delete [] CFP;
  return 0;
}
Basic
' Maksymalne skojarzenia
' Data: 26.10.2014
' (C)2014 mgr Jerzy Wałaszek
'---------------------------

Const MAXINT = 2147483647

' Definicja typu elementów listy
'-------------------------------
Type SLel
  next As SLel Ptr
  Data As Integer
End Type

' Definicja typu obiektowego queue
'---------------------------------
Type queue
  Private:
    head As SLel Ptr
    tail As SLel Ptr

  Public:
    Declare Constructor
    Declare Destructor
    Declare Function empty As Integer
    Declare Function front As Integer
    Declare Sub push(ByVal x As Integer)
    Declare Sub pop
End Type

'---------------------
' Metody obiektu queue
'---------------------

' Konstruktor
'------------
Constructor queue
  head = 0
  tail = 0
End Constructor

' Destruktor
'-----------
Destructor queue
  While empty = 0
    pop
  Wend
End Destructor

' Sprawdza, czy kolejka jest pusta
'---------------------------------
Function queue.empty As Integer
  If head = 0 Then Return 1
  Return 0
End Function

' Zwraca początek kolejki.
' Wartość specjalna to -MAXINT
'-----------------------------
Function queue.front As Integer
  If head = 0 then
    front = -MAXINT
  Else
    front = head->data
  End If
End Function

' Zapisuje do kolejki
'--------------------
Sub queue.push(ByVal v As Integer)
  Dim p As SLel Ptr
  p = new SLel
  p->next = 0
  p->data = v
  If tail = 0 Then
    head = p
  Else
    tail->next = p
  End If
  tail = p
End Sub

' Usuwa z kolejki
'----------------
Sub queue.pop()
  Dim p As SLel Ptr
  If head Then
    p = head
    head = head->next
    If head = 0 Then tail = 0
    Delete p
  End If
End Sub

'---------------
' Program główny
'---------------

' Kolejka
Dim Shared As queue Q
' Tablice kolorów,
' poprzedników
' i przepustowości rezydualnych
Dim Shared As Integer Ptr VColor,P,CFP
' Tablica list sąsiedztwa
Dim Shared As SLel Ptr Ptr graf
' Macierze przepustowości
' i przepływów netto
Dim As Integer Ptr Ptr C,F
' Zmienne proste algorytmu
Dim Shared As Integer n,m,fmax,cp,v,u,i,j
' Do wychodzenia z zagnieżdżonych pętli
Dim As Byte esc
' Wskaźniki elementów listy
Dim Shared As SLel Ptr pr,rr

' Funkcja zwraca true,
' jeśli graf jest dwudzielny.
' Ustala kolory wierzchołków
' w tablicy Color
'---------------------------
Function isBipartite As Byte
  ' Przechodzimy przez
  ' kolejne wierzchołki
  For i = 0 To n-1
    ' Szukamy wierzchołka szarego
    If VColor[i] = 0 Then
      ' Wierzchołek startowy
      ' kolorujemy na czerwono
      VColor[i] = 1
      ' i umieszczamy w kolejce
      Q.push(i)
      ' Przejście BFS
      While Q.empty = 0
        ' Pobieramy wierzchołek z kolejki
        v = Q.front
        ' Pobrany wierzchołek
        ' usuwamy z kolejki
        Q.pop
        pr = graf[v]
        ' Przeglądamy sąsiadów
        ' wierzchołka v
        While pr <> 0
          ' pobieramy z listy
          ' sąsiedztwa numer sąsiada
          u = pr->data
          If VColor[u] = VColor[v] Then _
            return 0
          ' Jeśli wierzchołek
          ' nie jest odwiedzony,
          If VColor[u] = 0 Then 
            ' kolorujemy go
            ' na kolor przeciwny
            VColor[u] = -VColor[v]
            ' i umieszczamy w kolejce
            Q.push(u)
          End If
          pr = pr->next
        Wend
      Wend
    End If
  Next
  return 1
End Function

Open Cons For Input As #1
' Ze standardowego wejścia odczytujemy
' n - liczbę wierzchołków w grafie sieci
' m - liczbę krawędzi
Input #1, n, m
' Tworzymy dane dynamiczne
' Tablica kolorów wierzchołków
VColor = New Integer [n]
' Tablica list sąsiedztwa
graf = New SLel Ptr [n]
For i = 0 To n-1
  graf[i] = 0
  VColor[i] = 0
Next
' Macierz przepustowości krawędzi
C = New Integer Ptr [n+2]
' Macierz przepływów netto
F = New Integer Ptr [n+2]
For i = 0 To n+1
  C[i] = New Integer [n+2]
  F[i] = New Integer [n+2]
  For j = 0 To n+1
    C[i][j] = 0
    F[i][j] = 0
  Next
Next
' Tablica poprzedników
P = New Integer [n+2]
' Tablica przepustowości rezydualnych
CFP = New Integer [n+2]
' Odczytujemy definicje krawędzi
' i dodajemy je do list sąsiedztwa
For i = 1 To m
  Input #1, v, u
  pr = New SLel
  pr->data = u
  pr->next = graf[v]
  graf[v]  = pr
  pr = New SLel
  pr->data = v
  pr->next = graf[u]
  graf[u]  = pr
Next
If isBipartite() = 1 Then
  ' Ustawiamy macierz przepustowości
  For i = 0 To n-1
    If VColor[i] = -1 Then
      pr = graf[i]
      ' Przeglądamy listę sąsiadów
      ' wierzchołka niebieskiego
      While pr <> 0
        ' Przepustowość krawędzi
        ' do wierzchołka czerwonego
        C[i][pr->data] = 1
        pr = pr->next
      Wend 
      ' Przepustowość krawędzi od źródła
      C[n][i] = 1
    Else
      ' Przepustowość krawędzi do ujścia
      C[i][n+1] = 1
    End If
  Next

  '*****************************
  '** Tutaj rozpoczyna się    **
  '** algorytm Edmondsa-Karpa **
  '*****************************

  fmax = 0
  While 1
    For i = 0 To n+1
      P[i] = -1
    Next
    P[n] = -2
    CFP[n] = MAXINT
    While Q.empty = 0
      Q.pop
    Wend
    Q.push(n)
    esc = 0
    While Q.empty = 0
      v = Q.front
      Q.pop
      For u = 0 To n+1
        cp = C[v][u]-F[v][u]
        if (cp > 0) AndAlso _
           (P[u] = -1) Then
          P[u] = v
          If CFP[v] > cp Then
          	CFP[u] = cp
          Else
          	CFP[u] = CFP[v]
          EndIf
          If u = n+1 Then
            fmax += CFP[n+1]
            i = u
            While i <> n
              v = P[i]
              F[v][i] += CFP[n+1]
              F[i][v] -= CFP[n+1]
              i = v
            Wend
            esc = 1
            Exit For
          End If
          Q.push(u)
        End If
      Next
      If esc = 1 Then Exit While
    Wend 
    If esc = 0 Then Exit While
  Wend
  ' Wyświetlamy wyniki
  Print
  Print "NUMBER OF MATCHES =";fmax
  Print
  If fmax > 0 Then
    For v = 0 To n-1
      For u = 0 To n-1
        if (C[v][u] = 1) AndAlso _
           (F[v][u] = 1) Then
          Print Using "&-&";v;u
        End If
      Next
    Next
  End If
Else
  Print
  Print "NOT A BIBARTITE GRAPH"
End If
Print
' Usuwamy dane dynamiczne
'------------------------
' Tablica kolorów wierzchołków
Delete [] VColor
For i = 0 To n-1
  pr = graf[i]
  While pr <> 0
    rr = pr
    pr = pr->next
    Delete rr
  Wend
Next
' Tablica list sąsiedztwa
Delete [] graf
For i = 0 To n+1
  Delete [] C[i]
  Delete [] F[i]
Next
' Macierz przepustowości krawędzi
Delete [] C
' Macierz przepływów netto
Delete [] F
' Tablica poprzedników
Delete [] P
' Tablica przepustowości rezydualnych
Delete [] CFP
End
Python (dodatek)
# Maksymalne skojarzenia
# Data: 9.04.2025
# (C)2025 mgr Jerzy Wałaszek
#---------------------------

MAXINT = 2147483647

# Klasa elementów listy
class SLel:
    def __init__(self):
        self.next = None
        self.data = 0

# Klasa obiektu Queue
class Queue:
    # Konstruktor
    def __init__(self):
        self.head = None
        self.tail = None

    # Destruktor
    def __del__(self):
        while not self.empty():
            self.pop()

    # Sprawdza,
    # czy kolejka jest pusta
    def empty(self):
        return not self.head

    # Zwraca początek kolejki.
    # Wartość specjalna to -MAXINT
    def front(self):
        global MAXINT
        if not self.head:
            return -MAXINT
        else:
            return self.head.data

    # Zapisuje do kolejki
    def push(self, v):
        p = SLel()
        p.next = None
        p.data = v
        if not self.tail:
            self.head = p
        else:
            self.tail.next = p
        self.tail = p

    # Usuwa z kolejki
    def pop(self):
        if self.head:
            self.head = self.head.next
            if not self.head:
                self.tail = None

#---------------
# Program główny
#---------------

# Kolejka
q = Queue()

# Funkcja zwraca true,
# jeśli graf jest dwudzielny.
# Ustala kolory wierzchołków
# w tablicy Color
def is_bipartite():
    global n,vcolor,q,graf
    # Przechodzimy przez
    # kolejne wierzchołki
    for i in range(n):
        # Szukamy wierzchołka szarego
        if not vcolor[i]:
            # Wierzchołek startowy
            # kolorujemy na czerwono
            vcolor[i] = 1
            # i umieszczamy w kolejce
            q.push(i)
            # Przejście BFS
            while not q.empty():
                # Pobieramy wierzchołek z kolejki
                v = q.front()
                # Pobrany wierzchołek
                # usuwamy z kolejki
                q.pop()
                pr = graf[v]
                # Przeglądamy sąsiadów
                # wierzchołka v
                while pr:
                    # pobieramy z listy
                    # sąsiedztwa numer sąsiada
                    u = pr.data
                    if vcolor[u] == vcolor[v]:
                        return False
                    # Jeśli wierzchołek
                    # nie jest odwiedzony,
                    if not vcolor[u]:
                        # kolorujemy go
                        # na kolor przeciwny
                        vcolor[u] = -vcolor[v]
                        # i umieszczamy w kolejce
                        q.push(u)
                    pr = pr.next
    return True

# Ze standardowego wejścia odczytujemy
# n — liczbę wierzchołków w grafie sieci
# m — liczbę krawędzi
x = input().split()
n = int(x[0])
m = int(x[1])
# Tworzymy dane dynamiczne
# Tablica kolorów wierzchołków
vcolor = [0] * n
# Tablica list sąsiedztwa
graf = [None] * n
# Macierz przepustowości krawędzi
c = [[0 for j in range(n+2)] for i in range(n+2)]
# Macierz przepływów netto
f = [[0 for j in range(n+2)] for i in range(n+2)]
# Tablica poprzedników
p = [0] * (n+2)
# Tablica przepustowości rezydualnych
cfp = [0] * (n+2)
# Odczytujemy definicje krawędzi
# i dodajemy je do list sąsiedztwa
for i in range(m):
    x = input().split()
    v = int(x[0])
    u = int(x[1])
    pr = SLel()
    pr.data = u
    pr.next = graf[v]
    graf[v] = pr
    pr = SLel()
    pr.data = v
    pr.next = graf[u]
    graf[u] = pr
if is_bipartite():
    # Ustawiamy macierz przepustowości
    for i in range(n):
        if vcolor[i] == -1:
            pr = graf[i]
            # Przeglądamy listę sąsiadów
            # wierzchołka niebieskiego
            while pr:
                # Przepustowość krawędzi
                # do wierzchołka czerwonego
                c[i][pr.data] = 1
                pr = pr.next
            # Przepustowość krawędzi od źródła
            c[n][i] = 1
        else:
            # Przepustowość krawędzi do ujścia
            c[i][n+1] = 1

    #*****************************
    #** Tutaj rozpoczyna się    **
    #** algorytm Edmondsa-Karpa **
    #*****************************

    fmax = 0
    while True:
        for i in range(n+2):
            p[i] = -1
        p[n] = -2
        cfp[n] = MAXINT
        while not q.empty():
            q.pop()
        q.push(n)
        esc = False
        while not q.empty():
            v = q.front()
            q.pop()
            for u in range(n+2):
                cp = c[v][u] - f[v][u]
                if (cp > 0) and (p[u] == -1):
                    p[u] = v
                    if cfp[v] > cp:
                        cfp[u] = cp
                    else:
                        cfp[u] = cfp[v]
                    if u == n+1:
                        fmax += cfp[n+1]
                        i = u
                        while i != n:
                            v = p[i]
                            f[v][i] += cfp[n+1]
                            f[i][v] -= cfp[n+1]
                            i = v
                        esc = True
                        break
                    q.push(u)
            if esc: break
        if not esc: break
    # Wyświetlamy wyniki
    print()
    print("NUMBER OF MATCHES =",fmax)
    print()
    if fmax > 0:
        for v in range(n):
            for u in range(n):
                if (c[v][u] == 1) and \
                   (f[v][u] == 1):
                    print(v,'-',u,sep="")
else:
    print()
    print("NOT A BIBARTITE GRAPH")
print()
# Usuwamy dane dynamiczne
#------------------------
# Tablica kolorów wierzchołków
del vcolor
for i in range(n):
    while graf[i]:
        graf[i] = graf[i].next
# Tablica list sąsiedztwa
del graf
for i in range(n+2):
    c[i] = None
    f[i] = None
# Macierz przepustowości krawędzi
del c
# Macierz przepływów netto
del f
# Tablica poprzedników
del p
# Tablica przepustowości rezydualnych
del cfp
Wynik:
10 17
0 2
0 3
0 4
0 8
1 2
1 4
1 8
2 5
2 7
3 7
3 9
4 5
4 9
5 6
6 7
6 9
8 9

NUMBER OF MATCHES = 5

2-0
3-7
4-1
6-5
8-9
 obrazek

do podrozdziału  do strony 

Zespół Przedmiotowy
Chemii-Fizyki-Informatyki

w I Liceum Ogólnokształcącym
im. Kazimierza Brodzińskiego
w Tarnowie
ul. Piłsudskiego 4
©2026 mgr Jerzy Wałaszek

Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone pod warunkiem podania źródła oraz niepobierania za to pieniędzy.
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.

Informacje dodatkowe.