1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 3 из 7

Сетевой симплекс: симплекс-метод, который ходит по деревьям

Коротко

Поток минимальной стоимости - это линейная программа, и её можно решать симплекс-методом. Но у транспортной сети особая структура: базисные рёбра образуют остовное дерево. Остальные рёбра стоят на нижней или верхней границе потока. Сетевой симплекс держит дерево и эти границы, считает по дереву потенциалы вершин (на рёбрах дерева приведённая цена c(u,v) + π(u) − π(v) равна нулю) и ищет вне дерева ребро с выгодным остаточным направлением. Добавленное к дереву, оно замыкает единственный цикл; поток меняют до достижения одной из границ. Это один шаг симплекса, но без таблиц: только обход дерева. На практике сетевой симплекс - один из самых быстрых способов решать такие задачи.

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

Сначала разберём транспортную задачу без верхних ограничений на отдельные маршруты: в ней небазисные рёбра действительно имеют нулевой поток. Затем добавим конечные ёмкости. Программа ниже реализует первый вариант; конечные ёмкости она не принимает.

Как думаешь, как выглядит угол многогранника для потока

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

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

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

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

Механизм: дерево, потенциалы, оборот

базис: остовное дерево, поток идёт только по его рёбрам
       (n вершин → n − 1 рёбер; рёбра вне дерева - ноль)

потенциалы: π(корня) = 0; на ребре дерева u → v: π(v) = π(u) + c(u,v)
приведённая цена ребра:  r(u,v) = c(u,v) + π(u) − π(v)
       на рёбрах дерева r = 0; вне дерева - выгода открыть ребро

шаг (оборот):
    вход: ребро вне дерева с r < 0  (нет таких - план оптимален)
    цикл: это ребро + путь в дереве между его концами
    по направлению u → v рёбра цикла «попутные» (+θ) или «встречные» (−θ)
    θ = наименьший поток на встречных рёбрах
    выход: встречное ребро, ставшее нулём

Почему базис - дерево в этом варианте. Если ненулевые рёбра содержат цикл, поток по нему можно сдвинуть на ±ε; цена меняется на ±ε · (цена цикла), и сдвиг в одну из сторон не хуже. Сдвигаем, пока ребро цикла не обнулится: циклов становится меньше, цена не растёт. В итоге ненулевые рёбра - лес, и до дерева его дополняют рёбрами с нулевым потоком.

Как получить стартовое дерево. Добавляют вершину-корень и от каждого склада к корню (от корня к каждому магазину) искусственное ребро очень большой цены - все запасы идут через корень. Симплекс сам вытеснит дорогие искусственные рёбра; если какое-то осталось с потоком, задача невыполнима.

«Большую цену» нельзя выбирать наугад. Общий надёжный способ - две фазы: сначала реальным дугам назначить цену 0, искусственным 1 и минимизировать суммарный искусственный поток. Положительный минимум означает невыполнимость. При нуле фиксируем потоки искусственных дуг нулём и возвращаем реальные цены. Нельзя оставить их доступными с ценой 1: они могли бы снова стать выгоднее настоящих маршрутов. Если нулевая искусственная дуга ещё нужна для описания дерева, её можно временно держать в базисе, сохраняя нулевую ёмкость. В целочисленной программе ниже применяется одна фаза с M=10⁶: весь реальный поток не больше 240 единиц, цены не больше 30, стоимость допустимого реального плана не больше 7200. Любой положительный целый искусственный поток дороже этой границы.

Цены r - те же потенциалы Джонсона из прошлого урока, только посчитанные по дереву, а не кратчайшими путями. Отрицательная приведённая цена у ребра вне дерева значит, что цикл через него имеет отрицательную стоимость: сетевой симплекс - это отмена отрицательных циклов, где циклы ищутся не Беллманом-Фордом, а по дереву.

Правило

В варианте без верхних ёмкостей сетевой симплекс держит остовное дерево с потоком и потенциалы, при которых приведённая цена c(u,v) + π(u) − π(v) на рёбрах дерева нулевая. Ребро вне дерева с отрицательной приведённой ценой входит в дерево, замыкая цикл; по циклу пускают θ - наименьший поток на встречных рёбрах, - и обнулившееся встречное ребро выходит. Нет рёбер с отрицательной ценой - план оптимален. Стартовое дерево дают дорогие искусственные рёбра через корень.

Разбор: два склада, два магазина

склады A 5, B 5;  магазины X 4, Y 6
цены: A→X 2  A→Y 4  B→X 3  B→Y 1

дерево: A→Y 5, B→X 4, B→Y 1       стоимость 20 + 12 + 1 = 33
потенциалы: π(A) = 0, π(Y) = 4, π(B) = 4 − 1 = 3, π(X) = 3 + 3 = 6
вне дерева: r(A→X) = 2 + 0 − 6 = −4  < 0 → входит

цикл: A→X (попутное), X…B по B→X (встречное), B→Y (попутное),
      Y…A по A→Y (встречное)
θ = min(B→X 4, A→Y 5) = 4
новый поток: A→X 4, B→X 0 (выходит), B→Y 5, A→Y 1
стоимость 8 + 5 + 4 = 17 - на 4 · 4 = 16 меньше

проверка: π(A) 0, π(X) 2, π(Y) 4, π(B) 3
          r(B→X) = 3 + 3 − 2 = 4 ≥ 0 → оптимум

Один оборот снизил стоимость ровно на |r| · θ = 4 · 4. Так и должно быть: цикл с приведённой ценой −4 прошли четыре единицы.

Разбор: конечные ёмкости меняют правило

Когда у рёбер есть верхние ёмкости

Теперь зададим 0≤f(e)≤u(e). В базисе свободные рёбра, чей поток может изменяться в обе стороны, не содержат цикла; их дополняют до дерева T. Рёбра вне дерева делятся на L с потоком 0 и U с потоком u(e). Ненулевые рёбра теперь могут содержать цикл: насыщенная небазисная дуга не обязана принадлежать дереву.

На дереве по-прежнему r(e)=c(e)+π(u)−π(v)=0. Для e из L можно только увеличивать поток, поэтому улучшение возможно при r(e)<0. Для e из U можно только уменьшать его: остаточное направление обратное, с ценой −r(e). Улучшение возможно при r(e)>0. Если нарушений нет, все остаточные направления имеют неотрицательную приведённую цену, отрицательных циклов нет и план оптимален.

После выбора направления его добавляют к дереву и меняют поток по циклу. Допустимая величина шага - минимум из u(e)−f(e) на попутных рёбрах и f(e) на встречных. Дуга, достигшая границы, может выйти из дерева. Если ограничивающей оказалась сама входящая дуга, дерево остаётся прежним, а дуга переходит между L и U. Это перестановка границы, а не обязательный обмен двух рёбер дерева.

Например, есть две параллельные дуги A→B и спрос на 5 единиц. Дорогая дуга стоит 5, имеет ёмкость 10 и входит в дерево с потоком 3. Дешёвая стоит 1, имеет ёмкость 2 и насыщена потоком 2 вне дерева. При π(A)=0,π(B)=5 её цена r=−4. Это уже оптимум: дешёвую дугу увеличить нельзя, её обратная остаточная цена +4. Правило «каждое отрицательное r вне дерева требует входа» здесь было бы неверно.

Другой старт: все 5 единиц на дорогой дуге, дешёвая пуста. Тогда по циклу можно перенести две единицы: запас дешёвой дуги 2 ограничивает шаг раньше, чем опустеет дорогая. Дешёвая насыщается и остаётся небазисной, стоимость падает с 25 до 17. Это проверка, что границы нужны и в выборе направления, и в ограничении шага.

Разбор: шесть складов, восемь магазинов

Программа решает транспортную задачу сетевым симплексом со стартом через искусственный корень и сверяет стоимость с алгоритмом дешёвых путей из прошлого урока.

setevoj.py
import random

rng = random.Random(22)
sklady, magaziny = 6, 8
n = sklady + magaziny                 # вершины 0..n−1, корень R = n
zapas = [rng.randint(10, 40) for _ in range(sklady)]
spros = [0] * magaziny
for _ in range(sum(zapas)):           # спрос в сумме равен запасу
    spros[rng.randrange(magaziny)] += 1
b = zapas + [-x for x in spros] + [0]
dugi = [(i, sklady + j, rng.randint(1, 30))       # (откуда, куда, цена)
        for i in range(sklady) for j in range(magaziny)]


def setevoj_simpleks():
    R, M = n, 10 ** 6                 # искусственные дуги - дорогие
    vse = list(dugi)
    potok = {}
    for v in range(n):                # старт: всё через корень
        d = (v, R, M) if b[v] >= 0 else (R, v, M)
        vse.append(d)
        potok[len(vse) - 1] = abs(b[v])
    derevo = set(potok)
    oborotov = 0
    while True:
        sos = {x: [] for x in range(n + 1)}
        for k in derevo:
            u, v, _ = vse[k]
            sos[u].append((v, k))
            sos[v].append((u, k))
        pi, otec, glub = {R: 0}, {R: None}, {R: 0}
        stek = [R]
        while stek:                   # потенциалы: на дереве r = 0
            x = stek.pop()
            for y, k in sos[x]:
                if y not in pi:
                    u, v, c = vse[k]
                    pi[y] = pi[x] + c if u == x else pi[x] - c
                    otec[y], glub[y] = (x, k), glub[x] + 1
                    stek.append(y)
        r = lambda k: vse[k][2] + pi[vse[k][0]] - pi[vse[k][1]]
        vhod = min((k for k in range(len(vse)) if k not in derevo),
                   key=r)
        u, v, c = vse[vhod]
        if r(vhod) >= 0:              # отрицательных цен нет - оптимум
            break
        vpered, nazad = [vhod], []    # цикл: дуга u→v и путь v … u
        a, z = u, v
        while a != z:                 # поднимаемся к общему предку
            if glub[a] >= glub[z]:
                x, k = otec[a]
                (vpered if vse[k][1] == a else nazad).append(k)
                a = x
            else:
                x, k = otec[z]
                (vpered if vse[k][0] == z else nazad).append(k)
                z = x
        teta = min(potok[k] for k in nazad)
        for k in vpered:
            potok[k] = potok.get(k, 0) + teta
        for k in nazad:
            potok[k] -= teta
        uhod = next(k for k in nazad if potok[k] == 0)
        derevo.add(vhod)
        derevo.remove(uhod)
        oborotov += 1
    assert all(potok.get(k, 0) == 0 for k in range(len(dugi), len(vse)))
    return sum(potok.get(k, 0) * vse[k][2] for k in range(len(dugi))), \
        oborotov


def ssp():                            # проверка: дешёвые пути по одному
    INF = float("inf")
    S, T = n, n + 1
    ost, cena = {}, {}
    def dobavit(u, v, cap, c):
        ost[u, v] = ost.get((u, v), 0) + cap
        ost.setdefault((v, u), 0)
        cena[u, v], cena[v, u] = c, -c
    for u, v, c in dugi:
        dobavit(u, v, INF, c)
    for i in range(sklady):
        dobavit(S, i, zapas[i], 0)
    for j in range(magaziny):
        dobavit(sklady + j, T, spros[j], 0)
    itog = shagov = 0
    while True:
        d, ot = {S: 0}, {}
        for _ in range(n + 2):        # Беллман-Форд по остаточной сети
            for (u, v), cap in ost.items():
                if cap > 0 and u in d:
                    if d[u] + cena[u, v] < d.get(v, INF):
                        d[v], ot[v] = d[u] + cena[u, v], u
        if T not in d:
            return itog, shagov
        put, v = [], T
        while v != S:
            put.append((ot[v], v))
            v = ot[v]
        f = min(ost[e] for e in put)
        for u, v in put:
            ost[u, v] -= f
            ost[v, u] += f
        itog, shagov = itog + f * d[T], shagov + 1


a, ob = setevoj_simpleks()
c2, sh = ssp()
print("сетевой симплекс: стоимость", a, " оборотов", ob)
print("дешёвые пути:     стоимость", c2, " путей", sh)
↳  выводpython3 setevoj.py
сетевой симплекс: стоимость 997  оборотов 14
дешёвые пути:     стоимость 997  путей 14

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

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

Первая ошибка - считать потенциалы не по дереву, а кратчайшими путями: на рёбрах дерева приведённая цена должна быть ровно нулём, иначе оборот выбирает не тот цикл. Вторая - перепутать попутные и встречные рёбра цикла: уменьшать надо встречные, и θ - минимум именно по ним; перепутав, получишь отрицательный поток. Третья - вырожденные обороты с θ = 0: дерево меняется, а план - нет, и простое правило выбора может ходить по кругу бесконечно; промышленные реализации держат «сильно допустимые» деревья, где такой круг невозможен. Четвёртая - забыть, что искусственные рёбра с потоком в конце - это ответ «план невыполним», а не часть плана.

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

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

Теперь сам

В разборе после оборота дерево - A→X, A→Y, B→Y. Склад B получил ещё одну единицу запаса, а магазин X - ещё одну единицу спроса. На сколько вырастет оптимальная стоимость и как это видно по потенциалам?

Ответ. Лишняя единица из B в X стоит разность потенциалов: π(X) − π(B) = 2 − 3 = −1 - стоимость УПАДЁТ на единицу, до 16. Звучит странно, но в дереве это видно: единица идёт B → Y (+1), и тогда A может везти в Y на единицу меньше (−4) и на единицу больше в X (+2): 1 − 4 + 2 = −1. Проверка: новый план A→X 5, B→Y 6, A→Y 0 стоит 10 + 6 = 16.

Главное

Дерево задаёт базис и потенциалы; небазисные дуги фиксированы на границах. Улучшение ищут в допустимом остаточном направлении: из нуля увеличивают, с верхней ёмкости уменьшают. Без верхних ёмкостей остаётся частный случай с нулевыми потоками вне дерева, который реализован в программе. Предел шага определяется границами всех дуг цикла.

Комментарии

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

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

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