Шаг 126.
Алгоритм нахождения максимального потока

    На этом шаге мы реализуем алгоритм нахождения максимального потока.

    Идея данного алгоритма состоит в нахождении сквозных путей с положительными потоками от источника к стоку.

    Рассмотрим ребро (i, j) с (начальной) пропускной способностью (Cij,Cji). В процессе выполнения алгоритма части этих пропускных способностей "забираются" потоками, проходящими через данное ребро, в результате каждое ребро будет иметь остаточную пропускную способность. Будем использовать запись (cij, cji) для представления остаточных пропускных способностей. Сеть, где все ребра имеют остаточную пропускную способность, назовем остаточной.

    Для произвольного узла j, получающего поток от узла i, определим метку [aj, i], где aj - величина потока, протекающего от узла i к узлу j. Алгоритм нахождения максимального потока предполагает выполнение следующих действий.

    Шаг 1. Для всех ребер (i, j) положим остаточную пропускную способность равной первоначальной пропускной способности, т.е. приравняем (cij, cji) = (Cij, Cji). Назначим a1 = бесконечности и пометим узел 1 меткой [бесконечность, -]. Полагаем i = 1 и переходим ко второму шагу.

    Шаг 2. Определяем множество Si как множество узлов j, в которые можно перейти из узла i по ребру с положительной остаточной пропускной способностью (т.е. cij > 0 для всех j, принадлежащих Si). Если Si не пустое множество, выполняем третий шаг, в противном случае переходим к шагу 4.

    Шаг 3. В множестве Si находим узел k, такой, что cik = max {cij} для всех j, принадлежащих Si. Положим ak = cik и пометим узел k меткой [ak, i]. Если последней меткой помечен узел стока (т.е. если k = n), сквозной путь найден, и мы переходим к пятому шагу.

    Шаг 4 (Откат назад). Если i = 1, сквозной путь невозможен, и мы переходим к шагу 6. Если i не равно 1, находим помеченный узел r, непосредственно предшествующий узлу i, и удаляем узел i из множества узлов, смежных с узлом r. Полагаем i = r и возвращаемся ко второму шагу.

    Шаг 5 (Определение остаточной сети). Обозначим через Np = {1, k1, k2, …, n} множество узлов, через которые проходит p-й найденный сквозной путь от узла источника (узел 1) до узла стока (узел n). Тогда максимальный поток, проходящий по этому пути, вычисляется как fp = min {a1, ak1, ak2, ..., an}.

    Остальные пропускные способности ребер, составляющих сквозной путь, уменьшается на величину fp в направлении движения потока и увеличиваются на эту же величину в противоположном направлении. Таким образом, для ребра (i, j), входящего в сквозной путь, текущие остаточные стоимости (cij, cji) изменятся следующим образом:

    Далее восстанавливаем все узлы, удаленные на шаге 4. полагаем i = 1 и возвращаемся ко второму шагу для поиска нового сквозного пути.

    Шаг 6 (Решение).

    Процесс отката назад на четвертом шаге выполняется тогда, когда алгоритм должен "убить" промежуточный узел до момента реализации сквозного пути. Коррекцию пропускных способностей, выполняемых на шаге 5, можно пояснить на примере простой сети, показанной на рис. 1. На рис. 1а найден первый сквозной путь N1 = {1, 2, 3, 4} с максимальным потоком f1 = 5. После этого остаточные пропускные способности ребер (1, 2), (2, 3) и (3, 4) изменятся соответственно с (5, 0) на (0, 5). На рис. 1б показан второй сквозной путь N2 = {1, 3, 2, 4} с максимальным потоком f2 = 5. После коррекции пропускных способностей получаем сеть, показанную на рис. 1в, где уже невозможно построить сквозной путь. Почему так получилось? При вычислении остаточных пропускных способностей на шаге 5 при переходе от сети (б) к сети (в) невозможна организация потока в направлении 2 -> 3. Получается, что алгоритм как бы "помнит", что поток в направлении 2 -> 3 уже был в предыдущих сквозных путях, и поэтому снова (на пятом шаге) изменяет пропускную способность с 0 до 5 в направлении от узла 3 к узлу 2.


Рис.1. Иллюстрация сквозных путей


    Пример. Найдем максимальный поток в сети из примера предыдущего шага. На рисунке 2 предлагается графическая иллюстрация выполнения алгоритма. Считаем полезным сравнить описание выполняемых алгоритмом вычислительных итераций с их графическим представлением.


Рис.2. Графическая иллюстрация выполнения алгоритма

    Итерация 1.

    Положим остаточные пропускные способности (cij, cji) всех ребер равными первоначальным пропускным способностям (Cij, Cji).

    Шаг 1. Назначаем a1 = бесконечности и помечаем узел 1 меткой [бесконечность,-]. Полагаем i = 1.

    Шаг 2. S1 = [2, 3, 4] (множество не пустое).

    Шаг 3. k = 3, поскольку c13 = max {c12, c13, c14} = max {20, 30, 10} = 30. Назначаем a3 = c13 = 30 и помечаем узел 3 меткой [30, 1]. Полагаем i = 3 и возвращаемся к шагу 2.

    Шаг 2. S2 = [4, 5].

    Шаг 3. k = 5 и a5 = c35 = max {10, 20} = 20. Помечаем узел 5 меткой [20,3]. Получен сквозной путь. Переходим к шагу 5.

    Шаг 5. Сквозной путь определяем по меткам, начиная с узла 5 и заканчивая узлом 1: (5) -> [20, 3] -> (3) -> [30, 1] -> (1). Таким образом, N1 = {1, 3, 5} и f1 = min {a1, a3, a5} = {бесконечность, 30, 20} = 20. Вычисляем остаточные пропускные способности вдоль пути N1:

  (c13, c31) = (30 - 20, 0 + 20) = (10, 20),
  (c35, c53) = (20 - 20, 0 + 20) = (0, 20).

    Итерация 2.

    Шаг 1. Назначаем a1 = бесконечности и помечаем узел 1 меткой [бесконечность,-]. Полагаем i = 1.

    Шаг 2. S1 = [2, 3, 4].

    Шаг 3. k = 2, назначаем a2 = c12 = max {20, 10, 10} = 20 и помечаем узел 2 меткой [20, 1]. Полагаем i =2 и возвращаемся к шагу 2.

    Шаг 2. S3 = [4] (отметим, что c35 = 0, поэтому узел 5 не включается в S3).

    Шаг 3. k = 4, назначаем a4 = c34 = 10 и помечаем узел 4 меткой [10, 3]. Полагаем i = 4 и возвращаемся к шагу 2.

    Шаг 2. S4 = [5] (поскольку узлы 1 и 3 уже помечены, они не включаются в S4).

    Шаг 3. k = 5 и a5 = c45 = 20. Помечаем узел 5 меткой [20, 4]. Получаем сквозной путь. Переходим к шагу 5.

    Шаг 5. N2 = {1, 2, 3, 4, 5} и f2 = min {бесконечность, 20, 40, 10, 20} = 10. Вычисляем остаточные пропускные способности вдоль пути N2:

  (c12, c21) = (20 - 10, 0 + 10) = (10, 10),
  (c23, c32) = (40 - 10, 0 + 10) = (30, 10),
  (c34, c43) = (10 - 10, 5 + 10) = (0, 15),
  (c45, c54) = (20 - 10, 0 + 10) = (10, 10).

    Итерация 3.

    Шаг 1. Назначаем a1 = бесконечности и помечаем узел 1 меткой [бесконечность,-]. Полагаем i = 1.

    Шаг 2. S1 = [2, 3, 4].

    Шаг 3. k = 2, назначаем a2 = c12 = max {10, 10, 10} = 10 и помечаем узел 2 меткой [10, 1]. Полагаем i = 2 и возвращаемся к шагу 2.

    Шаг 2. S2 = [3, 5].

    Шаг 3. k = 3 и a3 = c23 = 30. Помечаем узел 3 меткой [30, 2]. Полагаем i = 3 и возвращаемся к шагу 2.

    Шаг 2. S3 пусто, поскольку c34 = c35 = 0. Переходим к шагу 4.

    Шаг 4. Метка [30, 2] узла 3 показывает номер предшествующего узла r = 2. На этой итерации узел 3 в дальнейшем во внимание не принимается, его метку вычеркиваем. Полагаем i = r = 2 и возвращаемся к шагу 2.

    Шаг 2. S4 = [5] (поскольку узел 3 удален из возможного сквозного пути).

    Шаг 3. k = 5 и a5 = c25 = 30. помечаем узел меткой [30, 2]. Получаем сквозной путь. Переходим к шагу 5.

    Шаг 5. N3 = {1, 2, 5} и f3 = min {бесконечность, 10, 30} = 10. Вычисляем остаточные пропускные способности вдоль пути N3:

  (c12, c21) = (10 - 10, 10 + 10) = (0, 20),
  (c2, c52) = (30 - 10, 0 + 10) = (20, 10).

    Итерация 4. На этой итерации получен путь N4 = {1, 3, 2, 5} с f4 = 10.

    Итерация 5. На этой итерации получен путь N5 = {1, 4, 5} с f5 = 10.

    Итерация 6. Новые сквозные пути невозможны, поскольку все ребра, исходящие из узла 1, имеют нулевые остаточные пропускные способности. Переходим к шагу 6 для определения решения.

    Шаг 6. Максимальный объем потока в сети равен F = f1 + f2 + … +f5 = 20 + 10 + 10 + 10 + 10 = 60 единиц. Значения потоков по различным ребрам вычисляются путем вычитания последних значений остаточных пропускных способностей (т.е. (cij, cji)6) из первоначальных значений пропускных способностей (Cij, Cji). Результаты вычислений приведены ниже:

Ребро  (Cij, Cji)-(cij, cji)6        Величина потока  Направление
(1, 2)	(20, 0) - (0, 20) = (20, -20)	20	     1->2
(1, 3)	(30, 0) - (0, 30) = (30, -30)	30	     1->3
(1, 4)	(10, 0) - (0, 10) = (10, -10)	10	     1->4
(2, 3)	(40, 0) - (40, 0) = (0, 0)          0            -
(2, 5)	(30, 0) - (10, 20) = (20, -20)	20	     2->5
(3, 4)	(10, 5) - (0, 15) = (10, -10)	10	     3->4
(3, 5)	(20, 0) - (0, 20) = (20, -20)	20	     3->5
(4, 5)	(20, 0) - (0, 20) = (20, -20)	20	     4->5

    Приведем текст программы, решающей данную задачу:

#include <iostream.h>
#include <stdlib.h>  //Для функции abs().
#define TRUE 1
#define FALSE 0
#define MaxNodes 5   //Количество вершин.
#define MaxInt 1000  //Машинный эквивалент бесконечности.


//Описание типа узла.
struct Uzel
{
  int Element; //Заданный номер вершины.
  int Propusk; //Пропускная способность.
  int Metka;   //Помечена вершина или нет.
};


class Spisok
{
  private:
      int C[MaxNodes][MaxNodes];    //Матрица начальных пропускных способностей.
      int c[MaxNodes][MaxNodes];    //Матрица конечных пропускных способностей.
      int Put[MaxNodes][MaxNodes];  //Матрица сквозных путей.
      int Potok [MaxNodes];         //Потоки.
      int Est (Uzel*,int,int);
      int Tpk (int*,int,int);
  public:
      void Vvod_Ves();
      int Reshenie ();
      void Vyvod(int);

};

void main()
{
  Spisok A;

  A.Vvod_Ves();
  A.Vyvod(A.Reshenie());
}

void Spisok::Vvod_Ves()
//Ввод матрицы пропускных способностей.
{
  cout << "Вводите пропускные способности ребер:\n";
  for (int i=0;i<MaxNodes;i++)
   for (int j=0;j<MaxNodes;j++)
     {
       cout << "Введите C[" << (i+1) << "," << (j+1) << "]: "; 
       cin >> C[i][j];
       c[i][j] = C[i][j];
     }
}

void Spisok::Vyvod(int n)
//Вывод результатов.
{
  //Вычисление максимального объема потока.
  for (int i=0,sum=0;i<=n;sum+=Potok[i++]);
  cout << "\nМаксимальный объем потока в сети: " << sum;
  cout << "\nЗначения потоков по различным ребрам:\n";
  for (i=0;i<MaxNodes;i++)
   for (int j=i;j<MaxNodes;j++)
     if (C[i][j])
     {
         cout << "Ребро (" << (i+1) << "," << (j+1) <<"): ";
         cout << "(" << C[i][j]  << "," << C[j][i] << ")-(";
         cout << c[i][j]  << "," << c[j][i] << ")=(";
         cout << (C[i][j]-c[i][j]) << "," << (C[j][i]-c[j][i]) << ") ";
         cout << "Поток: " << abs(C[i][j]-c[i][j]) << " ";
         if (C[i][j]-c[i][j]!=0)
         {
           cout << "Направление: ";
           if (C[i][j]-c[i][j]>0)
              cout << (i+1) << "->" << (j+1);
           else
              cout << (j+1) << "->" << (i+1);
         }
         cout << endl;
     }
}

int Spisok::Reshenie()
{
  Uzel SS[MaxNodes]; //Множество узлов, в которые можно перейти.
  Uzel S[MaxNodes]; //Путь.
  int i,j=0,k,el,mx,mn;
  int m; //Текущее количество вершин в пути.
  int nomer=-1; //Текущее количество сквозных потоков.
  int Tupik[MaxNodes]; //Перечень "тупиковых" вершин.
  int N_Tupik; //Количество элементов в массиве.

  while (j!=-1)
  {
    i=m=0;
    S[i].Element=0;
    S[i].Propusk=MaxInt;
    S[i].Metka=TRUE;
    el=0;
    N_Tupik=-1;
    while (el!=MaxNodes-1)
    {
      j=-1;
      for (k=0;k<MaxNodes;k++)
       if (c[i][k]!=0) //Если есть ненулевой поток...
        if (i>0)   //и в путь уже включены вершины...
        {
          if (!Est(&S[0],m,k) && !Tpk(&Tupik[0],N_Tupik,k)) 
                            //то включаем текущую вершину,
                           //если ее нет в пути и если она не "тупиковая".
          {  
             SS[++j].Element=k;
             SS[j].Propusk=c[i][k];
             SS[j].Metka=FALSE;
          }
        }
        else 
          if (!Tpk(&Tupik[0],N_Tupik,k)) //Не вернулись ли назад?
               //Поток не нулевой, и это первая вершина.
          {    //Включаем эту вершину в путь.
               SS[++j].Element=k;
               SS[j].Propusk=c[i][k];
               SS[j].Metka=FALSE;
          }
      if (j!=-1) //Есть продолжение.
      {
         mx=SS[0].Propusk;
         el=0;
         for (k=1;k<=j;k++)
          if (SS[k].Propusk>mx)
            { el=k; mx=SS[k].Propusk; }
         S[++m].Element=SS[el].Element;
         S[m].Propusk=mx;
         S[m].Metka=TRUE;
         if (SS[el].Element==MaxNodes-1) //Найден сквозной путь.
         {
           nomer++;
           //Запоминаем сквозной путь.
           for (k=0;k<=m;k++)
              Put[nomer][k]=S[k].Element;
           //Ищем минимальный поток.
           mn=S[0].Propusk;
           el=0;
           for (k=1;k<=m;k++)
            if (S[k].Propusk<mn)
              { el=k; mn=S[k].Propusk; }
           Potok[nomer]=mn; //Запоминаем его.
           //Вычисляем остаточные пропускные способности.
           for (k=0;k<m;k++)
           { 
             c[S[k].Element][S[k+1].Element] -= Potok[nomer];
             c[S[k+1].Element][S[k].Element] += Potok[nomer];
           }
           el=MaxNodes-1; //Переход к следующей итерации.
           j=0;
         }
         else i=S[m].Element;
      }
      else //Продолжения нет. Это возможно тогда, когда:
      {
         if (i==0)  //а) все пропускные способности нулевые.
                    //   В этом случае - выход
              el=MaxNodes-1;
         else       //б) мы попали в тупик. Запомним тупиковую вершину
                    //   в массиве и отступим назад на одну вершину.
         {
           Tupik[++N_Tupik]=S[m].Element;
           m--;
           i=S[m].Element;
         }
      }
    }
  }
  return nomer; //Возвращает количество сквозных потоков.
}

int Spisok::Est(Uzel S[], int m, int k)
//Функция проверяет, есть ли вершина k в пути S.
//m - текущее количество элементов в пути.
//Возвращает 1, если вершина есть, и 0 - в противном случае.
{
  for (int l=0;l<=m;l++)
    if (S[l].Element==k) return 1;
  return 0;
}

int Spisok::Tpk(int Tupik[],int N_Tupik, int k) 
//Функция проверяет, есть ли вершина k в массиве "тупиковых" вершин.
//N_Tupik - текущее количество вершин в массиве.
//Возвращает 1, если вершина есть, и 0 - в противном случае.
{
  if (N_Tupik==-1) return 0;
  for (int l=0;l<=N_Tupik;l++)
    if (Tupik[l]==k) return 1;
  return 0;
}
Текст этой программы можно взять здесь.

    Результат работы этой программы для сети, изображенной на рисунке 2, будет следующим:


Рис.3. Результат работы приложения

    Со следующего шага мы начнем рассматривать задачу о нахождении потока наименьшей стоимости.




Предыдущий шаг Содержание Следующий шаг