Изменения

Перейти к: навигация, поиск

Задача о порядке перемножения матриц

343 байта добавлено, 14:20, 2 ноября 2020
Опечатка
{{Задача|definition = Дана последовательность из <tex>n<//Не окончательный вариант --[[Участник:GosuGDR|GosuGDR]] 07:02tex> матриц, 10 декабря 2011 (MSK)требуется найти самый эффективный способ их перемножения.}}
'''Задача о порядке У нас есть множество способов перемножить матрицы, потому что операция перемножения матриц''' — классическая задача, которая может быть решена с помощью динамического программированияассоциативна. Нам дается последовательность матрицДругими словами, нет разницы в которой мы хотим найти самый эффективный способ их перемножения. На самом деле задача заключается не в нахождении результата перемножениякаком порядке расставляются скобки между множителями, а в нахождении нужного порядка этого перемножениярезультат будет один и тот же.
У нас есть множество способов перемножить, потому что операция перемножения ассоциативна. Другими словами, нет разницы в каком порядке мы расставим скобки между множителями, результат будет один [[Правильные скобочные последовательности | Расстановок скобок]] достаточно много и тот жеих количество очень быстро растет. Например, если у нас есть четыре матрицы ''A'', ''B'', ''C'' и ''D'', то существуют следующие варианты::(''ABC'')''D'' = (''AB'')(''CD'') = ''A''(''BCD'') = ''A''(''BC'')''D'' = Точное количество всевозможных вариантов равно <tex>n</tex>–ому [[Числа Каталана | числу Каталана]].... Однако, порядок в котором мы расставим расставляются скобки между матрицами повлияет на количество арифметических операций, которые потребуются на вычисление ответа, или, другими словами, на ''эффективность''.
Например, предположим, что А <tex>\dim{A}= (10 &\times; 30)</tex>, <tex>\dim{B } = (30 &\times; 5)</tex>, <tex>\dim{C } = (5 &\times; 60)</tex>. Тогда:
:Для <tex> (''AB''A \times B)''\times C'' = </tex> будет <tex>(10&times;30&times;5\times30\times5) + (10&times;5&times;60\times5\times60) = 1500 + 3000 = 4500 </tex> операций:''Для <tex> A''\times(''BC''B \times C) = </tex> будет <tex>(30&times;5&times;60\times5\times60) + (10&times;30&times;60\times30\times60) = 9000 + 18000 = 27000 </tex> операций.
Как мы видим, первый способ гораздо эффективней. Теперь стало понятно, что нам надо найти оптимальную расстановку скобок в нашем выражении из ''n'' матриц. Как бы это сделать? Мы можем перебрать все расстановки скобок (brute force), но время выполнения этого алгоритма будет расти экспоненциально от ''n'' количества матриц. Решение данной задачи, как мы увидим — это разбить нашу задачу на подзадачи. Так же мы увидим, что с помощю решения однократного решения подзадачи и повторного использования ответа, мы сможем заметно сократить асимптотику.
== Решение задачи ==
== Решение задачи = Перебор всех вариантов ===
=== Перебор всех вариантов В данной задаче нужно узнать минимальное количество операций (Bruteforceили минимальную стоимость) ===, необходимых для перемножения матриц. Если перемножить только две матрицы, то можно осуществить это едиственным способом, следовательно минимальная стоимость — это стоимость перемножения этих двух матриц. В общем, можно найти минимальную стоимость используя следующий [[Динамическое программирование | рекурсивный алгоритм]]:
Сначала* взять последовательность матриц и разделить её на две части, давайте определимся, что мы хотим узнать минимальное количество операций (или * найти минимальную стоимость)перемножения на каждой подпоследовательности, необходимых для * сложить эти две стоимости и прибавить к этому стоимость перемножения двух получившихся матриц. Если мы перемножаем только две матрицы, то мы можем осуществить * сделать это едиственным способомдля каждой возможной позиции в последовательности, следовательно минимальная стоимость — это стоимость перемножения этих двух матрицв которой она может быть разделена и взять минимум среди всех результатов. В общем, мы можем найти минимальную стоимость используя следующий рекурсивный алгоритм:
* Взять последовательность матриц и разделить её на две частиИли другими словами, давайте обозначим через <tex>f(i, j)</tex> минимальное количество скалярных умножений для вычисления матрицы <tex>M_{i..j}</tex>, то получаем следующее рекуррентное соотношение:<tex> f(i,j) = \left \{ * Найти минимальную стоимость перемножения на каждой подпоследовательности.\begin{array}{ll}* Сложить эти две стоимости и прибавить к этому стоимость перемножения двух получившихся матриц. 0, & i=j \\* Сделать это для каждой возможной позиции в последовательности \min\limits_{i \leqslant k < j}{(f(i,k) + f(k+1, в которой она может быть разделена и взять минимум среди всех результатовj) + p_{i-1}p_kp_j)} & i < j \end{array} \right.</tex>
НапримерОбъясняется оно просто: для того, если у нас чтобы найти произведение матриц <tex>M_{i..j}</tex> при <tex>i=j</tex> не нужно ничего делать — это и есть четыре сама матрица <tex>M_i</tex>. При нетривиальном случае мы перебираем все точки разбиения матрицы <tex>M_{i..j}</tex> на матрицы ''ABCD''<tex>M_{i..k}</tex> и <tex>M_{k+1..j}</tex>, то мы посчитаем для (''A'')(''BCD''), (''AB'')(''CD'')ищем количество операций, необходимое чтобы их получить и затем перемножаем для получения матрицы <tex>M_{i..j}</tex>.(''ABC'')(''D'')Оно будет равно кол-ву операций, делая рекурсивные вызовы потраченное на отрезках ''ABC'', ''AB'', ''CD'', и ''BCD'', чтобы найти минимальную решение подзадач + стоимостьумножения матриц <tex>M_{i..k}M_{k+1.. Потом среди них мы выбираем лучший вариантj}</tex>). Так жеСчитаем, этот алгоритм дает не только минимальную стоимость, но что размеры матриц заданы в массиве <tex>p</tex> и показывает наилучший способ перемножения матриц: нужно только сгрупировать тем же образом размер матрицы, каким дается нам минимальная стоимость<tex>M_i</tex> равен <tex>p_{i-1} \times p_i</tex>.
Однако, если мы применим этот алгоритм, то мы обнаружим, что он работает также медленно, как и наивный способ перебирания всех скобочных последовательностей! Что пошло не так? Ответом на этот вопрос является то факт, что мы делаем значительное количество ненужной работы. Например, в выше описанном алгоритме, мы делали рекурсивный вызов, чтобы найти наилучшую стоимость для подсчета ''ABC'' и ''AB''. Но нахождение наилучшей стоимости для подсчета ''ABC'' так же требует нахождения лучшей стоимости для ''AB''. Так как рекурсия растет вглубь все больше и больше, то и число ненужных повторений увеличивается.
=== Оптимизации динамическим программированием ===Одно из простых решений — это ''мемоизация''. Каждый раз, когда мы считаем минимальную стоимость перемножения определенной подпоследовательностиЧтобы привести пример, давайте мы будем запоминать ответвернемся к нашим матрицам. Если мы когда либо ещё раз захотим посчитать это ещё разу нас есть четыре матрицы <tex>ABCD</tex>, то мы уже будет иметь ответ и не будем пересчитывать. Поскольку существует всего около посчитаем для <mathtex>n^2(A)(BCD)</2tex>, <tex>(AB)(CD)</mathtex> подотрезков, где ''n'' — это количество матриц, то память занимаемая программой будет не так велика. Можно сказать, что с помощью этого простого трюка мы уменьшили асимптотику алгоритма и <tex>(переборABC) с O(D)</tex>, делая рекурсивные вызовы на отрезках <mathtex>2^nABC</mathtex>) до O(, <tex>AB</tex>,<tex>CD</tex>, и <mathtex>n^3BCD</mathtex>), что является достаточно эффективным для реальных приложенийчтобы найти минимальную стоимость. Потом среди них выбираем лучший вариант. Так же, этот алгоритм дает не только минимальную стоимость, но и показывает наилучший способ перемножения матриц: нужно только сгрупировать тем же образом матрицы, каким дается нам минимальная стоимость.
=== Восстановление ответа ===С помощью вышеописанного алгоритма мы можем восстановить порядокОднако, если применить этот алгоритм, то обнаружим, что он работает также медленно, как и наивный способ перебирания всех [[Правильные скобочные последовательности | скобочных последовательностей]]. Делается значительное количество ненужной работы. Например, в котором нам необходимо перемножать матрицывыше описанном алгоритме, осуществляется рекурсивный вызов, чтобы достичь минимального количества арифметических операцийнайти наилучшую стоимость для подсчета <tex>ABC</tex> и <tex>AB</tex>. Но нахождение наилучшей стоимости для подсчета <tex>ABC</tex> так же требует нахождения лучшей стоимости для <tex>AB</tex>. Так как рекурсия растет вглубь все больше и больше, затрачиваемых на вычисление ответато и число ненужных повторений увеличивается. Когда мы узнаемИтоговая асимптотика, как нам нужно разбить отрезок было сказано выше, равняется <tex>n</tex>–ому [[Числа Каталана | числу Каталана]], да плюс вычисление для каждой [[Правильные скобочные последовательности | правильной скобочной последовательности]] ''затрат'' на два подотрезка, перемножение (то при восстановлении ответа мы заключаем эти два подотрезкаесть <tex>O(последовательности матрицn \cdot C_n)</tex>) в скобки и передаем получившийся ответ выше по рекурсии. Так как <tex>N</tex>­-ое [[Числа Каталана | число Каталана]] равняется <tex dpi="163"> \frac{1}{n+1}{2 n \choose n} </tex> или асимптотически <tex dpi="163"> \frac{4^n}{n^{3/2}\sqrt{\pi}} </tex>, а это быстро возрастающая функция, нам бы хотелось решение, которое работает быстрее.
=== Псевдокод ===
<pre>
int dp[1000][1000];
vector<pair<int, int> > v;
// v[i].first — размер i-той матрицы по горизонтали
// v[i].second — размер i-той матрицы по вертикали
// dp[i][j] — меморизация на отрезке [i, j)
int matrixChainMultiplication(int l, int r)
{
//l — включая в отрезок
//r — исключая из отрезка
if dp[l][r] == -1 //Если значение динамики не посчитано
if l == r - 1
dp[l][r] = 0; //Если у нас подотрезок длины 1, то количество операций для перемножения равно нулю
else
dp[l][r] = INFINITY;
for (int i = l + 1; i < r; i++)
dp[l][r] = min(dp[l][r], v[l].first * v[i].first * v[r - 1].second + matrixChainMultiplication(l, i) + matrixChainMultiplication(i, r));
return dp[l][r];
}
</pre>
'''int''' dp[][] <font color="green">// dp[i][j] — ответ на отрезке [i, j)</font> '''int''' v[] <font color= Литература "green">// Массив v[] — хранит все размеры матриц по порядку // Так как у нас размеры соседних матриц по вертикали и горизонтали совпадают, то они занесены в этот массив однократно // l — включая в отрезок, r — исключая из отрезка. Изначально l =0, r =n, где n {{---}} длина последовательности</font> '''int''' matrixChainMultiplication('''int''' l, '''int''' r) '''if''' dp[l][r] == -1 <font color="green">// Если значение динамики не посчитано</font> '''if''' l == r - 1 dp[l][r] = 0 <font color="green"> // Если у нас подотрезок длины 1, то количество операций для перемножения равно нулю</font> '''else''' dp[l][r] = <tex>\infty</tex> '''for''' i = l + 1 '''to''' r - 1 dp[l][r] = min(dp[l][r], v[l] * v[i] * v[r] + matrixChainMultiplication(l, i) + matrixChainMultiplication(i, r)) '''return''' dp[l][r] == См. также == *[[Задача о наибольшей общей подпоследовательности ]]*[[Кратчайший путь в ациклическом графе ]]*[[Задача о расстановке знаков в выражении]]*[[Задача о выводе в контекстно-свободной грамматике, алгоритм Кока-Янгера-Касами | Aлгоритм Кока-Янгера-Касами ]]*[[Правильные скобочные последовательности | Правильные скобочные последовательности ]]== Источники информации == *[http://en.wikipedia.org/wiki/Matrix_chain_multiplication Wikipedia {{---}} Matrix chain multiplication]
* Томас Х. Кормен и др. Алгоритмы: построение и анализ
* Sanjoy Dasgupta , Christos H. Papadimitriou, Umesh Vazirani Algorithms
* Английская википедиа [http://en.wikipedia.org/wiki/Matrix_chain_multiplication Matrix chain multiplication]
[[Категория: Дискретная математика и алгоритмы]]
[[Категория:Динамическое_программирование]]
Анонимный участник

Навигация