Привет, будущие инженеры и программисты! Сегодня мы разберём ещё один крутой алгоритм для поиска максимального потока — алгоритм проталкивания предпотока (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)
wataru
26.07.2025 02:17Хорошо бы в сравнении привести ассимптотику алгоритмов. А то это не серъезно вообще.
callmefordream
26.07.2025 02:17Для "голой" концепции проталкивания предпотока,где можно на рандом избыточные вершины выбирать, асимтотика вроде O(V²E). Но сразу же есть улучшение - алгоритм "поднять-в-начало", где особым способом выбираются эти вершины, и тут уже асимптотика O(V³)
wataru
26.07.2025 02:17Я могу вспомнить или погуглить сложности всех перечисленных в сравнении алгоритмов. Но эти оценки должны быть приведены в статье. А то там есть слова "быстрый и эффективный", но читателям не ясно, насколько.
leshchenko
Ох уж эти метафоры от ЧатГПТ...