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

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

Цепочка матриц: динамика по отрезкам и где ставить скобки

Коротко

Произведение A1 · A2 · … · An можно считать в любом порядке скобок - результат один, а цена разная: умножение матриц p × q и q × r стоит p·q·r умножений чисел. На трёх матрицах выбор скобок меняет цену в десять раз, на десяти - в тридцать с лишним. Расстановок экспоненциально много (это числа Каталана), а динамика находит лучшую за Θ(n³). Состояние - ОТРЕЗОК цепочки i..j; переход - выбор, после какой матрицы k делается последнее умножение: m[i][j] = min по k (m[i][k] + m[k+1][j] + p[i−1]·p[k]·p[j]). Отрезки считают по возрастанию длины. Это динамика по отрезкам - третий вид состояния после префиксов и пар префиксов.

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

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

Три матрицы: A1 размером 10 × 100, A2 - 100 × 5, A3 - 5 × 50. Умножение матрицы p × q на q × r даёт p × r и стоит p·q·r умножений чисел: каждый из p·r элементов ответа - сумма q произведений.

Прежде чем читать дальше, посчитай: сколько стоит (A1 · A2) · A3 и сколько A1 · (A2 · A3)? Ответ один и тот же - умножение матриц ассоциативно. А цена?

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

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

Механизм: отрезок, последнее умножение, порядок по длине

матрица A_i имеет размер p[i−1] × p[i]
m[i][j] - наименьшая цена произведения A_i · … · A_j

база:     m[i][i] = 0          (одна матрица - ничего не умножаем)
переход:  m[i][j] = min по k от i до j−1:
              m[i][k]          левая часть A_i … A_k, p[i−1] × p[k]
            + m[k+1][j]        правая часть,          p[k] × p[j]
            + p[i−1]·p[k]·p[j] последнее умножение двух частей

порядок:  по возрастанию длины отрезка j − i - тогда обе части
          уже посчитаны
ответ:    m[1][n];  запоминаем лучшее k - по нему строятся скобки

Состояний n²/2, у каждого до n вариантов k - Θ(n³). Перебор всех расстановок - числа Каталана: 1, 2, 5, 14, 42, …, 4862 для десяти матриц, и дальше растут примерно как 4^n.

Почему подзадачи независимы: если последнее умножение - после A_k, то левая половина считается отдельно и даёт матрицу p[i−1] × p[k] при ЛЮБОЙ своей расстановке скобок. Её лучшая расстановка не зависит от правой - оптимальная подструктура.

Правило

Цепочку матриц считают динамикой по отрезкам: m[i][j] - лучшая цена произведения A_i … A_j, переход перебирает место последнего умножения k: m[i][k] + m[k+1][j] + p[i−1]·p[k]·p[j]. Отрезки заполняют по возрастанию длины, цена - Θ(n³) против экспоненциального числа расстановок. Скобки восстанавливают по запомненному лучшему k, рекурсивно.

Разбор: три матрицы и четвёртая

p = [10, 100, 5, 50]:   A1 10×100,  A2 100×5,  A3 5×50
(A1·A2)·A3 = 10·100·5 + 10·5·50     =  5000 + 2500  =  7500
A1·(A2·A3) = 100·5·50 + 10·100·50   = 25000 + 50000 = 75000

добавим A4 размером 50×1:  p = [10, 100, 5, 50, 1]
длина 2:  m[1][2] = 5000   m[2][3] = 25000   m[3][4] = 250
длина 3:  m[1][3] = min(0 + 25000 + 50000,  5000 + 0 + 2500) = 7500
          m[2][4] = min(0 + 250 + 500,  25000 + 0 + 5000)    = 750
длина 4:  m[1][4] = min( k=1:    0 + 750  + 10·100·1 = 1750,
                         k=2: 5000 + 250  + 10·5·1   = 5300,
                         k=3: 7500 + 0    + 10·50·1  = 8000 )  = 1750
скобки: A1 · (A2 · (A3 · A4))

Без A4 лучшим было «сначала A1·A2», с ней - ровно противоположное: сначала свернуть хвост до узкого столбца 5 × 1. Локального правила здесь нет: на трёх матрицах лучшим было начать с A1 · A2, а с четвёртой матрицей это умножение в оптимальный порядок не входит вовсе - решение видно только из полной таблицы.

Разбор: десять матриц и все 4862 расстановки

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

cepochka.py
import random
from functools import lru_cache

rng = random.Random(24)
p = [rng.choice((5, 10, 20, 50, 100, 200, 500)) for _ in range(11)]
n = len(p) - 1                        # A_i размером p[i-1] × p[i]


def po_poryadku():                    # слева направо: ((A1 A2) A3) ...
    return sum(p[0] * p[k - 1] * p[k] for k in range(2, n + 1))


def dinamika():                       # m[i][j] - лучшая цена A_i … A_j
    m = [[0] * (n + 1) for _ in range(n + 1)]
    razrez = [[0] * (n + 1) for _ in range(n + 1)]
    for dlina in range(2, n + 1):     # по возрастанию длины отрезка
        for i in range(1, n - dlina + 2):
            j = i + dlina - 1
            m[i][j] = float("inf")
            for k in range(i, j):     # последнее умножение - после A_k
                c = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j]
                if c < m[i][j]:
                    m[i][j], razrez[i][j] = c, k
    def skobki(i, j):
        if i == j:
            return f"A{i}"
        k = razrez[i][j]
        return f"({skobki(i, k)} {skobki(k + 1, j)})"
    return m[1][n], skobki(1, n)


@lru_cache(None)
def vse_rasstanovki(i, j):            # перебор всех скобок: цены
    if i == j:
        return [0]
    return [a + b + p[i - 1] * p[k] * p[j]
            for k in range(i, j)
            for a in vse_rasstanovki(i, k)
            for b in vse_rasstanovki(k + 1, j)]


cena, skobki = dinamika()
vse = vse_rasstanovki(1, n)
print("размеры:", p)
print("слева направо:", po_poryadku())
print("динамика:     ", cena)
print("скобки:", skobki)
print("перебор", len(vse), "расстановок: лучшая", min(vse),
      " худшая", max(vse))
↳  выводpython3 cepochka.py
размеры: [200, 50, 500, 100, 10, 10, 10, 10, 10, 200, 200]
слева направо: 23680000
динамика:      1658000
скобки: ((A1 ((A2 (A3 A4)) (A5 (A6 (A7 A8))))) (A9 A10))
перебор 4862 расстановок: лучшая 1658000  худшая 55280000

Динамика совпала с перебором. Лучший порядок в четырнадцать раз дешевле порядка «слева направо» и в тридцать три раза дешевле худшего. Обрати внимание на скобки: внутренние группы сворачивают узкие матрицы 10 × 10 раньше, чем трогать широкие 200 × 50 и 500 × 100.

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

Первая ошибка - заполнять таблицу по строкам i, а не по длине отрезка: для m[1][4] нужны m[2][4] и m[1][3], а построчный порядок посчитает m[1][4] раньше m[2][4]. Вторая - забыть третье слагаемое p[i−1]·p[k]·p[j] или взять в нём не те индексы: размер левой части - p[i−1] × p[k], правой - p[k] × p[j]. Третья - пытаться решать жадно (самое дешёвое умножение первым): на многих цепочках это далеко от оптимума. Четвёртая - не запомнить k и потом не суметь выписать скобки.

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

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

Теперь сам

p = [5, 10, 3, 12]: A1 - 5 × 10, A2 - 10 × 3, A3 - 3 × 12. Какая расстановка дешевле и сколько она стоит?

Ответ. (A1·A2)·A3 = 5·10·3 + 5·3·12 = 150 + 180 = 330. A1·(A2·A3) = 10·3·12 + 5·10·12 = 360 + 600 = 960. Дешевле (A1·A2)·A3 за 330: сначала схлопнуть внутреннюю размерность 10 до 3. Таблица динамики даёт то же: m[1][2] = 150, m[2][3] = 360, m[1][3] = min(0 + 360 + 600, 150 + 0 + 180) = 330.

Главное

Цепочку матриц решает динамика по отрезкам: состояние - отрезок i..j, переход - место последнего умножения k, слагаемые - две половины и цена их перемножения p[i−1]·p[k]·p[j]. Отрезки считают по возрастанию длины; Θ(n³) против экспоненциального числа расстановок. На десяти матрицах лучший порядок в 14 раз дешевле «слева направо», и добавление одной матрицы может перевернуть оптимальные скобки целиком.

Комментарии

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

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

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