1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10

# теория · шаг 4 из 10

Разложение потока: какие грузовики по каким маршрутам

Коротко

Алгоритм потока отвечает числами на рёбрах: по этому ребру 11, по тому 3. Диспетчеру нужно другое - МАРШРУТЫ: сколько машин пустить по какому пути от склада до магазина. Теорема о разложении потока: любой поток раскладывается на пути из s в t и циклы, и тех и других вместе не больше числа рёбер с ненулевым потоком. Алгоритм простой: идти из s по рёбрам с потоком до t (или до повтора вершины - тогда найден цикл), снять с найденного пути или цикла его наименьшее значение и повторить. Каждый шаг обнуляет хотя бы одно ребро. Пути дают величину потока, циклы - бесполезный круговорот, который можно выбросить. Если поток целый, целыми выходят и значения на путях.

Если ты умеешь раскладывать поток на пути и циклы, знаешь, почему их не больше E, и понимаешь, что делать с циклами, листай до «Теперь сам».

Как думаешь, чем числа на рёбрах хуже маршрутов

Максимальный поток из прошлых уроков говорит: склад может отправить в магазин 35 машин в час, и вот загрузка каждой дороги. Но водителю нужно не «по ребру 13→1 едет 12», а «ты едешь 0 → 13 → 1 → 24 → 5 → 29».

Прежде чем читать дальше, ответь себе: всегда ли числа на рёбрах можно собрать в маршруты? Сколько разных маршрутов понадобится, если рёбер с потоком 17? И что делать, если где-то в числах спрятан круг, по которому машины ездят, никуда не приезжая?

На что это похоже

Счёт за воду в многоквартирном доме. Счётчики на трубах показывают, сколько прошло через каждый участок. Чтобы понять, сколько воды пришло в каждую квартиру, расход распутывают: прослеживают трубы от ввода до кранов. Иногда находится кольцо - вода гоняется насосом по кругу и ни в одну квартиру не попадает. Счётчики честно его учли, но квартирам он ничего не даёт.

Механизм: идти по потоку и снимать минимум

пока есть рёбра с ненулевым потоком:
    если из s выходит поток: идём от s, каждый раз по любому ребру
        с потоком, пока не придём в t или в уже пройденную вершину
    иначе: идём так же от начала любого ребра с потоком
    нашли путь s → t или цикл:
        d = наименьший поток на его рёбрах
        вычесть d со всех его рёбер (хотя бы одно обнулится)
        записать (d, путь) или (d, цикл)

Почему идти можно всегда. В каждой промежуточной вершине поток сохраняется: вошло - столько же вышло. Значит, придя в вершину по ребру с потоком, мы найдём ребро с потоком, чтобы уйти. Остановить нас может только t или повтор вершины. Вычитание d с пути или цикла сохранение не ломает: из каждой вершины пути уходит ровно столько, сколько пришло.

Почему не больше E штук. Каждое вычитание обнуляет хотя бы одно ребро (то, где был минимум), а обнулённое ребро больше не участвует. Шагов не больше числа рёбер с потоком.

Целочисленность: если все значения потока целые, то и каждое d - минимум целых - целое.

Правило

Любой поток раскладывается на пути из s в t и циклы, и их вместе не больше числа рёбер с ненулевым потоком. Строят разложение, идя по рёбрам с потоком до t или до повтора вершины и вычитая наименьшее значение на найденном пути или цикле. Сумма по путям равна величине потока; циклы величины не добавляют и при поиске потока минимальной стоимости выдают лишнюю трату. Целый поток раскладывается на целые пути.

Разбор: путь и спрятанный цикл

поток: s→a 2   a→b 3   b→t 2   b→c 1   c→a 1
проверка сохранения: в a пришло 2 + 1 = 3, ушло 3;  в b пришло 3, ушло 3

шаг 1: от s: s → a → b → t        минимум 2
       вычитаем: s→a 0, a→b 1, b→t 0
       путь (2): s a b t
шаг 2: из s потока нет; начинаем с a→b: a → b → c → a - повтор a
       минимум 1, вычитаем: всё обнулилось
       цикл (1): a b c a

итог: один путь на 2 и цикл на 1. Величина потока - 2.

Цикл a → b → c → a гоняет единицу по кругу. Если у рёбер есть цена, это чистый убыток: выбросив цикл, получим поток той же величины и дешевле. Именно так рассуждает алгоритм отмены циклов из главы о потоках минимальной стоимости.

Разбор: тридцать перекрёстков

Программа находит максимальный поток в случайной сети, раскладывает его на пути и циклы, складывает их обратно и сверяет с исходным потоком.

razlozhenie.py
import random
from collections import deque

rng = random.Random(17)
V, s, t = 30, 0, 29
cap = {}
for _ in range(120):
    u, v = rng.randrange(V), rng.randrange(V)
    if u != v and v != s and u != t:
        cap[u, v] = cap.get((u, v), 0) + rng.randint(1, 20)


def maks_potok():                     # Эдмондс-Карп на словаре рёбер
    ost = dict(cap)
    for (u, v) in cap:
        ost.setdefault((v, u), 0)
    sos = {}
    for (u, v) in ost:
        sos.setdefault(u, []).append(v)
    znach = 0
    while True:
        otec, och = {s: None}, deque([s])
        while och and t not in otec:
            u = och.popleft()
            for v in sos.get(u, []):
                if ost[u, v] > 0 and v not in otec:
                    otec[v] = u
                    och.append(v)
        if t not in otec:
            break
        put, v = [], t
        while otec[v] is not None:
            put.append((otec[v], v))
            v = otec[v]
        d = min(ost[e] for e in put)
        for u, v in put:
            ost[u, v] -= d
            ost[v, u] += d
        znach += d
    return znach, {e: cap[e] - ost[e] for e in cap if cap[e] > ost[e]}


def razlozhit(potok):                 # пути s→t и циклы
    f, puti, cikly = dict(potok), [], []
    iz = {}
    for (u, v) in f:
        iz.setdefault(u, []).append(v)
    while f:
        start = s if any(u == s for u, _ in f) else next(iter(f))[0]
        put, gde = [start], {start: 0}
        while put[-1] != t or start != s:
            u = put[-1]
            v = next(v for v in iz[u] if (u, v) in f)
            if v in gde:              # замкнулся цикл
                put = put[gde[v]:] + [v]
                break
            gde[v] = len(put)
            put.append(v)
            if v == t and start == s:
                break
        rebra = list(zip(put, put[1:]))
        d = min(f[e] for e in rebra)
        for e in rebra:
            f[e] -= d
            if f[e] == 0:
                del f[e]
        kuda = puti if put[0] == s and put[-1] == t else cikly
        kuda.append((d, put))
    return puti, cikly


znach, potok = maks_potok()
puti, cikly = razlozhit(potok)
sobrano = {}                          # сложить пути обратно в поток
for d, p in puti + cikly:
    for e in zip(p, p[1:]):
        sobrano[e] = sobrano.get(e, 0) + d
assert sobrano == potok
print("величина потока:", znach, " рёбер с потоком:", len(potok))
print("путей:", len(puti), " циклов:", len(cikly),
      " сумма по путям:", sum(d for d, _ in puti),
      " поток восстановлен")
for d, p in puti[:3]:
    print(f"  {d:3} по пути {'-'.join(map(str, p))}")
↳  выводpython3 razlozhenie.py
величина потока: 35  рёбер с потоком: 17
путей: 6  циклов: 0  сумма по путям: 35  поток восстановлен
   11 по пути 0-13-1-24-5-29
    1 по пути 0-13-1-24-29
    3 по пути 0-14-18-5-29

Поток величиной 35 на 17 рёбрах разложился на 6 маршрутов - меньше, чем рёбер, как и обещает теорема. Циклов в этом потоке нет, но так бывает не всегда: после отмен по обратным рёбрам поток алгоритма дополняющих путей может содержать круговорот, и разложение его покажет. Сборка путей обратно дала ровно исходные числа на рёбрах.

Что ломается без этого

Первая ошибка - раздавать маршруты, не проверив циклы: машины, отправленные «по циклу», катаются без пользы, а диспетчер их учёл как доставку. Вторая - ждать, что разложение единственно: из одного потока можно получить разные наборы путей, в зависимости от того, какое ребро выбирать на развилке, - теорема обещает только их число. Третья - раскладывать дробный поток и ждать целых путей: целыми они будут, только если целым был сам поток. Четвёртая - искать «по ребру с потоком» перебором всех рёбер на каждом шаге: на большой сети это квадрат; списки исходящих рёбер с указателем на первое ненулевое дают O(V · E) на всё разложение.

Зачем это в работе

Любая задача, где поток считали ради маршрутов: развоз грузов (пути - рейсы), маршрутизация трафика в сети (пути - туннели, по которым пускают пакеты), назначение задач по цепочкам исполнителей, восстановление непересекающихся путей после подсчёта их числа потоком. Разложение же - инструмент доказательств: многие утверждения о потоках проверяют на путях и циклах, а потом собирают обратно. Если результат потока надо кому-то объяснить, объясняют его разложением.

Теперь сам

Поток: s→a 4, s→b 1, a→b 2, a→t 2, b→t 3. Разложи его на пути. Сколько путей получится и какова их сумма?

Ответ. От s по s→a: s → a → b → t, минимум min(4, 2, 3) = 2; остаётся s→a 2, a→b 0, b→t 1, s→b 1, a→t 2. Дальше s → a → t с минимумом 2, затем s → b → t с минимумом 1. Три пути: 2 + 2 + 1 = 5 - ровно величина потока (из s выходит 4 + 1 = 5). Рёбер с потоком пять, путей три - в пределах теоремы.

Главное

Числа на рёбрах становятся маршрутами через разложение: идти по рёбрам с потоком до стока или до повтора вершины и вычитать наименьшее. Получается не больше E путей и циклов, пути дают величину потока, циклы - бесполезный круговорот, который выбрасывают; целый поток раскладывается на целые пути. Поток в 35 на 17 рёбрах - это 6 маршрутов.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.