Привет, будущие инженеры и программисты! Сегодня мы разберём ещё один крутой алгоритм для поиска максимального потока — алгоритм проталкивания предпотока (Push‑Relabel). Если алгоритм Форда‑Фалкерсона — это как если бы вы искали дорогу в городе с фонариком, а алгоритм Диница — как если бы вы строили уровни и шли по ним этажами, то проталкивание предпотока — это как если бы вы взяли гидравлический домкрат и начали «выдавливать» воду из источника!

Представьте, что у вас есть система водопроводных труб, и вы хотите прокачать максимальное количество воды из водонапорной башни в городской район. Но вместо того чтобы искать пути и аккуратно направлять воду, вы решили действовать по‑другому: накачать воду под давлением в башню и позволить ей «выдавливаться» через трубы, постепенно находя оптимальные пути. Это и есть идея алгоритма проталкивания предпотока!

Такой подход используется в:

Гидравлике: моделирование потоков жидкости под давлением
Сетевых технологиях: распределение трафика с учётом «приоритетов» узлов
Логистике: оптимизация грузоперевозок с промежуточными складами
Компьютерном зрении: сегментация изображений методом минимального разреза

Алгоритм был разработан Эндрю Гольдбергом и Робертом Тарьяном в 1986 году как альтернатива классическим методам поиска пути.

1. Основные понятия: "Высота" и "избыточный поток"

Чтобы понять алгоритм, давайте сначала разберёмся с новыми терминами:

Предпоток (Preflow): В отличие от обычного потока, предпоток может нарушать закон сохранения в промежуточных вершинах. То есть в некоторые вершины может «втекать» больше воды, чем «вытекать». Эта «лишняя» вода называется избыточным потоком.

Избыточный поток (Excess): Это количество «лишней» воды, которая скопилась в вершине. Если в вершину втекает 10 единиц, а вытекает 7, то избыточный поток равен 3.

Высота (Height/Label): Каждой вершине присваивается «высота» — целое число, которое показывает, насколько она «высоко» относительно стока. Исток имеет максимальную высоту (обычно равную количеству вершин), сток — высоту 0. Вода может «стекать» только с более высокой вершины на более низкую или равную по высоте.

Допустимое ребро: Ребро из вершины u в вершину v называется допустимым, если:

  • У него есть остаточная пропускная способность (можно протолкнуть воду)

  • Высота u больше высоты v на 1: h[u] = h[v] + 1

Это как если бы вода могла течь только «вниз по лестнице» — на одну ступеньку за раз.

2. Идея алгоритма: "Накачай и выдавливай"

Алгоритм проталкивания предпотока работает по принципу:

1. Инициализация:

  • Устанавливаем высоту истока равной количеству вершин n

  • Все остальные вершины получают высоту 0

  • «Накачиваем» из истока максимальный поток во все соседние вершины

  • Теперь в некоторых вершинах есть избыточный поток

2. Основной цикл: Пока есть вершины с избыточным потоком (кроме истока и стока):

  • Выбираем любую такую вершину

  • Пытаемся протолкнуть избыточный поток в соседние вершины

  • Если протолкнуть не получается, поднимаем высоту вершины

3. Завершение: Когда избыточного потока больше нет, весь поток, который дошёл до стока, и есть максимальный поток.

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

3. Операции Push и Relabel

Алгоритм использует две основные операции:

Push (Проталкивание)

Условие: у вершины u есть избыточный поток, 
         есть допустимое ребро u → v
Действие: проталкиваем min(excess[u], capacity[u→v]) единиц потока

Это как если бы вы взяли часть воды из переполненного ведра и вылили её в соседнее ведро, которое стоит ниже и может принять воду.

Relabel (Переобозначение/Поднятие)

Условие: у вершины u есть избыточный поток,
         но нет допустимых рёбер для проталкивания
Действие: увеличиваем высоту u до минимальной высоты соседей + 1

Это как если бы вы встали на стул, чтобы найти место, куда можно вылить воду.

4. Пример работы алгоритма

Рассмотрим простой граф:

0 → 1 (пропускная способность 10)
0 → 2 (пропускная способность 10)
1 → 3 (пропускная способность 4)
2 → 3 (пропускная способность 10)

Исток s = 0, сток t = 3.

Инициализация:

  • height[0] = 4, height[1] = height[2] = height[3] = 0

  • Проталкиваем из 0: 10 в вершину 1, 10 в вершину 2

  • excess[1] = 10, excess[2] = 10

Итерация 1 (обрабатываем вершину 1):

  • excess[1] = 10, height[1] = 0

  • Смотрим ребро 1 → 3: height[1] = 0, height[3] = 0

  • Ребро не допустимо (нужно height[1] = height[3] + 1)

  • Relabel: height[1] = height[3] + 1 = 1

  • Теперь ребро 1 → 3 допустимо

  • Push: проталкиваем min(10, 4) = 4 единицы в вершину 3

  • excess[1] = 6, excess[3] = 4

Итерация 2 (обрабатываем вершину 2):

  • excess[2] = 10, height[2] = 0

  • Ребро 2 → 3 допустимо (height[2] = 0, height[3] = 0, но нужно height[2] = height[3] + 1)

  • Relabel: height[2] = 1

  • Push: проталкиваем min(10, 10) = 10 единиц в вершину 3

  • excess[2] = 0, excess[3] = 14

Итерация 3 (обрабатываем вершину 1):

  • excess[1] = 6, height[1] = 1

  • Ребро 1 → 3 больше не допустимо (height[1] = 1, height[3] = 0, но пропускная способность = 0)

  • Нет других рёбер из 1

  • Relabel: height[1] = 2

  • Но из 1 всё ещё нельзя ничего протолкнуть

  • Вода "застряла" в вершине 1

Алгоритм продолжается, пока весь избыточный поток не будет либо доставлен в сток, либо возвращён в исток.

5. Код на C# с подробными комментариями

// Алгоритм проталкивания предпотока (Push-Relabel)

using System;
using System.Collections.Generic;
using System.Linq;

namespace PushRelabelMaxFlow
{
    /// <summary>Структура ребра для алгоритма проталкивания предпотока.</summary>
    public class Edge
    {
        public int To;      // куда ведёт ребро
        public int Rev;     // индекс обратного ребра
        public long Cap;    // остаточная пропускная способность
        public long Flow;   // текущий поток

        public Edge(int to, int rev, long cap)
        {
            To = to;
            Rev = rev;
            Cap = cap;
            Flow = 0;
        }
    }

    /// <summary>Реализация алгоритма проталкивания предпотока.</summary>
    public class PushRelabel
    {
        private readonly List<Edge>[] _graph;
        private readonly int _n;
        private long[] _excess;     // избыточный поток в каждой вершине
        private int[] _height;      // высота каждой вершины
        private Queue<int> _active; // очередь активных вершин (с избыточным потоком)

        public PushRelabel(int n)
        {
            _n = n;
            _graph = new List<Edge>[n];
            for (int i = 0; i < n; i++)
                _graph[i] = new List<Edge>();
            
            _excess = new long[n];
            _height = new int[n];
            _active = new Queue<int>();
        }

        /// <summary>Добавляет ребро from → to с пропускной способностью cap.</summary>
        public void AddEdge(int from, int to, long cap)
        {
            // Прямое ребро
            Edge forward = new Edge(to, _graph[to].Count, cap);
            // Обратное ребро с нулевой пропускной способностью
            Edge backward = new Edge(from, _graph[from].Count, 0);
            
            _graph[from].Add(forward);
            _graph[to].Add(backward);
        }

        /// <summary>Находит максимальный поток от источника source к стоку sink.</summary>
        public long MaxFlow(int source, int sink)
        {
            // Инициализация
            InitializePreflow(source);
            
            // Основной цикл: обрабатываем активные вершины
            while (_active.Count > 0)
            {
                int u = _active.Dequeue();
                
                // Если в вершине всё ещё есть избыточный поток
                if (_excess[u] > 0)
                {
                    // Пытаемся протолкнуть поток или поднять высоту
                    if (!PushFlow(u, sink))
                    {
                        Relabel(u);
                        // После поднятия высоты вершина снова становится активной
                        if (_excess[u] > 0)
                            _active.Enqueue(u);
                    }
                }
            }
            
            // Максимальный поток равен избыточному потоку в стоке
            return _excess[sink];
        }

        /// <summary>Инициализирует предпоток: "накачивает" воду из источника.</summary>
        private void InitializePreflow(int source)
        {
            // Источник получает максимальную высоту
            _height[source] = _n;
            
            // Проталкиваем максимальный поток из источника во все соседние вершины
            foreach (var edge in _graph[source])
            {
                if (edge.Cap > 0)
                {
                    // Отправляем всю доступную пропускную способность
                    long flow = edge.Cap;
                    edge.Flow += flow;
                    edge.Cap -= flow;
                    
                    // Увеличиваем пропускную способность обратного ребра
                    _graph[edge.To][edge.Rev].Cap += flow;
                    
                    // Создаём избыточный поток в целевой вершине
                    _excess[edge.To] += flow;
                    _excess[source] -= flow;
                    
                    // Если целевая вершина получила избыточный поток, делаем её активной
                    if (_excess[edge.To] > 0 && edge.To != source)
                    {
                        _active.Enqueue(edge.To);
                    }
                }
            }
        }

        /// <summary>Пытается протолкнуть поток из вершины u. Возвращает true, если удалось.</summary>
        private bool PushFlow(int u, int sink)
        {
            foreach (var edge in _graph[u])
            {
                // Проверяем, является ли ребро допустимым
                if (edge.Cap > 0 && _height[u] == _height[edge.To] + 1)
                {
                    // Вычисляем количество потока для проталкивания
                    long pushFlow = Math.Min(_excess[u], edge.Cap);
                    
                    // Обновляем поток и пропускные способности
                    edge.Flow += pushFlow;
                    edge.Cap -= pushFlow;
                    _graph[edge.To][edge.Rev].Cap += pushFlow;
                    
                    // Обновляем избыточные потоки
                    _excess[u] -= pushFlow;
                    _excess[edge.To] += pushFlow;
                    
                    // Если целевая вершина получила избыточный поток и это не сток,
                    // добавляем её в очередь активных вершин
                    if (_excess[edge.To] == pushFlow && edge.To != sink)
                    {
                        _active.Enqueue(edge.To);
                    }
                    
                    // Если весь избыточный поток протолкнут, возвращаем true
                    if (_excess[u] == 0)
                        return true;
                }
            }
            
            return false; // Не удалось протолкнуть поток
        }

        /// <summary>Поднимает высоту вершины u.</summary>
        private void Relabel(int u)
        {
            int minHeight = int.MaxValue;
            
            // Находим минимальную высоту среди соседей с доступной пропускной способностью
            foreach (var edge in _graph[u])
            {
                if (edge.Cap > 0)
                {
                    minHeight = Math.Min(minHeight, _height[edge.To]);
                }
            }
            
            // Поднимаем высоту на 1 выше минимальной найденной
            if (minHeight != int.MaxValue)
            {
                _height[u] = minHeight + 1;
            }
        }

        /// <summary>Выводит текущее состояние для отладки.</summary>
        public void PrintState()
        {
            Console.WriteLine("Текущее состояние:");
            Console.WriteLine("Вершина | Высота | Избыток");
            Console.WriteLine("--------|--------|--------");
            for (int i = 0; i < _n; i++)
            {
                Console.WriteLine($"   {i}    |   {_height[i]}    |   {_excess[i]}");
            }
            Console.WriteLine();
        }
    }

    internal static class Program
    {
        private static void Main()
        {
            // Создаём граф с 4 вершинами
            int n = 4;
            PushRelabel pr = new PushRelabel(n);

            // Добавляем рёбра с пропускными способностями
            pr.AddEdge(0, 1, 10);  // от 0 к 1, пропускная способность 10
            pr.AddEdge(0, 2, 10);  // от 0 к 2, пропускная способность 10
            pr.AddEdge(1, 3, 4);   // от 1 к 3, пропускная способность 4
            pr.AddEdge(2, 3, 10);  // от 2 к 3, пропускная способность 10

            Console.WriteLine("Поиск максимального потока...");
            
            // Находим максимальный поток от вершины 0 к вершине 3
            long maxFlow = pr.MaxFlow(0, 3);
            
            Console.WriteLine($"Максимальный поток = {maxFlow}");
            
            // Выводим финальное состояние
            pr.PrintState();
        }
    }
}

6. Сравнение с другими алгоритмами

Вы можете спросить: "А зачем нужен ещё один алгоритм максимального потока?" Отличный вопрос!

Форд-Фалкерсон: Простой и понятный, но может работать медленно на некоторых графах. Это как идти пешком — надёжно, но не быстро.

Диниц: Быстрый и эффективный, но требует построения уровневых графов. Это как ехать на машине по навигатору — быстро, но нужно планировать маршрут.

Проталкивание предпотока: Работает «локально» — каждая вершина решает свои проблемы независимо. Это как система автоматического полива — каждая секция работает самостоятельно, но в итоге весь сад политый.

Преимущества Push‑Relabel:

  • Хорошо параллелится (разные вершины можно обрабатывать одновременно)

  • Стабильная производительность на разных типах графов

  • Легко модифицируется для специальных задач

Недостатки:

  • Сложнее для понимания новичкам

  • Требует больше памяти для хранения высот и избыточных потоков

  • Может создавать «промежуточные» состояния с большим количеством избыточного потока

7. Заключение

Алгоритм проталкивания предпотока — это элегантный пример того, как можно решить задачу, подойдя к ней с совершенно другой стороны. Вместо поиска путей мы «накачиваем давление» и позволяем потоку найти оптимальное распределение самостоятельно.

Этот подход не только красив теоретически, но и практически полезен. Он лежит в основе многих современных алгоритмов компьютерного зрения, используется в системах управления трафиком и помогает решать задачи оптимизации в реальном времени.

Главное — не пугайтесь новых концепций вроде «предпотока» и «высот». За каждым термином стоит простая физическая интуиция: вода течёт сверху вниз, и если где‑то её слишком много, она найдёт способ стечь дальше. Алгоритм просто формализует эту интуицию и превращает её в точные правила.

Помните: лучший способ понять любой алгоритм — взять простой пример и проследить его выполнение шаг за шагом. Нарисуйте граф, поставьте высоты, начните «проталкивать» поток, и вы увидите, как абстрактные правила превращаются в понятные действия.

Удачи в изучении алгоритмов! И не забывайте: каждый сложный алгоритм — это просто набор простых идей, собранных вместе. Главное — не торопиться и разбирать по одной идее за раз.

Комментарии (4)


  1. leshchenko
    26.07.2025 02:17

    Ох уж эти метафоры от ЧатГПТ...


  1. wataru
    26.07.2025 02:17

    Хорошо бы в сравнении привести ассимптотику алгоритмов. А то это не серъезно вообще.


    1. callmefordream
      26.07.2025 02:17

      Для "голой" концепции проталкивания предпотока,где можно на рандом избыточные вершины выбирать, асимтотика вроде O(V²E). Но сразу же есть улучшение - алгоритм "поднять-в-начало", где особым способом выбираются эти вершины, и тут уже асимптотика O(V³)


      1. wataru
        26.07.2025 02:17

        Я могу вспомнить или погуглить сложности всех перечисленных в сравнении алгоритмов. Но эти оценки должны быть приведены в статье. А то там есть слова "быстрый и эффективный", но читателям не ясно, насколько.