Введение
Задача математической оптимизации. Умножение цепочки матриц (или задача об оптимальном порядке умножения матриц) вычисляет, для каждого 2 ≤ k ≤ n, минимальную стоимость всех подцепочек длины k, используя стоимость уже вычисленных меньших подцепочек. Она имеет ту же асимптотическую сложность и не требует рекурсии. Псевдокод:
// Матрица A[i] имеет размерность dims[i-1] x dims[i] для i = 1 до n
MatrixChainOrder(int dims[])
{
// length[dims] = n + 1
n = dims.length - 1;
// m[i,j] = Минимальное количество скалярных умножений (т.е. стоимость),
// необходимое для вычисления матрицы A[i]A[i+1]…A[j] = A[i…j]
// Стоимость равна нулю при умножении одной матрицы
for (i = 1; i <= n; i++)
m[i, i] = 0;
Matrix chain multiplication (or the matrix chain ordering problem computes, for each 2 ≤ k ≤ n, the minimum costs of all subsequences of length k using the costs of smaller subsequences already computed. It has the same asymptotic runtime and requires no recursion. Pseudocode:
// Matrix A[i] has dimension dims[i 1] x dims[i] for i = 1 n
MatrixChainOrder(int dims[])
{
// length[dims] = n + 1
n = dims. length 1;
// m[i,j] = Minimum number of scalar multiplications (i. e., cost)
// needed to compute the matrix A[i]A[i+1] A[j] = A[i j]
// The cost is zero when multiplying one matrix
for (i = 1; i <= n; i++)
m[i, i] = 0;
for (len = 2; len <= n; len++) { // Длины подцепочек
for (i = 1; i <= n - len + 1; i++) {
j = i + len - 1;
m[i, j] = MAXINT;
for (k = i; k <= j - 1; k++) {
cost = m[i, k] + m[k+1, j] + dims[i-1]*dims[k]*dims[j];
if (cost < m[i, j]) {
m[i, j] = cost;
s[i, j] = k; // Индекс разбиения подцепочки, при котором достигнута минимальная стоимость
}
}
}
}
}
Примечание: Первый индекс для dims равен 0, а первый индекс для m и s равен 1. Реализация на Python с использованием декоратора memoization из стандартной библиотеки:
for (i = 1; i <= n len + 1; i++) {
j = i + len 1;
m[i, j] = MAXINT;
for (k = i; k <= j 1; k++) {
cost = m[i, k] + m[k+1, j] + dims[i 1]*dims[k]*dims[j];
if (cost < m[i, j]) {
m[i, j] = cost;
s[i, j] = k; // Index of the subsequence split that achieved minimal cost
}
}
}
}
}
Note : The first index for dims is 0 and the first index for m and s is 1. A Python implementation using the memoization decorator from the standard library:
from functools import cache
def matrixChainOrder(dims: list[int]) -> int:
@cache
def a(i, j):
return min((a(i, k) + dims[i] * dims[k] * dims[j] + a(k, j)
for k in range(i + 1, j)), default=0)
@cache
def a(i, j):
return min((a(i, k) + dims[i] * dims[k] * dims[j] + a(k, j)
for k in range(i + 1, j)), default=0)
return a(0, len(dims) - 1)
Более эффективные алгоритмы
Существуют алгоритмы, которые эффективнее алгоритма динамического программирования со сложностью O(n³), хотя они и более сложные.
Другие алгоритмы O ((n log n)
Ван, Чжу и Тянь опубликовали упрощенный алгоритм O(n log m), где n – количество матриц в цепи, а m – количество локальных минимумов в последовательности размеров данной цепи матриц. Нимбарк, Гохель и Доши опубликовали жадный алгоритм O(n log n), но их доказательство оптимальности неверно, и их алгоритм не способен найти наиболее эффективное расстановка скобок для некоторых цепей матриц. Алгоритм Hu & Shing выполняется за O(n) и выдает расстановку скобок, которая не более чем на 15,47% хуже оптимального решения. В большинстве случаев алгоритм находит оптимальное решение или решение, которое лишь на 1–2% хуже оптимального. Одним из несколько искусственных частных случаев является конкатенация строк из списка. Например, в C стоимость конкатенации двух строк длиной m и n с использованием strcat составляет O(m + n), поскольку требуется O(m) времени для поиска конца первой строки и O(n) времени для копирования второй строки в конец первой. Используя эту функцию стоимости, можно разработать алгоритм динамического программирования для поиска наиболее быстрого способа конкатенации последовательности строк. Однако эта оптимизация в значительной степени бесполезна, поскольку строки можно напрямую конкатенировать за время, пропорциональное сумме их длин. Аналогичная проблема возникает для односвязных списков. Другое обобщение заключается в решении задачи при наличии параллельных процессоров. В этом случае, вместо суммирования затрат на вычисление каждого множителя матричного произведения, мы берем максимум, поскольку вычисления можно выполнять одновременно. Это может существенно повлиять как на минимальную стоимость, так и на окончательную оптимальную группировку; предпочтительны более "сбалансированные" группировки, которые обеспечивают загрузку всех процессоров. Существуют и более сложные подходы.