Симплекс-метод

Симплекс-метод гениально прост: иди по рёбрам многогранника от угла к углу, всегда в сторону роста цели, пока расти некуда.

Симплекс-метод — алгоритм решения LP, переходящий между смежными вершинами многогранника по улучшающим рёбрам до достижения оптимальной вершины.

Идея за минуту

Раз оптимум в вершине, а вершин слишком много для перебора, симплекс действует как умный альпинист: стартует в какой-то вершине и идёт по ребру к соседней вершине, если та даёт больше цели. Повторяет, пока из текущей вершины нет улучшающего ребра — это и есть оптимум. На практике симплекс обходит лишь несколько вершин из астрономического их числа.

Стандартная форма и slack-переменные

Неравенства $Ax\le b$ превращают в равенства, добавляя дополнительные (slack) переменные $s\ge0$: ограничение $4x_1+2x_2\le 40$ становится $4x_1+2x_2+s_1=40$. Slack показывает неиспользованный ресурс. После этого задача — система линейных уравнений, и вершины соответствуют выбору, какие переменные «базисные» (ненулевые), а какие нули.

Симплекс-таблица: правила хода

  • Выбор входящей переменной (pivot column): та, что сильнее всего увеличивает цель (самый «выгодный» отрицательный коэффициент в строке цели для задачи на максимум).
  • Выбор выходящей переменной (pivot row): правило минимального отношения $b_i/a_{ij}$ — определяет, какое ограничение «упрётся» первым.
  • Pivot (исключение Гаусса): пересчитываем таблицу, меняя базис, — это шаг к соседней вершине.

Реализация на нашей задаче

Решим симплексом ту же задачу о цехе: $\max 30x_1+20x_2$ при $4x_1+2x_2\le40$, $2x_1+3x_2\le42$, $x\ge0$. Реализуем табличный симплекс на чистом Python.

def simplex(c, A, b):
    m, n = len(A), len(c)
    # таблица: столбцы = n переменных + m slack + RHS; нижняя строка = -c (цель)
    T = [row[:] + [1 if j == i else 0 for j in range(m)] + [b[i]]
         for i, row in enumerate(A)]
    T.append([-ci for ci in c] + [0] * m + [0])
    width = n + m + 1
    while True:
        # входящий столбец: самый отрицательный коэффициент в строке цели
        last = T[-1]
        col = min(range(width - 1), key=lambda j: last[j])
        if last[col] >= -1e-9:
            break                    # все неотрицательны -> оптимум
        # выходящая строка: минимальное отношение RHS / элемент
        ratios = []
        for i in range(m):
            a = T[i][col]
            ratios.append(T[i][-1] / a if a > 1e-9 else float('inf'))
        row = min(range(m), key=lambda i: ratios[i])
        # pivot
        piv = T[row][col]
        T[row] = [v / piv for v in T[row]]
        for i in range(m + 1):
            if i != row and abs(T[i][col]) > 1e-12:
                factor = T[i][col]
                T[i] = [T[i][j] - factor * T[row][j] for j in range(width)]
    return T[-1][-1]                  # значение цели в правом нижнем углу

c = [30, 20]
A = [[4, 2], [2, 3]]
b = [40, 42]
print("Оптимальная прибыль:", round(simplex(c, A, b), 2))

Вывод:

Оптимальная прибыль: 355.0

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

Как работает под капотом

Каждый pivot — это шаг от одной базисной вершины к смежной с гарантированным (нестрогим) ростом цели. Поскольку цель монотонно не убывает, а вершин конечно, метод завершается (защита от зацикливания на вырожденных задачах — правило Бленда). В худшем теоретическом случае симплекс может обойти экспоненциально много вершин (примеры Кли–Минти), но на реальных задачах он почти всегда невероятно быстр — это одна из самых успешных эвристик в истории вычислений. Гарантированно полиномиальные альтернативы — методы внутренней точки.

Частые ошибки

  • Неверное правило отношения. Брать минимум $b_i/a_{ij}$ только по положительным $a_{ij}$; иначе нарушится допустимость.
  • Зацикливание на вырождении. При равных отношениях нужно правило Бленда, иначе метод может застрять.
  • Забыть про неограниченность. Если в улучшающем столбце нет положительных элементов — цель неограничена сверху.

Итоги

  • Симплекс идёт по рёбрам от вершины к вершине, увеличивая цель, пока есть улучшающее ребро.
  • Slack-переменные превращают неравенства в равенства; ходы — это pivot (Гаусс) в симплекс-таблице.
  • На практике очень быстр, хотя в худшем случае экспоненциален; альтернатива — внутренняя точка.
Проверьте себя
1. Как симплекс-метод ищет оптимум?
AПеребирает все вершины
BИдёт по рёбрам от вершины к смежной, улучшая цель
CИдёт сквозь центр многогранника
DСлучайно выбирает точки
2. Зачем нужны slack-переменные?
AЧтобы убрать цель
BЧтобы превратить неравенства $\le$ в равенства
CЧтобы увеличить размерность ради красоты
DЧтобы сделать задачу нелинейной
3. Что верно о сложности симплекс-метода?
AВсегда полиномиален
BВ худшем случае экспоненциален, но на практике очень быстр
CВсегда экспоненциален и медлен
DНе завершается