Теоретический материал

Сайт: Информатикс
Курс: Алгоритмы на графах
Книга: Теоретический материал
Напечатано:: Гость
Дата: Пятница, 18 Июль 2025, 21:35

Поиск максимального паросочетания в двудольном графе

Граф называют двудольным, если множество его вершин можно разбить на 2 непересекающихся множества (каждая вершина должна обязательно войти в одно из этих множеств), причем каждое ребро графа начинается в первом из этих множеств, а заканчивается в другом. Максимальное паросочетание — это максимально возможное количество ребер, не имеющих общих концов.

В основной программе каждой вершине одного из множеств мы пытаемся найти допустимую пару (очевидно, что если каждой вершине пара будет найдена, то максимальное паросочетание построено). Делается это с помощью функции dfs. Если для вершины j свободной пары в противоположном множестве нет, то делается попытка построить чередующуюся цепочку, начинающуюся с j-й вершины.

const max=…{размер большего из двух множеств}

var new: array[1..max] of boolean;{смысл противоположен названию!!!}

  res: array[1..max] of integer;

  n, m, i, j, cnt: integer;

function dfs(j: integer): boolean;

{пытается найти вершине j пару}

var i: integer;

begin

  if new[j] then {j в текущем паросочетании уже просмотрена}

    begin dfs:= false; exit end;

  new[j]:= true;

  for i:= 1 to n do

    if a(i, j) {ребро между i и j существует} and

     ((res[i] = 0) {у i еще нет пары}

      or{пару i можно пристроить к другой вершине} dfs(res[i])) then

        begin

          dfs:=true; res[i]:=j; exit

        end;

  dfs:=false

end;

begin {Main}

 …{здесь вводим описание графа}

  fillchar(res,sizeof(res),0);

  cnt:=0;{счетчик ребер в паросочетании}

  for j:= 1 to m do

  {каждой вершине одного из множеств пытаемся найти пару}

    begin

      fillchar(new,sizeof(new),false);

      if dfs(j) then inc(cnt)

    end;

  writeln(cnt);

  for i:= 1 to n do

    if res[i] <> 0 then write(i,' ',res[i])

end.


Решение задачи о назначениях (Венгерский алгоритм)

В данной задаче требуется найти в двудольном графе полное паросочетание минимального веса. Не снижая общности мы можем считать, что такое паросочетание всегда существует (двудольный граф можно дополнить до полного ребрами с весом, превышающим сумму остальных весов ребер данного графа и при необходимости удалить подобные ребра из ответа).

Лемма 1. Если веса всех ребер графа, инцидентных какой-либо вершине, увеличить (уменьшить) на одно и то же число, то искомое паросочетание является искомым и в новом графе.

Одно из следствий – мы можем считать, что веса всех ребер у нас неотрицательные.

Лемма 2. Если веса всех ребер неотрицательные, и некоторое паросочетание состоит из ребер нулевого веса, то оно искомое.

Схема решения

  for i:= 1 to n do begin

  {каждой вершине одного из множеств находим назначение}

    for j:=1 to n do min[j]:=maxint;

    repeat

      fillchar(new,sizeof(new),false);

      f := dfs(i);{пару ищем только по ребрам нулевого веса}

      if not f then begin

1)      {ищем минимальный элемент в помеченных dfs строках new[i]=true

        среди элементов столбцов не вошедших в текущую цепочку

        (res[j] = 0) or (not new[res[j]])

         обозначим найденный элемент del}

2)      {перестраиваем матрицу так:

         из всех помеченных dfs строк вычитаем del

         ко всем вошедшим в текущую цепочку столбцам прибавляем del

      end;

    until f;

  end;

  for j:= 1 to n do

    writeln(j,' ',res[j])

1

3

2

3

2

1

3

6

4

2

6

8

4

5

2

7

0

2

1

2

2

1

3

6

4

2

6

8

4

5

2

7

0

2

1

2

1

0

2

5

4

2

6

8

4

5

2

7

0

2

1

2

1

0

2

5

2

0

4

6

4

5

2

7

0

3

1

2

0

0

1

4

1

0

3

5

4

6

2

7

0

3

0

1

0

0

0

3

1

0

2

4

4

7

2

7

и т.д.


 

 

 

 

 

Понятная программа решения с быстрым пересчетом

const max=100;{размер большего из двух множеств}

var new: array[0..max] of boolean;

  res, min, u, v: array[0..max] of integer;

  n, i, j, del: integer;

  f: boolean;

function a(i,j:integer):integer;

begin

  a:=c[j,i];{транспонированная матрица исходных данных}

end;

function dfs(i: integer): boolean;

{пытается найти вершине j пару}

var j: integer;

begin

  if new[i] then {j в текущем паросочетании уже просмотрена}

    begin dfs:= false; exit end;

  new[i]:= true;

  for j:= 1 to n do

    if a(i, j)-u[i]-v[j]=0 then

      if (res[j] = 0) or dfs(res[j]) then

        begin

          dfs:=true; res[j]:=i;  exit

        end

      else

    else

      if ((res[j] = 0) or (not new[res[j]])) and

         (a(i, j)-u[i]-v[j] < min[j]) then begin

        min[j]:=a(i, j)-u[i]-v[j];

        if del>min[j] then del:=min[j];

      end;

  dfs:=false;

end;

begin {Main}

  fillchar(res,sizeof(res),0);

  fillchar(u,sizeof(v),0);

  fillchar(v,sizeof(v),0);

  n :=100;

  for i:= 1 to n do begin

  {каждой вершине одного из множеств находим назначение}

    for j:=1 to n do min[j]:=maxint;

    repeat

      del:=maxint;

      fillchar(new,sizeof(new),false);

      f := dfs(i);{в эффективной программе заменяется на перестройку цепочки}

      if not f then begin

        for j:=1 to n do

          if (res[j]>0)and new[res[j]] then begin

             v[j]:=v[j]-del;

             u[res[j]]:=u[res[j]]+del

          end;

        u[i]:=u[i]+del;{в этой строке результата нет, но она помечена!!!}

      end;

    until f;

  end;

  for j:= 1 to n do

    writeln(j,' ',res[j])

end.


Программа решения за N^3

begin {Main}

  fillchar(res,sizeof(res),0);

  fillchar(u,sizeof(v),0);

  fillchar(v,sizeof(v),0);

  …{задание исходных данных}

  for i:= 1 to n do

  {каждой вершине одного из множеств находим назначение}

    begin

      for j:=1 to n do min[j]:=maxint;{минимальные значения столбцов}

      fillchar(new,sizeof(new),false);

      j0 := 0;

      res[0] := i;

      repeat

       del:=maxint;

       new[j0] := true;{в данной версии мы метим столбцы!!!}

       i0 := res[j0];

       for j := 1 to n do

         if not new[j] then begin

           if a(i0,j)-u[i0]-v[j] < min[j] then begin

            min[j] := a(i0,j)-u[i0]-v[j];

            way[j] := j0

           end;

           if min[j] < del then begin

            del := min[j];

            j1 := j

           end

         end;{нашли новое значение del}

       for j := 0 to n do

         if new[j] then begin

           u[res[j]] := u[res[j]] + del;

           v[j]:=v[j]-del;

         end

         else min[j] := min[j] - del;{пересчитываем минимумы}

         j0 := j1; {}

       until res[j0]=0; {свободный ноль позволит увеличить паросочетание}

    repeat {перестраиваем цепочку с учетом выбранного нуля}

      j1 := way[j0];

      res[j0] := res[j1];

      j0 := j1

    until j0 = 0;

  end;

  writeln(-v[0]); {здесь мы получили сумму всех назначений}

  for j:= 1 to n do

    writeln(a(res[j],j));

end.