개념

행렬은 값을 행과 열로 배치한 것입니다. R×KR\times K 행렬과 K×CK\times C 행렬을 곱하면 R×CR\times C 행렬을 얻습니다. 앞 행렬의 열 수와 뒤 행렬의 행 수가 같아야 합니다.

행렬 곱셈

결과의 한 원소는 앞 행렬의 행과 뒤 행렬의 열에서 같은 위치의 값을 곱해 더합니다.

Cij=∑k=0K−1AikBkj.C_{ij}=\sum_{k=0}^{K-1} A_{ik}B_{kj}.

일반적으로 ABAB와 BABA는 다르므로 곱하는 순서를 지켜야 합니다. 행렬 곱셈에는 결합법칙이 성립하므로 (AB)C=A(BC)(AB)C=A(BC)입니다.

예를 들어 다음 두 행렬의 곱에서 첫 행·첫 열의 값은 1×5+2×7=191\times5+2\times7=19입니다. 첫 행·둘째 열의 값은 1×6+2×8=221\times6+2\times8=22입니다.

(1234)(5678)=(19224350).\begin{pmatrix}1&2\\3&4\end{pmatrix}\begin{pmatrix}5&6\\7&8\end{pmatrix}=\begin{pmatrix}19&22\\43&50\end{pmatrix}.

결과 행렬의 원소 하나를 구할 때 앞 행렬의 한 행과 뒤 행렬의 한 열에서 같은 개수의 원소를 짝지어야 하므로, 앞의 열 수와 뒤의 행 수가 같아야 합니다.

행렬 거듭제곱

정사각행렬 AA를 반복해서 곱한 ABA^B도 지수를 절반으로 나누어 계산합니다. A2k=AkAkA^{2k}=A^kA^k, A2k+1=AkAkAA^{2k+1}=A^kA^kA입니다.

지수 00의 결과는 항등행렬입니다. 항등행렬은 대각 원소가 11, 나머지가 00이며 AI=IA=AAI=IA=A를 만족합니다.

같은 규칙을 여러 단계 적용하는 계산을 행렬로 표현할 수 있으면 행렬 거듭제곱을 사용할 수 있습니다. 예를 들어 두 수 (x,y)(x,y)를 한 단계마다 (x+y,x)(x+y,x)로 바꾸는 규칙은 다음과 같습니다.

(1110)(xy)=(x+yx).\begin{pmatrix}1&1\\1&0\end{pmatrix}\begin{pmatrix}x\\y\end{pmatrix}=\begin{pmatrix}x+y\\x\end{pmatrix}.

(1,0)(1,0)에서 시작하면 (1,1)→(2,1)→(3,2)(1,1)\to(2,1)\to(3,2)가 됩니다. 같은 행렬을 세 번 곱하면 세 단계의 결과를 얻으므로, BB단계의 결과는 행렬의 BB제곱으로 계산합니다. 결합법칙에 따라 곱셈을 묶을 수 있어 지수를 절반으로 줄여 구한 행렬을 제곱하는 방법이 성립합니다. 상태의 개수에 해당하는 행렬 크기는 작고, 반복 단계 수는 큰 문제에 적합합니다.

복잡도

정사각행렬의 한 변 크기를 NN, 지수를 BB라고 합니다.

연산시간복잡도공간복잡도
행렬 곱셈O(N3)O(N^3)결과 행렬 O(N2)O(N^2)
재귀 빠른 거듭제곱O(N3log⁡(B+1))O(N^3\log(B+1))중간 행렬·재귀 O(N2log⁡(B+1))O(N^2\log(B+1))

예제

문제

정사각행렬의 BB제곱을 구하여라.

입력

첫째 줄에 행렬의 크기 NN과 지수 BB가 공백을 사이에 두고 주어진다. 1≤N≤31\le N\le3; 0≤B≤80\le B\le8이다.

다음 NN개 줄에 행렬의 각 행을 이루는 정수 NN개가 공백을 사이에 두고 주어진다. 각 원소는 00 이상 22 이하이다.

출력

결과 행렬을 NN개 줄에 출력한다. 같은 행의 값은 공백으로 구분한다.

예제 입력

2 5
1 1
1 0

예제 출력

8 5
5 3

예제 코드

C++20

#include <iostream>
#include <vector>

using namespace std;

int n;
vector<vector<int>> a, t;

vector<vector<int>> mul(const vector<vector<int>>& a, const vector<vector<int>>& b) {
    int i, j, k;
    vector<vector<int>> c(n, vector<int>(n));

    for (i = 0; i < n; ++i) {
        for (j = 0; j < n; ++j) {
            for (k = 0; k < n; ++k) {
                // a의 i행과 b의 j열에서 같은 위치의 곱을 누적
                c[i][j] += a[i][k] * b[k][j];
            }
        }
    }
    return c;
}

vector<vector<int>> power(const vector<vector<int>>& a, int b) {
    int i;
    vector<vector<int>> t;

    // 행렬의 0제곱은 대각 원소가 1인 항등행렬
    if (b == 0) {
        t.assign(n, vector<int>(n));
        for (i = 0; i < n; ++i) {
            t[i][i] = 1;
        }
        return t;
    }
    // 절반 지수의 결과를 한 번 계산하여 재사용
    t = power(a, b / 2);
    t = mul(t, t);
    // 홀수 지수에서 제곱 후 남은 행렬 a를 한 번 곱함
    if (b % 2 == 1) t = mul(t, a);
    return t;
}

int main() {
    int b, i, j;

    cin >> n >> b;
    a.assign(n, vector<int>(n));
    for (i = 0; i < n; ++i) {
        for (j = 0; j < n; ++j) {
            cin >> a[i][j];
        }
    }
    // 절반 지수의 결과를 한 번 계산하여 재사용
    t = power(a, b);

    for (const auto& row : t) {
        for (int x : row) {
            cout << x << ' ';
        }
        cout << '\n';
    }
}