Черновик:Перемножение матриц — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(A Dynamic Programming Algorithm)
м (rollbackEdits.php mass rollback)
 
(не показано 6 промежуточных версий 4 участников)
Строка 1: Строка 1:
'''Задача о порядке перемножения матриц''' — классическая задача, которая может быть решена с помощью динамического программирования. В этой задаче нам последовательность матриц, в которой мы хотим найти самый эффективный способ перемножения их. На самом деле задача заключается не в нахождении результата перемножения, а просто нужно найти порядок, в котором нам надо их перемножить.
+
'''Задача о порядке перемножения матриц''' — классическая задача, которая может быть решена с помощью динамического программирования. В этой задаче нам дана последовательность матриц, в которой мы хотим найти самый эффективный способ перемножения. На самом деле задача заключается не в нахождении результата перемножения, а просто в нахождении нужного порядока, в котором мы будем перемножать.
  
У нас есть много способов, потому что перемножение ассоциативно. Другими словами, нет разницы как мы расставим скобки между множителями, результат будет один и тот же.  Например, если у нас есть четыре матрицы ''A'', ''B'', ''C'' и ''D'', то у нас есть следующие варианты:
+
У нас есть много способов, потому что операция перемножения ассоциативна. Другими словами, нет разницы как мы расставим скобки между множителями, результат будет один и тот же.  Например, если у нас есть четыре матрицы ''A'', ''B'', ''C'' и ''D'', то у нас есть следующие варианты:
 
:(''ABC'')''D'' = (''AB'')(''CD'') = ''A''(''BCD'') = ''A''(''BC'')''D'' = ....
 
:(''ABC'')''D'' = (''AB'')(''CD'') = ''A''(''BCD'') = ''A''(''BC'')''D'' = ....
  
Однако, порядок в котором мы расставим скобки в нашем выражении повлияет на количество арифметических операций, которые мы потратим на вычисление ответа, или, другими словами, на ''эффективность''.
+
Однако, порядок в котором мы расставим скобки в нашем выражении повлияет на количество простых арифметических операций, которые мы потратим на вычисление ответа, или, другими словами, на ''эффективность''.
 
   
 
   
 
Например, предположим, что А = (10 × 30), B = (30 × 5), C = (5 × 60). Тогда:
 
Например, предположим, что А = (10 × 30), B = (30 × 5), C = (5 × 60). Тогда:
Строка 12: Строка 12:
  
 
Очевидно, что первый способ гораздо эффективней. Теперь мы поняли, что нам надо найти оптимальную расстановку скобок в нашем выражении из ''n'' матриц.   
 
Очевидно, что первый способ гораздо эффективней. Теперь мы поняли, что нам надо найти оптимальную расстановку скобок в нашем выражении из ''n'' матриц.   
Как это сделать? Мы можем перебрать все расстановки скобок (brute force), но время выполнение этого алгоритма будет эксапаненциально рости от ''n'' количества матриц. Решение данной проблемы, как мы увидим — это разбить нашу задачу на подзадачи. С помощю решения подзадач по одному разу и повторного использования решения, мы сможем заметно сократить асимптотику. Эта задача входит в классические задачи
+
Как это сделать? Мы можем перебрать все расстановки скобок (brute force), но время выполнение этого алгоритма будет эксапаненциально рости от ''n'' количества матриц. Решение данной проблемы, как мы увидим — это разбить нашу задачу на подзадачи. Так же мы увидим, что с помощю решения однократного решения подзадачи  и повторного использования ответа, мы сможем заметно сократить асимптотику.
  
 
== Решение динамическим программированием ==
 
== Решение динамическим программированием ==
  
Сначала, давайте считать то, что мы реально хотим знать минимальную стоимость или минимальное количесвто операций, необходимых для перемножения матриц. Если мы перемножаем только две матрицы, то существует едиственный способ сделать это, следовательно минимальная стоимость это стоимость этого перемножения. В общем, мы моежм найти минимальную стоимость испозльуя следующий рекурсивный алгоритм:
+
Сначала, давайте считать то, что мы хотим знать минимальное количесвто операций (или минимальную стоимость), необходимых для перемножения матриц. Если мы перемножаем только две матрицы, то мы можем осуществить это только едиственным способом, следовательно минимальная стоимость это стоимость этого перемножения. В общем, мы можем найти минимальную стоимость используя следующий рекурсивный алгоритм:
  
 
* Взять последовательность матриц и разделить её на две части.
 
* Взять последовательность матриц и разделить её на две части.
Строка 23: Строка 23:
 
* Сделать это для каждой возможной позиции в последовательности, в которой она может быть разделена и взять минимум среди всех результатов.
 
* Сделать это для каждой возможной позиции в последовательности, в которой она может быть разделена и взять минимум среди всех результатов.
  
Для пример, если у нас есть четыре матрицы ''ABCD'', мы посчитаем для (''A'')(''BCD''), (''AB'')(''CD''), и (''ABC'')(''D''), делая рекурсивные вызовы, чтобы найти минимальную стоимость на ''ABC'', ''AB'', ''CD'', и ''BCD''. Потом мы выбираем лучший вариант. Так же, этот алгоритм дает не только минимальную стоимость, но и показывает наилучший способ перемножить матрицы: нужно только сгрупировать тем же образом, каким дается нам минимальная стоимость и сделать тоже самое для каждого множителя.
+
Например, если у нас есть четыре матрицы ''ABCD'', мы посчитаем для (''A'')(''BCD''), (''AB'')(''CD''), и (''ABC'')(''D''), делая рекурсивные вызовы, чтобы найти минимальную стоимость на ''ABC'', ''AB'', ''CD'', и ''BCD''. Потом среди них мы выбираем лучший вариант. Так же, этот алгоритм дает не только минимальную стоимость, но и показывает наилучший способ перемножения матриц: нужно только сгрупировать тем же образом, каким дается нам минимальная стоимость и перемножить их между собой.
  
Внезапно, если мы применим этот алгоритм, то мы обнаружим, что это так же медленно, как и наивный способ перебирания всех скобочных последовательностей! Что пошло не так? Ответом является то, что мы делаем много ненужной работы. Например, выше мы сделали рекурсивный вызов, чтобы найти наилучшую стоимость для подсчета ''ABC'' и ''AB''. Но нахождение лучшей стоимости для подсчета ''ABC'' так же требует нахождения лучшей стоимости для ''AB''. Так как рекурсия растет вглбь все больше и больше, то и число ненужных повторений увеличивается.
+
Внезапно, если мы применим этот алгоритм, то мы обнаружим, что это так же медленно, как и наивный способ перебирания всех скобочных последовательностей! Что пошло не так? Ответом на этот вопрос является то факт, что мы делаем много ненужной работы. Например, в выше описанном алгоритме, мы сделали рекурсивный вызов, чтобы найти наилучшую стоимость для подсчета ''ABC'' и ''AB''. Но нахождение наилучшей стоимости для подсчета ''ABC'' так же требует нахождения лучшей стоимости для ''AB''. Так как рекурсия растет вглбь все больше и больше, то и число ненужных повторений увеличивается.
  
 +
Одно из простых решений: ''меморизация''. Каждый раз, когда мы считаем минимальную стоимость на отрезке, мы сохраняем ответ. Когда у нас просят посчитать это ещё раз, то мы сразу же выдадим ответ и не будем пересчитывать. Хоть у нас <math> n^2/2 </math>
  
Pseudocode (without memoization):
+
Одно из простых решений — это меморизация. Каждый раз, когда мы считаем минимальную стоимость перемножения определенной подпоследовательности, давайте мы будем запоминать ответ. Если мы когда либо ещё раз захотим посчитать это ещё раз, то мы уже будет иметь ответ и не будем пересчитывать. Поскольку существует всего около <math>n^2/2</math>, где ''n'' — это количество матриц, то память занимаемая программой будет не так велика. Можно сказать, что с помощью этого простого трюка мы уменьшили асимптотику алгоритма с O(<math>2n</math>) до O(<math>n^3</math>), что является достаточно эффективным для реальных приложений.
 +
 
 +
Псевдокод:
 
<pre>
 
<pre>
// Matrix Ai has dimension p[i-1] x p[i] for i = 1..n
+
 
Matrix-Chain-Order(int p[])
+
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)
 
{
 
{
    // length[p] = n + 1
+
//l — включая в отрезок
    n = p.length - 1;
+
//r — исключая из отрезка
    // m[i,j] = Minimum number of scalar multiplications (i.e., cost)
+
if dp[l][r] == -1
    // needed to compute the matrix A[i]A[i+1]...A[j] = A[i..j]
+
if l == r - 1
    // cost is zero when multiplying one matrix
+
dp[l][r] = 0;
    for (i = 1; i <= n; i++)
+
else
      m[i,i] = 0;
+
dp[l][r] = 1000 * 1000 * 1000;
 
+
for (int i = l + 1; i < r; i++)
    for (L=2; L<=n; L++) { // L is chain length
+
dp[l][r] = min(dp[l][r], v[l].first * v[i].first * v[r - 1].second + matrixChainMultiplication(l, i) + matrixChainMultiplication(i, r));
        for (i=1; i<=n-L+1; i++) {
+
return dp[l][r];
            j = i+L-1;
 
            m[i,j] = MAXINT;
 
            for (k=i; k<=j-1; k++) {
 
                // q = cost/scalar multiplications
 
                q = m[i,k] + m[k+1,j] + p[i-1]*p[k]*p[j];
 
                if (q < m[i,j]) {
 
                    m[i,j] = q;
 
                    // s[i,j] = Second auxiliary table that stores k
 
                    // k      = Index that achieved optimal cost
 
                    s[i,j] = k;
 
                }
 
            }
 
        }
 
    }
 
 
}
 
}
 
</pre>
 
</pre>
  
*Note : The first index for p is 0 and the first index for m and s is 1
+
*Замечания : Индексирование массива ''p'' начинается с нуля, а у массивов ''m'' и ''s'' с единицы.
Another solution is to anticipate which costs we will need and precompute them. It works like this:
 
* For each ''k'' from 2 to ''n'', the number of matrices:
 
** Compute the minimum costs of each subsequence of length ''k'', using the costs already computed.
 
When we're done, we have the minimum cost for the full sequence. Although it also requires O(''n''<sup>3</sup>) time, this approach has the practical advantages that it requires no recursion, no testing if a value has already been computed, and we can save space by throwing away some of the subresults that are no longer needed. This is bottom-up dynamic programming: a second way by which this problem can be solved.
 
  
 
==Ссылки==
 
==Ссылки==
Строка 72: Строка 62:
 
* Томас Х. Кормен и др. Алгоритмы: построение и анализ
 
* Томас Х. Кормен и др. Алгоритмы: построение и анализ
 
* Sanjoy Dasgupta , Christos H. Papadimitriou, Umesh Vazirani Algorithms
 
* Sanjoy Dasgupta , Christos H. Papadimitriou, Umesh Vazirani Algorithms
 
[[Категория: Дискретная математика и алгоритмы]]
 
[[Категория:Динамическое_программирование]]
 

Текущая версия на 19:41, 4 сентября 2022

Задача о порядке перемножения матриц — классическая задача, которая может быть решена с помощью динамического программирования. В этой задаче нам дана последовательность матриц, в которой мы хотим найти самый эффективный способ перемножения. На самом деле задача заключается не в нахождении результата перемножения, а просто в нахождении нужного порядока, в котором мы будем перемножать.

У нас есть много способов, потому что операция перемножения ассоциативна. Другими словами, нет разницы как мы расставим скобки между множителями, результат будет один и тот же. Например, если у нас есть четыре матрицы A, B, C и D, то у нас есть следующие варианты:

(ABC)D = (AB)(CD) = A(BCD) = A(BC)D = ....

Однако, порядок в котором мы расставим скобки в нашем выражении повлияет на количество простых арифметических операций, которые мы потратим на вычисление ответа, или, другими словами, на эффективность.

Например, предположим, что А = (10 × 30), B = (30 × 5), C = (5 × 60). Тогда:

(AB)C = (10×30×5) + (10×5×60) = 1500 + 3000 = 4500 операций
A(BC) = (30×5×60) + (10×30×60) = 9000 + 18000 = 27000 операций.

Очевидно, что первый способ гораздо эффективней. Теперь мы поняли, что нам надо найти оптимальную расстановку скобок в нашем выражении из n матриц. Как это сделать? Мы можем перебрать все расстановки скобок (brute force), но время выполнение этого алгоритма будет эксапаненциально рости от n количества матриц. Решение данной проблемы, как мы увидим — это разбить нашу задачу на подзадачи. Так же мы увидим, что с помощю решения однократного решения подзадачи и повторного использования ответа, мы сможем заметно сократить асимптотику.

Решение динамическим программированием

Сначала, давайте считать то, что мы хотим знать минимальное количесвто операций (или минимальную стоимость), необходимых для перемножения матриц. Если мы перемножаем только две матрицы, то мы можем осуществить это только едиственным способом, следовательно минимальная стоимость — это стоимость этого перемножения. В общем, мы можем найти минимальную стоимость используя следующий рекурсивный алгоритм:

  • Взять последовательность матриц и разделить её на две части.
  • Найти минимальную стоимость перемножения на каждой подпоследовательности.
  • Сложить эти две стоимости и прибавить к этому стоимость перемножения двух получившихся матриц.
  • Сделать это для каждой возможной позиции в последовательности, в которой она может быть разделена и взять минимум среди всех результатов.

Например, если у нас есть четыре матрицы ABCD, мы посчитаем для (A)(BCD), (AB)(CD), и (ABC)(D), делая рекурсивные вызовы, чтобы найти минимальную стоимость на ABC, AB, CD, и BCD. Потом среди них мы выбираем лучший вариант. Так же, этот алгоритм дает не только минимальную стоимость, но и показывает наилучший способ перемножения матриц: нужно только сгрупировать тем же образом, каким дается нам минимальная стоимость и перемножить их между собой.

Внезапно, если мы применим этот алгоритм, то мы обнаружим, что это так же медленно, как и наивный способ перебирания всех скобочных последовательностей! Что пошло не так? Ответом на этот вопрос является то факт, что мы делаем много ненужной работы. Например, в выше описанном алгоритме, мы сделали рекурсивный вызов, чтобы найти наилучшую стоимость для подсчета ABC и AB. Но нахождение наилучшей стоимости для подсчета ABC так же требует нахождения лучшей стоимости для AB. Так как рекурсия растет вглбь все больше и больше, то и число ненужных повторений увеличивается.

Одно из простых решений: меморизация. Каждый раз, когда мы считаем минимальную стоимость на отрезке, мы сохраняем ответ. Когда у нас просят посчитать это ещё раз, то мы сразу же выдадим ответ и не будем пересчитывать. Хоть у нас [math] n^2/2 [/math]

Одно из простых решений — это меморизация. Каждый раз, когда мы считаем минимальную стоимость перемножения определенной подпоследовательности, давайте мы будем запоминать ответ. Если мы когда либо ещё раз захотим посчитать это ещё раз, то мы уже будет иметь ответ и не будем пересчитывать. Поскольку существует всего около [math]n^2/2[/math], где n — это количество матриц, то память занимаемая программой будет не так велика. Можно сказать, что с помощью этого простого трюка мы уменьшили асимптотику алгоритма с O([math]2n[/math]) до O([math]n^3[/math]), что является достаточно эффективным для реальных приложений.

Псевдокод:


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;
		else
			dp[l][r] = 1000 * 1000 * 1000;
			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];
}
  • Замечания : Индексирование массива p начинается с нуля, а у массивов m и s с единицы.

Ссылки

использованы материалы ru.wikipedia.org [1]

Литература

  • Томас Х. Кормен и др. Алгоритмы: построение и анализ
  • Sanjoy Dasgupta , Christos H. Papadimitriou, Umesh Vazirani Algorithms