Задачи на умножение матриц - Алгоритмика
Задачи на умножение матриц

Задачи на умножение матриц

авторы Сергей Слотин Максим Иванов
обновлено авг. 30, 2021
пререквизиты Бинарное возведение в степень Матрицы

Когда мы говорили о линейных функциях и матрицах, все примеры были про геометрию. Так просто проще думать: представлять в уме повороты и растяжения векторов на плоскости легче, чем рассуждать о свойствах абстрактных n-мерных пространств. Однако мы программисты, и нас в основном интересуют задачи вне геометрии.

Большинство применений в контексте олимпиад опираются на ассоциативность матричного умножения:

ABC=(AB)C=A(BC) ABC = (AB)C = A(BC) Если задачу можно свести к возведению матрицы AA в какую-то большую степень nn — то есть к домножению единичной матрицы nn раз на матрицу AA — то можно просто воспользоваться свойством ассоциативности и посчитать результат бинарным возведением в степень: A8=A222=((AA)(AA))((AA)(AA)) A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA))

Такой подход имеет множество применений в динамическом программировании, комбинаторике, теории вероятностей и математике в целом.

#Линейные рекурренты

Задача. Дано число n<1018n < 10^{18}. Требуется вычислить nn-ное число Фибоначчи:

f0=0f1=1fn=fn1+fn2 \begin{aligned} f_0 &= 0 \\ f_1 &= 1 \\ f_{n} &= f_{n-1} + f_{n-2} \end{aligned}

Заметим, что вычисление очередного числа Фибоначчи выражается как линейная функция от двух предыдущих чисел Фибоначчи — а именно, как их сумма.

Это означает, что мы можем построить матрицу 2×22 \times 2, которая будет соответствовать этому преобразованию: как по двум соседним числам Фибоначчи (fn,fn+1)(f_n, f_{n+1}) вычислить следующее число, то есть перейти к паре (fn+1,fn+2)(f_{n+1}, f_{n+2}).

(fn+1fn+2)=(0+fn+1fn+fn+1)=(0111)(fnfn+1) \begin{pmatrix} f_{n+1} \\ f_{n+2} \\ \end{pmatrix} = \begin{pmatrix} 0 + f_{n+1} \\ f_{n} + f_{n+1} \\ \end{pmatrix} = \begin{pmatrix} 0 & 1 \\ 1 & 1 \\ \end{pmatrix} \begin{pmatrix} f_{n} \\ f_{n+1} \\ \end{pmatrix}

Обозначим за AA эту матрицу перехода. Чтобы посчитать nn-ное число Фибоначчи, нужно применить nn раз эту матрицу к вектору (f0,f1)=(0,1)(f_0, f_1) = (0, 1).

Так как размер матрицы AA константный, её умножение саму на себя будет работать за O(1)O(1). Значит, можно посчитать AnA^n бинарным возведением в степень за O(logn)O(\log n) операций, домножить на вектор (0,1)(0, 1) и взять первый элемент результата — он будет равен в точности fnf_n, что мы и искали.

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

#Общий случай

В общем случае, когда мы учитываем не 22, а kk последних элементов, причём с разными весами, линейная рекуррента fn=a1fn1+a2fn2++akfnkf_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k} имеет такую матрицу перехода:

(010000100001akak1ak2a1) \begin{pmatrix} 0 & 1 & 0 & \ldots & 0 \\ 0 & 0 & 1 & \ldots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \ldots & 1 \\ a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ \end{pmatrix}

При домножении на вектор (fn,fn+1,,fn+k1)(f_n, f_{n+1}, \ldots, f_{n+k-1}) для последнего элемента получается в точности формула из определения, а все (k1)(k-1) предыдущих просто перекопируются.

Отметим, что рекуррентные последовательности задаются не только коэффициентами aia_i, но и начальными значениями (f1,f2,,fk)(f_1, f_2, \ldots, f_k).

Упражнение. Для линейной рекурренты порядка kk известны её коэффициенты aia_i и kk пар вида (i,fi)(i, f_i). Восстановите первые kk элементов последовательности.

#Геометрическая прогрессия

В статье про бинарное возведение в степень мы обсуждали модификацию алгоритма для нахождения суммы геометрической прогрессии:

f(n)=1+a+a2++an1 f(n) = 1 + a + a^2 + \ldots + a^{n-1} Можно подойти к этой задаче с другой стороны. Заметим, что сумма первых nn степеней следующим образом выражается через сумму первых (n1)(n-1) степеней: f(n)=f(n1)a+1 f(n) = f(n-1) \cdot a + 1 Это почти линейная зависимость от предыдущего — только ещё добавляется неудобная константа. Можно применить следующий трюк: представить, что мы также зависим ещё и от (n2)(n-2)-го элемента, но положить его постоянно равным единице: (fn+11)=(fna+10+1)=(a101)(fn1) \begin{pmatrix} f_{n+1} \\ 1 \\ \end{pmatrix} = \begin{pmatrix} f_n \cdot a + 1 \\ 0 + 1 \\ \end{pmatrix} = \begin{pmatrix} a & 1 \\ 0 & 1 \\ \end{pmatrix} \begin{pmatrix} f_{n} \\ 1 \\ \end{pmatrix}

Тогда эту матрицу можно аналогично возвести в степень nn и вернуть её правое верхнее поле.

#Динамическое программирование

Переходы в некоторых динамиках иногда тоже можно выразить в терминах матричного умножения.

Задача. Дана ориентированный граф GG размера n<500n < 500. Требуется найти количество путей из ss в tt через ровно k<1018k < 10^{18} переходов.

Если бы ограничение на kk было сильно меньше, мы бы ввели динамику fi,vf_{i,v}, равную количеству способов дойти до vv-той вершины через ровно ii переходов. Пересчёт — перебор предпоследней вершины в пути:

fi,v=ugvfi1,u f_{i,v} = \sum_{u \in g_v} f_{i-1,u} Перепишем эту динамику в терминах матрицы смежности, в ячейке GuvG_{uv} которой содержится число ребер между uu и vv (ровно поэтому она называется матрицей). fi,v=ufi1,uGuv f_{i,v} = \sum_u f_{i-1,u} \cdot G_{uv} Выясняется, что вектор fif_{i} зависит от fi1f_{i-1} линейно: его vv-тый элемент получается скалярным умножением вектора fi1f_{i-1} и vv-того столбца матрицы смежности GG. Значит, имеет место следующее равенство: fi=Gfi1 f_i = G \cdot f_{i-1}

Получается, fkf_k можно раскрыть как fk=f0GGGf_k = f_0 \cdot G \cdot G \cdot \ldots \cdot G и применить бинарное возведение в степень.

По этой формуле нам потребуется O(logk)O(\log k) раз перемножить две матрицы размера n×nn \times n, что суммарно будет работать за O(n3logk)O(n^3 \log k).

const int n;

matrix binpow(matrix a, int p) {
    // создадим единичную матрицу
    matrix b(n, vector<int>(n, 0));
    for (int i = 0; i < n; i++)
        b[i][i] = 1;

    while (p > 0) {
        if (p & 1)
            b = matmul(b, a);
         a = matmul(a, a);
         p >>= 1;
    }

    return b;
}

В получившейся матрице в ячейке GijG_{ij} будет находиться количество способов дойти из ii-той вершины в jj-тую, используя ровно kk переходов. Ответом нужно просто вывести GstG_{st}.

#Модификации задачи

С небольшими изменениями этим методом можно решать много похожих задач:

  • Практически не меняя сам алгоритм, можно решить задачу «с какой вероятностью мы попадём из вершины ss в вершину tt», если вместо матрицы смежности даны вероятности, с которыми мы переходим из вершины в вершину в марковской цепи.
  • Если нам не нужно количество способов, а только сам факт, можно ли дойти за ровно kk переходов, то можно обернуть матрицу в битсеты и сильно ускорить решение.
  • Если нас спрашивают «за не более, чем kk переходов», то вместо kk-той степени матрицы мы можем вышеописанным методом посчитать сумму геометрической прогрессии. Альтернативно, можно для каждой вершины добавить вторую, в которую будет вести ребро из изначальной, а единственное исходящее ребро — это петля в саму себя.

Эту технику можно применить и к другим динамикам, где нужно посчитать количество способов что-то сделать — иногда очень неочевидными способами.

Например, можно решить такую задачу: найти количество строк длины k1018k \approx 10^{18}, не содержащих данные маленькие запрещённые подстроки. Для этого нужно построить граф «легальных» переходов в Ахо-Корасике, возвести его матрицу смежности в kk-тую степень и просуммировать в нём первую строчку.

В некоторых изощрённых случаях в матричном умножении вместо умножения и сложения нужно использовать другие операции, которые ведут себя как умножение и сложение. Пример задачи: «найти путь от ss до tt с минимальным весом ребра, использующий ровно kk переходов»; здесь нужно возводить в (k1)(k-1)-ую степень матрицу весов графа, и вместо и сложения, и умножения использовать минимум из двух весов.

Также в контексте нахождения кратчайших путей известен «distance product»:

Cij=mink{aik+bkj} C_{ij} = \min_k \{ a_{ik} + b_{kj} \} Возводя матрицу весов в kk-тую степень мы получаем, соответственно, минимальное расстояние от aa до bb, используя не более kk переходов. В частности, при k=nk=n мы за O(n3logn)O(n^3 \log n) операций получим матрицу кратчайших расстояний между всеми парами вершины, хотя для этого есть и более прямые методы.