서론
키타마사법(Kitamasa Method, きたまさ法)은 $A_N = c_1A_{N-1} + c_2A_{N-2} + \cdots + c_KA_{N-K} = \sum_{i=1}^{K} c_iA_{N-i}$와 같은 선형 점화식의 $N$번째 항을 $O(K^2 \log N)$, 더 나아가 $O(K \log K \log N)$에 계산하는 알고리즘입니다.
이 글에서는 키타마사법을 최대한 쉽게 설명하는 것에 초점을 두기 때문에 엄밀한 증명이 생략될 수 있습니다. 양해 부탁드립니다.
식의 변형
키타마사법의 목표는 이전 $K$개의 항으로 결정되는 $A_N = \sum_{i=1}^{K} c_iA_{N-i}$ 꼴의 점화식을 최초 $K$개의 항으로 결정되도록 하는 $A_N = \sum_{i=0}^{K-1} d_iA_i$ 꼴의 식을 찾는 것입니다. 이 식을 만족시키는 수열 $d_i$를 $T(N, K)$ 시간에 찾을 수 있다면, 단순히 $\sum_{i=0}^{K-1} d_iA_i$를 계산하면 되므로 점화식의 $N$번째 항을 $O(T(N, K) + K)$ 시간에 계산할 수 있습니다.
이러한 수열 $d_i$는 항상 존재하며, 적절한 식을 대입해 $A_{N-1}, A_{N-2}, \cdots$를 소거하는 방식으로 직접 찾을 수 있습니다. 그 예시로, BOJ 1003번 피보나치 수열 문제는 $c_1 = c_2 = 1$일 때(피보나치 수열) $d_i$를 찾는 문제입니다.
$c_1 = 2, c_2 = 1$이고 $N = 5$일 때의 $d_i$를 직접 구해보며 어떻게 하면 효율적으로 계산할 수 있을지 고민해봅시다.
- $A_5 = 2A_4 + A_3$
- $A_5 = 2(2A_3 + A_2) + A_3 = 5A_3 + 2A_2$
- $A_5 = 5(2A_2 + A_1) + 2A_2 = 12A_2 + 5A_1$
- $A_5 = 12(2A_1 + A_0) + 5A_1 = 29A_1 + 12A_0$
- $d_0 = 12, d_1 = 29$가 됩니다.
사실 이 과정은 양변에 $A_x - c_1A_{x-1} - c_2A_{x-2} - \cdots = 0$의 상수배를 빼는 과정이라고 생각해볼 수 있습니다. 위 예시에서는 $A_x - 2A_{x-1} - A_{x-2} = 0$의 상수배를 빼게 되겠지요.
- $A_5 = 2A_4 + A_3$, 양변에서 $2(A_4 - 2A_3 - A_2) = 0$을 빼면
- $A_5 = 5A_3 + 2A_2$, 양변에서 $5(A_3 - 2A_2 - A_1) = 0$을 빼면
- $A_5 = 12A_2 + 5A_1$, 양변에서 $12(A_2 - 2A_1 - A_0) = 0$을 빼면
- $A_5 = 29A_1 + 12A_0$
이 과정은 $x^N$을 $f(x) = x^K - c_1x^{K-1} - c_2x^{K-2} - \cdots - c_Kx^0 = x^K - \sum_{i=1}^{K}c_ix^{K-i}$로 나눈 나머지를 구하는 과정과 완벽하게 동일합니다. 위 예시는 사실 $x^5$를 $x^2-2x-1$로 나눈 나머지를 구하는 과정이었고, 실제로 $x^5 = (x^3 + 2x^2 + 5x + 12)(x^2 - 2x - 1) + 29x + 12$입니다.
우리는 이제 $x^N \mod f(x)$를 빠르게 계산하는 방법을 알면 됩니다.
분할 정복을 이용한 거듭 제곱
키타마사법을 사용해야하는 문제는 대부분 $N$이 매우 크기 때문에 $x^N$을 그대로 다룰 수 없습니다. 우리는 어떤 수의 거듭 제곱은 $O(\log N)$에 구할 수 있으니, 이 방법을 응용해봅시다.
$x^N$은 $x^1, x^2, x^4, x^8, \cdots$들의 곱으로 표현할 수 있고, 총 $O(\log N)$번의 다항식 곱셈을 요구하게 됩니다. $x^N \mod f(x)$를 구하는 것이 목표이기 때문에, $(x^1 \mod f(x)), (x^2 \mod f(x)), (x^4 \mod f(x))$ 들의 곱을 $f(x)$로 나눈 나머지를 구하면 됩니다. $mod\ f(x)$를 취하면 차수가 $K$ 미만이기 때문에, 차수가 $K$ 미만인 두 다항식끼리의 곱셈과 나눗셈을 각각 $O(\log N)$번 수행하게 됩니다.
아래와 같이 구현하면, 키타마사법의 시간 복잡도는 Mul함수와 Div함수의 시간 복잡도에 따라 결정됩니다. 구체적으로, Mul함수와 Div함수의 시간 복잡도가 $P(K)$일 때 키타마사법의 시간 복잡도는 $O(P(K) \log N)$이 됩니다.
1 |
|
$O(K^2 \log N)$ 키타마사법
다항식의 곱셈과 나눗셈을 Naive하게 구현하면 $O(K^2)$ 시간이 걸리므로 이때 키타마사법의 시간 복잡도는 $O(K^2 \log N)$이 됩니다. 아래 코드는 BOJ 11444번 피보나치 수 6 문제의 정답 코드입니다.
1 |
|
$O(K \log K \log N)$ 키타마사법
$O(K^2 \log N)$을 $O(K \log K \log N)$로 최적화시키는 방법은 당연히 다항식의 곱셈과 나눗셈을 $O(K \log K)$에 수행하는 것입니다.
FFT를 이용해 다항식의 곱셈을 $O(K \log K)$에 수행하는 방법은 제 블로그에 올라와있고, 비슷하게 FFT를 이용해 다항식의 나눗셈을 $O(K \log K)$에 수행하는 방법은 삼성 소프트웨어 멤버십 블로그에 잘 설명되어 있습니다.
NTT를 이용해 키타마사법을 $O(K \log K \log N)$에 수행하는 코드를 첨부하고 글을 마무리하겠습니다.
1 |
|
참고 자료
- algoshitpo(by ahgus89) - Linear Recurrence (https://algoshitpo.github.io/2020/05/20/linear-recurrence/)
- junis3 - 키타마사 법 (https://blog.junie.land/27)
- 삼성소프트웨어멤버십(by cubelover) - 빠른 다항식 나눗셈 (http://www.secmem.org/blog/2019/04/10/polynomial-division/)
- koosaga github - algebra.cpp (https://github.com/koosaga/olympiad/blob/master/Library/codes/math/algebra.cpp)