ALGORITHM NOTE1

BOJ 10830 - 행렬 제곱

행렬 곱하기 행렬 곱하기 행렬 곱하기...

#algorithm#boj#gold#math#divide-and-conquer#fast-exponentiation#linear-algebra
아카이브로 돌아가기

문제 링크

문제

크기가 N*N인 행렬 A가 주어진다. 이때, A의 B제곱을 구하는 프로그램을 작성하시오. 수가 매우 커질 수 있으니, A^B의 각 원소를 1,000으로 나눈 나머지를 출력한다.

입력

첫째 줄에 행렬의 크기 N과 B가 주어진다. (2 ≤ N ≤ 5, 1 ≤ B ≤ 100,000,000,000)

둘째 줄부터 N개의 줄에 행렬의 각 원소가 주어진다. 행렬의 각 원소는 1,000보다 작거나 같은 자연수 또는 0이다.

출력

첫째 줄부터 N개의 줄에 걸쳐 행렬 A를 B제곱한 결과를 출력한다.

풀이

지수를 그대로 곱하면 너무 느리기 때문에, 분할 정복을 이용한 거듭제곱이 필요하다. A^B를 계산할 때 B를 절반으로 줄여 구한 뒤, 제곱하거나 한 번 더 곱하는 방식으로 줄여 나간다.

각 단계에서 필요한 것은 행렬 곱셈 하나뿐이다. 따라서 pow(matrix, b) 형태로 재귀를 두고, 짝수면 절반 결과를 두 번 곱하고 홀수면 원본 행렬을 한 번 더 곱하면 된다.

숫자 거듭제곱을 빠르게 구하는 방식이 그대로 행렬에도 적용된다.

코드

java
import java.io.*;
import java.util.StringTokenizer;
 
public class Main {
 
    static int N;
    static int[][] origin;
 
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
 
        N = Integer.parseInt(st.nextToken());
        long B = Long.parseLong(st.nextToken());
 
        origin = new int[N][N];
        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 0; j < N; j++) {
                origin[i][j] = Integer.parseInt(st.nextToken()) % 1000;
            }
        }
 
        int[][] result = solve(origin, B);
 
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                sb.append(result[i][j]).append(" ");
            }
            sb.append('\n');
        }
 
        System.out.println(sb);
    }
 
    private static int[][] solve(int[][] matrix, long exp) {
        if (exp == 1)
            return matrix;
 
        int[][] tmp = solve(matrix, exp / 2);
        tmp = multiply(tmp, tmp);
 
        if (exp % 2 == 1L) {
            tmp = multiply(tmp, origin);
        }
 
        return tmp;
    }
 
    private static int[][] multiply(int[][] m1, int[][] m2) {
        int[][] tmp = new int[N][N];
 
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                for (int k = 0; k < N; k++) {
                    tmp[i][j] += m1[i][k] * m2[k][j];
                    tmp[i][j] %= 1000;
                }
            }
        }
 
        return tmp;
    }
 
}

복잡도

  • 시간 복잡도: 행렬 곱셈 O(N3)O(N^3) 기준 전체 O(N3logB)O(N^3 \log B)
  • 공간 복잡도: 행렬 저장 공간 기준 O(N2)O(N^2)

마무리

행렬 제곱도 결국 빠른 거듭제곱과 같은 구조다. 지수를 반으로 줄여 나가면 큰 거듭제곱도 충분히 처리할 수 있다.