Олександр Рудик

Пошук максимального потоку


(Інформатика та інформаційні технології в навчальних закладах, 2008, № 6, с. 94–98)

Означення 1. Запровадимо такі поняття й позначення.

  1. Для довільної вершини v:

    • in(v) — множина дуг з кінцевою вершиною v;
    • out(v) — множина дуг з початковою вершиною v.

  2. Мережею називають орієнтований граф (G, V, E ) разом з ваговою функцією с: E  N (з множини ребер E у множину натуральних чисел N) та вершинами a, z, що мають відповідно нульові степені входу й виходу:

    • жодна дуга не закінчується у вершині a;
    • жодна дуга не починається у вершині z.

    | in (a) | = 0 = | out (z) | — стислий запис останніх двох висловлювань.

  3. Потоком у мережі (G, V, E) називають функцію f: E  N0 (з множини ребер E у множину невід'ємних цілих чисел N0), при якій:

    • для довільної дуги e справджуються нерівності: 0 ≤ f (e) ≤ c(e);
    • для довільної вершини, відмінної від a та z, маємо:

      f (e)    =f (e).
      e ∈ out (v)e ∈ in (v)

      Мережа є моделлю водогону, у якій:
c(e) — максимальна швидкість транспортування у напрямку, поданому дугою e;
f (e) — (реальна) швидкість транспортування у напрямку, поданому дугою e.
      Рівність сум в означенні потоку описує відсутність накопичення рідини на проміжних станціях, поданих вершинами графа.

Теорема 1. Для довільного потоку f маємо:

f (e)    =f (e),
e ∈ out (a) e ∈ in (z)

тобто сумарні потоки через джерело a і стік z збігаються.

Доведення. Нехай S — довільна підмножина V, що містить a, але не містить z, T = V \ S. Додавши почастинно рівності з означення потоку за всіма вершинами S, відмінними від a, отримаємо еквівалентні рівності:

f (e)    = f (e);
vS \ {a}   e ∈ out (v) vS \ {a}   e ∈ in (v)

f (e)    – f (e)    =  0;
vS \ {a}   e ∈ out (v) vS \ {a}   e ∈ in (v)

f (e)    – f (e)    = f (e).
vS   e ∈ out (v) vS   e ∈ in (v) e ∈ out (a)

Позначимо через (ST) множину дуг, спрямованих з S у T, через (TS) — множину дуг, спрямованих з T в S. У лівій частині останньої рівності, якщо обидві вершини дуги e належать до S, то потік по e буде враховано в обох сумах, а відповідні доданки взаємно знищаться:

Маємо:

f (e)    – f (e)    = f (e).
e ∈ (S; T) e ∈ (T; S) e ∈ out (a)

Отже, для довільної множини вершин S, що містить a і не містить z, різниця потоків, які виходять з S і входять в S дорівнює потоку, що витікає з a.

Виберемо: S = V \{z}, T = {z}, при яких:

Врахувавши все це, маємо:

f (e)    =f (e),
e ∈ in (z) e ∈ out (a)

що й потрібно було довести.

Означення 2. Запровадимо такі поняття й позначення:

  1. Величиною потоку f називають величину:

    f (e)    =f (e),
    e ∈ out (a) e ∈ in (z)

    яку позначають через val ( f ).

  2. Нехай S — довільна підмножина V, T = V \ S. Перерізом (ST ) називають множину дуг, спрямованих з S у T. Якщо S містить джерело a і T містить стік z, то такий переріз називають a – z перерізом.

  3. Величину

    С(S, T )   = c(e)
    e ∈(S; T)

    називають пропускною спроможністю перерізу.

  4. Потік fmax називають максимальним, якщо його величина не менша від величини будь-якого можливого потоку f у мережі: val ( f ) ≤ val ( fmax).

  5. a – z переріз (ST ) називають мінімальним перерізом, якщо С(ST ) не перевищує пропускну спроможність довільного a – z перерізу.

Теорема 2. Нехай f — довільний потік у мережі S — довільна підмножина V, що містить джерело a і не містить стік z, T = V \ S. Тоді val ( f ) ≤ С(ST ).

Доведення. Як встановлено вище, величина потоку val ( f ) дорівнює такій різниці:

f (e)    – f (e)    ≤ f (e)    ≤ c (e)    = С(S, T).
e ∈ (S; T) e ∈ (T; S) e ∈ (S; T) e ∈ (S; T)

Наслідок 1. Якщо val ( f ) = С(ST ) при деяких потоці f та a – z перерізі (ST ), то f — максимальний потік, (ST ) — мінімальний переріз.

Наслідок 2. Рівність val ( f ) = С(ST ) справджується тоді й лише тоді, коли:

e ∈ (S; T )    f (e) = c(e);
e ∈ (T; S )    f (e) = 0.

Пошук максимального потоку здійснимо шляхом збільшення потоку, починаючи з нульового, таким чином. Формуємо ланцюг — послідовність дуг, що сполучають вершини a – z без урахування напряму дуг. Для кожного такого ланцюга збільшуємо загальний потік, якщо це можливо, змінивши потік лише вздовж ланок ланцюга:

Кожній вершині поставимо у відповідність впорядковану пару, в якій:

Алгоритм Форда-Фалкерсона
(Ford-Fulkerson algorithm)

  1. Встановлюємо, що кожної вершини її попередник невизначений і резерв від'ємний (невизначений). Означимо резерв для вершини a як ∞, щоб не обмежувати резерв решти вершин. Покладемо A = {a}.

  2. Якщо A — порожня множина, то припиняємо виконання алгоритму, бо потік максимізовано. Інакше вибираємо довільну вершину v з множини A і вилучаємо її з A.

  3. Розглянемо всі вершини w, що задовольняють такі умови:

    • (v; w) є дугою;
    • f ((v; w)) < с((v; w)).

    Для кожної такої вершини w обчислимо мінімум з різниці с((vw)) – f ((vw)) та резерву вершини v. Якщо обчислена на попередньому кроці величина більша за резерв вершини w, то:

    • замінюємо величину резерву w на цю величину;
    • (пере)означимо попередника w — вершину v;
    • якщо w відмінна від z, то долучаємо w до множини A.
  4. Розглянемо всі вершини w, що задовольняють такі умови:

    • (w; v) є дугою;
    • 0 < f ((w; v)).

    Для кожної такої вершини w обчислимо мінімум з f ((v;w)) та резерву вершини v. Якщо обчислена на попередньому кроці величина більша за резерв вершини w, то:

    • замінюємо величину резерву w на цю величину;
    • (пере)означимо попередника w — вершину v;
    • якщо w відмінна від z, то долучаємо w до множини A.
  5. Якщо резерв і попередник вершини z не визначено, то переходимо до виконання пункту 2.

  6. Якщо резерв і попередник вершини z визначено, то:

    • використовуючи попередників вершин, відновлюємо ланцюг a – z;

    • для кожної дуги ланцюга, орієнтація якої збігається з напрямком руху від a до z вздовж ланцюга, збільшуємо потік на визначений резерв вершини z;

    • для кожної дуги ланцюга, орієнтація якої протилежна до напрямку руху від a до z вздовж ланцюга, зменшуємо потік на визначений резерв вершини z;

    • переходимо до виконання пункту 1.

Теорема 3. Алгоритм Форда-Фалкерсона будує максимальний потік у мережі.

Доведення. Позначимо через S множину всіх тих вершин, для яких визначено резерв під час останнього проходження алгоритму, T = V \ S. Множина S непорожня, бо містить вершину a. При цьому:

Згідно з наслідками 1–2 f є максимальним потоком, а (ST ) — мінімальним перерізом.

Наслідок 3. Потік f у мережі є максимальним тоді й лише тоді, коли існує переріз (S; T), при якому

val ( f ) = С(S, T).

Подамо приклад програми мовою Turbo Pascal 7.0 з коментарями щодо стуктури вхідних і вихідних даних.

{$I+}{$N+}                 {Верхнi межi:}
const nv_max=2000;     {кiлькостi вершин}
      l_max=2147483647;          {потоку}
type  pointe=^edge;
      edge=record                 {Дуга:}
      v,                        {початок}
      w: word;                   {кiнець}
      c,         {пропускна спроможнiсть}
      f: longint;                 {потiк}
             {вказiвник на наступну дугу}
      b,           {з тим самим початком}
      e: pointe end;         {... кiнцем}

      pointa=^forsetA;
      forsetA=record   {Елемент списку A}
      v: word;                  {вершина}
      n: pointa end;  {наступний елемент}
   pointer=array[1..nv_max] of pointe;
   longers=array[1..nv_max] of longint;
var p:^pointer;    {Дуги з попередниками}
    r:^longers;          {Резерви вершин}
   n0,            {Вказiвники на першу й}
   n,     {останню дуги з даним початком}
   m0,            {Вказiвники на першу й}
m:^pointer; {останню дуги з даним кiнцем}
  A1,           {Перший елемент списку A}
  A ,         {Поточний елемент списку A}
  Aw: pointa;        {Елемент w списку A}
   e: pointe;                     {Ребро}
  nv,                  {Кiлькiсть вершин}
   aa,                          {Джерело}
    z,                             {Стiк}
  v,w: word;      {Дуга: початок, кiнець}
    c,         {i пропускна спроможнiсть}
   rw: longint; {Можливий резерв вершини}
    o: text;                 {Файл даних}
 stop: boolean; {Потрiбно припинити цикл}

       {Долучення вершини w до множини A}
procedure includew;                 BEGIN
if A1=nil then begin
new(A1);  {A порожня}
A1^.v:=w;
A1^.n:=nil       end           else begin
A:=A1;    {A не порожня}
stop:=false;
while (A^.v<>w) and (A^.n<>nil)do A:=A^.n;
if     A^.v<>w then                 begin
new(Aw);    {Якщо w не належить до A}
 A^.n:=Aw;
Aw^.v:=w;
Aw^.n:=nil                    end end END;

 {Запис у вихiдний файл даних про потiк:
 у кожному рядку - початок i кiнець дуги
 й потiк вздовж неї. Дуги перераховано у
 порядку зростання початку, а для сталого
 початку - у тому самому порядку, як вони
           зустрiчалися у вхiдному файлi}
procedure output;                   BEGIN
assign (o,'FLOW.RES');   rewrite(o);
for v:=1 to nv do                   begin
if n0^[v]<>nil then                 begin
e:=n0^[v];
stop:=false;
repeat writeln(o,v,' ',e^.w,' ',e^.f);
       if e^.b=nil then stop:=true
                   else e:=e^.b
until stop                        end end;
close(o);  halt                       END;
                                    BEGIN
nv:=0;  new(n0); new(n); new(m0); new(m);
for v:=1 to nv_max do               begin
n0^[v]:=nil;
m0^[v]:=nil                           end;
assign(o,'FLOW.DAT');   reset(o);
{Зчитування рядкiв вхiдного файлу, кожний
з яких мiстить: початок дуги, кiнець дуги
                i пропускну спроможнiсть}
REPEAT readln(o,v,w,c);
       if v>nv then nv:=v;
       if w>nv then nv:=w;
       new(e);
       e^.v:=v;
       e^.w:=w;
       e^.c:=c;
       e^.f:=0;
       e^.b:=nil;
       e^.e:=nil;
                 {Облiк дуг з початком v}
       if n0^[v]=nil then n0^[v]   :=e
                     else  n^[v]^.b:=e;
       n^[v]:=e;
                   {Облiк дуг з кiнцем w}
       if m0^[w]=nil then m0^[w]   :=e
                     else  m^[w]^.e:=e;
       m^[w]:=e;
UNTIL  seekeof(o);        close(o);
new(p);   new(r);   new(A1);
aa:=1;                          {Джерело}
z:=nv;                             {Стiк}
REPEAT                           {Крок 1}
for v:=1 to nv do begin
p^[v]:=nil;
r^[v]:=-1           end;
r^[aa]:=l_max;
A1^.v:=aa;
A1^.n:=nil;
REPEAT   if A1=nil then output else begin
                              {Кроки 2-5}
v:=A1^.v;    {Вилучення першого елемента
                             зi списку A}
if A1^.n=nil then A1:=nil
             else          begin
                   A:=A1;
                  A1:=A1^.n;
                  dispose(A) end;
n^[v]:=n0^[v];
stop:=false;
repeat             {Розгляд дуг (v;w)}
  w:=n^[v]^.w;
  if n^[v]^.f < n^[v]^.c       then begin
     rw:=n^[v]^.c-n^[v]^.f     ;
     if r^[v] < rw then rw:=r^[v];
     if r^[w] < rw then             begin
        r^[w]:=rw;
        p^[w]:=n^[v];
        if w<>z then includew     end end;
     if n^[v]^.b<>nil then n^[v]:=n^[v]^.b
                      else stop:=true;
until stop;
if m0^[v]<>nil then                 begin
   m^[v]:=m0^[v];
   stop:=false;
   repeat             {Розгляд дуг (w;v)}
     w:=m^[v]^.v;
     if  0 < m^[v]^.f          then begin
       rw:=m^[v]^.f;
        if r^[v] < rw then rw:=r^[v];
        if r^[w] < rw then          begin
           r^[w]:=rw;
           p^[w]:=m^[v];
           if w<>z then includew  end end;
     if m^[v]^.e<>nil then m^[v]:=m^[v]^.e
                      else stop:=true;
   until stop                     end end
UNTIL r^[z]>0;
w:=z;                            {Крок 6}
repeat e:=p^[w];
       if w =e^.w then   begin
          w:=e^.v;
          e^.f:=e^.f+r^[z] end else begin
          e^.f:=e^.f-r^[z];
          w:=e^.w                     end
until  w=aa;
UNTIL A1=nil;  output                 END.

Подана далі програма без динамічного розподілу пам'яті не увійшла до журнальної публікації, але може виявитися користною для першого знайомства з прикладом програмної реалізації.

const nv_=555;  ne_=555;
var                   b: array[0..nv_] of boolean;
                 p,r,ep: array[1..nv_] of word;
v1,v2,c,f,e1,e2,ne1,ne2: array[0..ne_] of word;
   a,z, nv,ne,j,k,v,w,d: word;
                      o: text;
label l1,l2;

procedure fin;
BEGIN
d:=0;
for j:=ne1[a-1]+1 to ne1[a] do d:=d+f[e1[j]];
writeln(o,d);
for j:=1 to ne do writeln(o,f[j]);
close(o);
halt
END;

function lt1(i,j: word): boolean;
BEGIN
lt1:=(v1[i]<v1[j]) or (v1[i]=v1[j]) and (v2[i]<v2[j]);
END;

procedure QSort1(l,r: word);
         var x,y,i,j: word;
BEGIN
x:=e1[l+random(r-l+1)];
i:=l; j:=r;
while i<=j do   begin{1}
  while lt1(e1[i],x) do i:=i+1;
  while lt1(x,e1[j]) do j:=j-1;
  if i<=j then  begin{2}
    y:=e1[i];
       e1[i]:=e1[j];
              e1[j]:=y;
    i:=i+1;
    j:=j-1        end{2}
                  end{1};
if l<j then qsort1(l,j);
if i<r then qsort1(i,r);
END;

function lt2(i,j: word): boolean;
BEGIN
lt2:=(v2[i]<v2[j]) or (v2[i]=v2[j]) and (v1[i]<v1[j]);
END;

procedure QSort2(l,r: word);
         var x,y,i,j: word;
BEGIN
x:=e2[l+random(r-l+1)];
i:=l; j:=r;
while i<=j do   begin{1}
  while lt2(e2[i],x) do i:=i+1;
  while lt2(x,e2[j]) do j:=j-1;
  if i<=j then  begin{2}
    y:=e2[i];
       e2[i]:=e2[j];
              e2[j]:=y;
    i:=i+1;
    j:=j-1        end{2}
                  end{1};
if l<j then qsort2(l,j);
if i<r then qsort2(i,r);
END;

BEGIN
assign(o,'flow.in');
reset(o);
readln(o,a,z);
ne:=0;
nv:=0;
repeat inc(ne);
       readln(o,v1[ne],v2[ne],c[ne]);
       if nv<v1[ne] then nv:=v1[ne];
       if nv<v2[ne] then nv:=v2[ne];
       f[ne]:=0
 until seekeof(o);
close(o);
assign(o,'flow.out');
rewrite(o);
for j:=1 to ne do begin
  e1[j]:=j;
  e2[j]:=j          end;
randomize;
qsort1(1,ne);
qsort2(1,ne);
ne1[0]:=0;  ne1[nv]:=ne;
ne2[0]:=0;  ne2[nv]:=ne;
for j:=1 to ne-1 do                             begin
  if v1[e1[j]]<v1[e1[j+1]] then ne1[v1[e1[j]]]:=j;
  if v2[e2[j]]<v2[e2[j+1]] then ne2[v2[e2[j]]]:=j end;
ne1[v1[e1[ne]]]:=ne;
ne2[v2[e2[ne]]]:=ne;
for j:=1 to nv-1 do                   begin
  if ne1[j]=0 then ne1[j]:=ne1[j-1];
  if ne2[j]=0 then ne2[j]:=ne2[j-1]     end;

{1} l1:

for j:=1 to nv do begin
  p[j]:=0;
  r[j]:=0;
  b[j]:=false       end;

r[a]:=65535;
b[a]:=true;

{2} l2:
v:=0;
repeat inc(v)
 until (b[v]) or (v=nv);

if not b[v] then fin;
b[v]:=false;

{3}
for j:=ne1[v-1]+1 to ne1[v] do
  if f[e1[j]]<c[e1[j]] then   begin
  d:=c[e1[j]]-f[e1[j]];
  if d>r[v] then d:=r[v];
  w:=v2[e1[j]];
  if d>r[w] then          begin
    r[w]:=d;
    p[w]:=v;
    ep[w]:=e1[j];
    if w<>z then b[w]:=true end end;

{4}
for j:=ne2[v-1]+1 to ne2[v] do
  if 0<f[e2[j]] then          begin
  d:=f[e1[j]];
  if d>r[v] then d:=r[v];
  w:=v1[e2[j]];
  if d>r[w] then          begin
    r[w]:=d;
    p[w]:=v;
    ep[w]:=e2[j];
    if w<>z then b[w]:=true end end;

{5}
if r[z]=0 then goto l2;

{6}
v:=z;

repeat if p[v]=v1[ep[v]] then f[ep[v]]:=f[ep[v]]+r[z] else
       if p[v]=v2[ep[v]] then f[ep[v]]:=f[ep[v]]-r[z] else
       v:=p[v]
 until v=a;
goto l1;
END.