Алгоритм Форда–Фалкерсона: как найти максимальный поток в сети (для начинающих)
Привет, будущие инженеры и программисты! Сегодня мы разберём классический алгоритм Форда–Фалкерсона — дедушку всех алгоритмов максимального потока. Если алгоритм Диница — это современный спорткар, то Форд–Фалкерсон — это надёжная «классика», которая учит основам и помогает понять суть задачи.
Представьте, что вы владелец сети трубопроводов, и вам нужно понять, сколько воды можно прокачать из водохранилища в город. У каждой трубы есть максимальная пропускная способность, и вода может течь только в одном направлении. Ваша задача — найти такой способ распределения воды по трубам, чтобы в город попало максимальное количество воды. Это и есть задача максимального потока!
Такие задачи встречаются повсюду:
Транспорт: сколько грузовиков может проехать от склада до магазинов по дорогам города?
Интернет: сколько данных можно передать от сервера к пользователям через сетевые каналы?
Производство: как максимально эффективно распределить ресурсы между цехами?
Алгоритм Форда–Фалкерсона — это простой и понятный способ решить эту задачу. Он был придуман американскими математиками Лестером Фордом и Делбертом Фалкерсоном в 1956 году.
1. Основная идея: «Ищи путь — толкай воду»
Представьте, что вы стоите у водохранилища с фонариком и хотите найти путь к городу. Алгоритм Форда–Фалкерсона работает очень просто:
Найди путь: Ищем любой путь от водохранилища (истока) до города (стока) по трубам, которые ещё не заполнены до предела.
Толкай воду: Определяем, сколько воды можно протолкнуть по этому пути (это будет равно самой узкой трубе на пути), и отправляем её.
Повторяй: Ищем новый путь и снова толкаем воду. Повторяем до тех пор, пока не найдём ни одного пути.
Готово: Сумма всей воды, которую мы протолкнули, и есть максимальный поток!
Звучит просто, правда? И это действительно так! Алгоритм Форда–Фалкерсона — это воплощение принципа «делай простые вещи, пока они работают».
2. Остаточный граф: «Я передумал, хочу по-другому!»
Но есть одна хитрость. Представьте, что вы протолкнули воду по пути A → B → C, а потом поняли, что было бы лучше отправить часть воды по пути A → D → C. Как это сделать?
В реальной жизни вода не может течь назад по трубе. Но в алгоритме мы можем «передумать» и перенаправить поток. Для этого используется концепция остаточного графа:
Прямые рёбра: Когда мы добавляем трубу A → B с пропускной способностью 10, мы можем по ней протолкнуть до 10 единиц воды.
Обратные рёбра: Одновременно мы создаём «воображаемую» трубу B → A с пропускной способностью 0. Это означает, что пока мы ничего не отправляли, мы не можем «отменить» поток.
Магия перенаправления: Когда мы отправляем 5 единиц воды по A → B:
-
Пропускная способность A → B становится 10 — 5 = 5
-
Пропускная способность B → A становится 0 + 5 = 5
Теперь, если алгоритм найдёт путь, который использует B → A, это будет означать «отмени 5 единиц потока, которые мы отправили по A → B, и отправь их в другое место». Это позволяет алгоритму «исправлять ошибки» и находить оптимальное решение.
3. Поиск пути: BFS vs DFS
Алгоритм Форда–Фалкерсона — это не один конкретный алгоритм, а семейство алгоритмов. Главное отличие — как искать путь от истока к стоку.
BFS (Breadth-First Search): Ищем самый короткий путь (по количеству рёбер). Это как если бы вы искали путь в лабиринте, исследуя все комнаты на расстоянии 1 шага, потом 2 шага, и так далее. Такой вариант называется алгоритмом Эдмондса–Карпа.
DFS (Depth-First Search): Ищем любой путь, идя «вглубь» графа. Это как если бы вы шли по лабиринту, всегда выбирая первый доступный поворот, пока не дойдёте до конца или тупика.
В нашем примере мы используем DFS, потому что его проще понять и реализовать.
4. Пример работы алгоритма
Давайте разберём простой пример. У нас есть 4 точки (0, 1, 2, 3) и следующие трубы с их пропускными способностями:
0 → 1 (пропускная способность 10) 0 → 2 (пропускная способность 10) 1 → 2 (пропускная способность 2) 1 → 3 (пропускная способность 4) 2 → 1 (пропускная способность 6) 2 → 3 (пропускная способность 10)
Исток s = 0, сток t = 3.
Итерация 1:
-
Ищем путь: 0 → 1 → 3
-
Пропускные способности на пути: 10, 4
-
Узкое место: min(10, 4) = 4
-
Отправляем 4 единицы потока
-
Обновляем граф:
-
0 → 1: 10 — 4 = 6
-
1 → 3: 4 — 4 = 0
-
1 → 0: 0 + 4 = 4 (обратное ребро)
-
3 → 1: 0 + 4 = 4 (обратное ребро)
-
-
Общий поток: 4
Итерация 2:
-
Ищем путь: 0 → 2 → 3
-
Пропускные способности на пути: 10, 10
-
Узкое место: min(10, 10) = 10
-
Отправляем 10 единиц потока
-
Обновляем граф:
-
0 → 2: 10 — 10 = 0
-
2 → 3: 10 — 10 = 0
-
2 → 0: 0 + 10 = 10 (обратное ребро)
-
3 → 2: 0 + 10 = 10 (обратное ребро)
-
-
Общий поток: 4 + 10 = 14
Итерация 3:
-
Ищем путь: 0 → 1 → 2 → 3
-
Но 2 → 3 имеет пропускную способность 0, а 0 → 2 тоже 0
-
Пробуем другие пути, но все дороги из 0 либо заблокированы, либо ведут в тупик
-
Путей больше нет, алгоритм завершается
Максимальный поток = 14
5. Код на C# с подробными комментариями
Теперь давайте посмотрим, как это всё работает в коде. Я постарался сделать его максимально понятным, с комментариями на каждом шаге.
// Алгоритм Форда-Фалкерсона — поиск максимального потока в сети using System; using System.Collections.Generic; namespace FordFulkersonMaxFlow { /// <summary>Реализация алгоритма Форда-Фалкерсона с поиском в глубину (DFS).</summary> public class FordFulkerson { private readonly int[,] _capacity; // матрица пропускных способностей private readonly int[,] _flow; // матрица текущих потоков private readonly bool[] _visited; // массив для отслеживания посещённых вершин private readonly int _n; // количество вершин public FordFulkerson(int n) { _n = n; _capacity = new int[n, n]; _flow = new int[n, n]; _visited = new bool[n]; } /// <summary>Добавляет ребро от from к to с пропускной способностью capacity.</summary> public void AddEdge(int from, int to, int capacity) { _capacity[from, to] = capacity; } /// <summary>Находит максимальный поток из источника source в сток sink.</summary> public int MaxFlow(int source, int sink) { int maxFlow = 0; // Основной цикл: ищем пути и толкаем поток, пока это возможно while (true) { // Обнуляем массив посещённых вершин для нового поиска Array.Fill(_visited, false); // Ищем путь от источника к стоку с помощью DFS int pathFlow = DFS(source, sink, int.MaxValue); // Если путь не найден, значит достигли максимума if (pathFlow == 0) break; // Добавляем найденный поток к общему maxFlow += pathFlow; } return maxFlow; } /// <summary> /// Поиск в глубину (DFS) для нахождения пути от current к sink. /// Возвращает минимальную пропускную способность на найденном пути. /// </summary> private int DFS(int current, int sink, int minCapacity) { // Если дошли до стока, возвращаем текущую минимальную пропускную способность if (current == sink) return minCapacity; // Отмечаем текущую вершину как посещённую _visited[current] = true; // Перебираем все возможные следующие вершины for (int next = 0; next < _n; next++) { // Проверяем, можем ли мы пойти в следующую вершину: // 1. Она не должна быть посещённой // 2. Остаточная пропускная способность должна быть больше 0 if (!_visited[next] && GetResidualCapacity(current, next) > 0) { // Вычисляем остаточную пропускную способность до следующей вершины int residualCapacity = GetResidualCapacity(current, next); // Рекурсивно ищем путь дальше, передавая минимум из текущей // минимальной пропускной способности и остаточной способности ребра int pathFlow = DFS(next, sink, Math.Min(minCapacity, residualCapacity)); // Если путь найден (pathFlow > 0), обновляем потоки if (pathFlow > 0) { // Увеличиваем поток по прямому ребру _flow[current, next] += pathFlow; // Уменьшаем поток по обратному ребру (для возможности "отмены") _flow[next, current] -= pathFlow; return pathFlow; } } } // Если ни один путь не найден, возвращаем 0 return 0; } /// <summary> /// Вычисляет остаточную пропускную способность между двумя вершинами. /// Это разность между изначальной пропускной способностью и текущим потоком. /// </summary> private int GetResidualCapacity(int from, int to) { return _capacity[from, to] - _flow[from, to]; } /// <summary>Выводит текущее состояние потоков (для отладки).</summary> public void PrintFlows() { Console.WriteLine("Текущие потоки:"); for (int i = 0; i < _n; i++) { for (int j = 0; j < _n; j++) { if (_capacity[i, j] > 0 && _flow[i, j] > 0) { Console.WriteLine($" {i} → {j}: {_flow[i, j]} / {_capacity[i, j]}"); } } } } } internal static class Program { private static void Main() { // Создаём граф с 4 вершинами int n = 4; FordFulkerson ff = new FordFulkerson(n); // Добавляем рёбра (трубы) с их пропускными способностями ff.AddEdge(0, 1, 10); // от 0 к 1, пропускная способность 10 ff.AddEdge(0, 2, 10); // от 0 к 2, пропускная способность 10 ff.AddEdge(1, 2, 2); // от 1 к 2, пропускная способность 2 ff.AddEdge(1, 3, 4); // от 1 к 3, пропускная способность 4 ff.AddEdge(2, 1, 6); // от 2 к 1, пропускная способность 6 ff.AddEdge(2, 3, 10); // от 2 к 3, пропускная способность 10 // Ищем максимальный поток от вершины 0 к вершине 3 int maxFlow = ff.MaxFlow(0, 3); Console.WriteLine($"Максимальный поток = {maxFlow}"); // Выводим, как распределился поток по рёбрам ff.PrintFlows(); } } }
6. Сравнение с алгоритмом Диница
Вы можете спросить: «А зачем изучать Форда–Фалкерсона, если есть более быстрый алгоритм Диница?» Отличный вопрос!
Форд–Фалкерсон — это основа: Он помогает понять суть задачи максимального потока. Это как изучение арифметики перед алгеброй.
Простота реализации: Код Форда–Фалкерсона проще понять и отладить. Для многих практических задач его скорости вполне достаточно.
Гибкость: Легко модифицировать под специфические требования. Хотите искать пути по BFS вместо DFS? Просто замените одну функцию.
Педагогическая ценность: Алгоритм Диница использует те же принципы остаточного графа, но с оптимизациями. Понимая Форда–Фалкерсона, вы легче поймёте и более сложные алгоритмы.
7. Заключение
Алгоритм Форда–Фалкерсона — это не просто исторический артефакт, а живой инструмент для решения реальных задач. Он может быть не самым быстрым, но зато он понятный, надёжный и легко модифицируемый.
Представьте, что у вас есть сложная логистическая задача: как оптимально распределить товары со складов по магазинам, учитывая пропускную способность дорог? Или как эффективно распределить вычислительную нагрузку между серверами? Алгоритм Форда–Фалкерсона поможет вам найти ответ.
Главное — не пугаться сложных названий и формул. За каждым алгоритмом стоит простая человеческая логика: «найди путь, протолкни что можешь, повтори». Именно такой подход помогает превратить сложную задачу в последовательность простых шагов.
Удачи в изучении алгоритмов! И помните: лучший способ понять алгоритм — это взять карандаш, нарисовать граф и прогнать алгоритм вручную. Компьютер подождёт, а понимание останется с вами навсегда.
ссылка на оригинал статьи https://habr.com/ru/articles/927400/
Добавить комментарий