5. Branch and bound
Введение: Элита дискретной оптимизации
В предыдущей главе 4. Backtracking мы детально исследовали процедуру исчерпывающего перебора вариантов с отсечением тупиковых ветвей, явно нарушающих граничные правила. Поиск с возвратом безупречно решает задачи генерации комбинаций, поиска всех допустимых путей или расстановки фигур.
Однако перед бэкенд-инженером регулярно возникает задача совершенно иного уровня сложности: найти не просто какое-либо допустимое решение, а строго наилучшее (глобально оптимальное) распределение ресурсов.
Мы могли бы обратиться к динамическому программированию (DP, см. 3. Dynamic programming - динамическое программирование), однако DP требует огромного пространства памяти. Если вместимость задачи масштабна (например, ограничение рюкзака составляет $W = 10^9$), динамическое программирование вызовет мгновенное исчерпание RAM, попытавшись аллоцировать колоссальную матрицу состояний.
Когда динамическое программирование парализовано нехваткой оперативной памяти, а классический Backtracking работает недопустимо медленно из-за монотонного перебора всех допустимых, но неоптимальных конфигураций, в бой вступает высшая лига дискретной оптимизации — Метод ветвей и границ (Branch and Bound, B&B).
Концепция: Умный поиск с предсказанием
Метод ветвей и границ — это интеллектуальная математическая эволюция поиска с возвратом. Вместо прямолинейного блуждания по дереву решений с отсечением по факту нарушения правил, B&B опирается на строгую математическую релаксацию, позволяющую предсказывать верхний предел выгоды в поддереве.
Алгоритм непрерывно удерживает в памяти Текущий рекорд (Incumbent) — наилучшее валидное решение, найденное на текущий момент работы.
Для каждого рассматриваемого узла дерева алгоритм рассчитывает Оптимистичную границу (Bound) — строгую верхнюю математическую оценку качества решения, которое в принципе достижимо, если продолжить движение вглубь данной ветви.
Золотое правило метода Branch and Bound:
Если расчетная оптимистичная оценка (
Bound) текущей ветви оказывается хуже или равна нашему зафиксированному Рекорду (Incumbent), эта ветвь немедленно уничтожается (Pruning).
Нам незачем тратить такты процессора на ее исследование: математически доказано, что в этом поддереве физически не может существовать результат, превосходящий уже найденный нами оптимум.
flowchart TD
Root["Старт<br/>Рекорд Incumbent: 0"] -->|"Включаем предмет 1"| B1["Узел 1<br/>Оптимистичная оценка Bound: 120"]
Root -->|"Исключаем предмет 1"| B2["Узел 2<br/>Оптимистичная оценка Bound: 85"]
B1 -->|"Найдено полное решение"| Sol1["Лист дерева<br/>Фактический профит: 95<br/>ОБНОВЛЕНИЕ РЕКОРДА: 95"]
B2 -.->|"Оценка 85 <= Рекорда 95<br/>МГНОВЕННОЕ ОТСЕЧЕНИЕ"| Pruned["Ветвь отсечена<br/>Экономия миллиардов тактов CPU"]
classDef default fill:#0f172a,stroke:#334155,color:#f8fafc
classDef branch fill:#1e293b,stroke:#818cf8,color:#f8fafc
classDef record fill:#1e293b,stroke:#10b981,color:#10b981
classDef cut fill:#1e293b,stroke:#f43f5e,color:#f43f5e
class Root default
class B1 branch
class B2,Pruned cut
class Sol1 record
Механика: Ветвление, Оценка, Отсечение
Классическим полигоном для метода выступает Дискретная задача о рюкзаке (0/1 Knapsack Problem): имеется набор предметов с заданным весом и стоимостью; вместимость рюкзака ограничена; требуется максимизировать суммарную ценность. Динамическое программирование требует $O(N \cdot W)$ памяти и времени. Если $W$ велико, DP бессильно.
Архитектура Branch and Bound решает задачу в три приема:
- Ветвление (Branching): На каждом шаге рекурсивного спуска принимается бинарное альтернативное решение: «Взять предмет в рюкзак» либо «Пропустить предмет». Дерево состояний ветвится вглубь.
- Оценка (Bounding): Ключевой интеллектуальный этап. Для получения оптимистичной оценки мы применяем математическую релаксацию (Relaxation). В дискретной задаче предметы неделимы. Ослабим это ограничение: представим, что предметы разрешено дробить на произвольные непрерывные доли.
В условиях дробного рюкзака начинает работать молниеносный жадный алгоритм (2. Greedy алгоритмы): мы жадно набираем предметы с максимальным удельным отношениемЦенность / Вес(илиValue / Weight), а последний невместившийся предмет отсекаем ровно по остатку свободной емкости. Это дает абсолютный теоретический потолок ценности — Верхнюю границу (Upper Bound). Поскольку в дискретной реальности ломать предметы запрещено, реальный профит этой ветви гарантированно не превысит вычисленный Bound. - Отсечение (Pruning): Если вычисленный
Upper Bound$\le$maxProfit, ветвь мгновенно аннулируется.
Mechanical Sympathy: Память vs Процессор
В отличие от динамического программирования, поглощающего сотни мегабайт оперативной памяти ($O(N \cdot W)$), реализация B&B на базе поиска в глубину (DFS) функционирует strictly In-Place.
Расход оперативной памяти строго лимитирован предельной высотой стека вызовов, равной общему количеству предметов ($O(N)$). Весь процесс расчета границы Bound сводится к плотной скалярной арифметике, циркулирующей в ультрабыстром L1-кэше процессора. Метод ветвей и границ выгодно разменивает нехватку памяти на вычислительную мощность процессорных ядер.
Идиоматичная реализация на Go (DFS B&B)
Ниже представлен высокопроизводительный production-каркас решения дискретной задачи о рюкзаке с алгоритмом DFS Branch and Bound:
package branchandbound
import (
"cmp"
"slices"
)
// Item представляет предмет для рюкзака
type Item struct {
Weight int
Value int
Ratio float64 // Удельная ценность Value / Weight
}
// KnapsackBB решает задачу о рюкзаке методом Ветвей и Границ
func KnapsackBB(capacity int, weights, values []int) int {
n := len(weights)
items := make([]Item, n)
for i := 0; i < n; i++ {
items[i] = Item{
Weight: weights[i],
Value: values[i],
Ratio: float64(values[i]) / float64(weights[i]),
}
}
// 1. ЖАДНЫЙ ВЫБОР для Оценки. Сортируем предметы по убыванию удельной ценности.
// Это критически важно для эффективного отсечения.
slices.SortFunc(items, func(a, b Item) int {
return cmp.Compare(b.Ratio, a.Ratio) // По убыванию
})
maxProfit := 0
// 2. Вспомогательная функция для вычисления оптимистичной оценки (Bound)
bound := func(level, currentWeight, currentValue int) float64 {
if currentWeight >= capacity {
return 0
}
profitBound := float64(currentValue)
totalWeight := currentWeight
j := level
// Жадный набор целых предметов
for j < n && totalWeight+items[j].Weight <= capacity {
totalWeight += items[j].Weight
profitBound += float64(items[j].Value)
j++
}
// Добираем "кусочек" последнего предмета (дробный рюкзак)
if j < n {
remainingCapacity := capacity - totalWeight
profitBound += float64(remainingCapacity) * items[j].Ratio
}
return profitBound
}
// 3. DFS функция (Бэктрекинг с отсечением)
var dfs func(level, currentWeight, currentValue int)
dfs = func(level, currentWeight, currentValue int) {
// Обновляем рекорд
if currentWeight <= capacity && currentValue > maxProfit {
maxProfit = currentValue
}
// Базовый случай: дошли до конца
if level == n {
return
}
// Вычисляем Bound для случая "БЕРЕМ предмет"
if currentWeight+items[level].Weight <= capacity {
// Мы даже не заходим в ветку, если оценка хуже текущего рекорда
if bound(level+1, currentWeight+items[level].Weight, currentValue+items[level].Value) > float64(maxProfit) {
dfs(level+1, currentWeight+items[level].Weight, currentValue+items[level].Value)
}
}
// Вычисляем Bound для случая "НЕ БЕРЕМ предмет"
// Если даже мы пропустим этот предмет, сможем ли мы побить рекорд?
if bound(level+1, currentWeight, currentValue) > float64(maxProfit) {
dfs(level+1, currentWeight, currentValue)
}
}
// Запускаем DFS с нулевого уровня
dfs(0, 0, 0)
return maxProfit
}
Где применяется Branch and Bound в Бэкенде?
Парадигма ветвей и границ лежит в сердце специализированных движков дискретной оптимизации (Solvers):
- Маршрутизация и логистика (VRP - Vehicle Routing Problem): Построение кратчайших маршрутов доставки флота курьеров (Яндекс Доставка, логистические распределители). Классическая задача коммивояжера (
TSP) решается через вариации B&B. - Кластерные планировщики задач (Schedulers): Распределение подов в кластерах
Kubernetesс учетом квот (Resource Quotas) и топологий размещения (Node Affinities), где требуется решить задачу оптимальной упаковки контейнеров (Bin Packing). - Финансовые матчинговые движки (Matching Engines): В высокочастотном трейдинге (
HFT) для расчета оптимального ребалансирования портфелей и сведения сложных составных ордеров.
Итог раздела Детерминированных Алгоритмов
Мы завершили фундаментальное исследование классических детерминированных парадигм точного решения задач:
- Divide and Conquer — делит массив на независимые изолированные половины и сливает решения.
- Greedy — жадно выбирает сиюминутный локальный оптимум, игнорируя историю.
- Dynamic Programming — систематически аккумулирует решения перекрывающихся подзадач в таблице.
- Backtracking — исчерпывающе обходит пространство состояний, отсекая логические тупики.
- Branch and Bound — умно перебирает варианты, отсекая поддеревья на базе предиктивных математических оценок.
Все эти алгоритмы объединяет фундаментальный принцип: они абсолютно детерминированы. При идентичных входных данных они гарантированно повторяют одну и ту же траекторию и выдают 100% точный результат.
Но что делать, если пространство поиска колоссально настолько, что даже B&B потребует тысячелетий вычислений? Что, если бизнес устраивает результат с вероятностью успеха 99.999%, но рассчитанный ровно за 2 миллисекунды?
В таких сценариях инженеры отбрасывают строгий детерминизм и впускают в систему управляемую случайность: 6. Randomized алгоритмы.