키타마사 법(kitamasa method, きたまさ法)에 대한 공부
1. 키타마사 법(kitamasa method)
수열 $a_{n}$의 점화식을 이전의 몇개 항으로 정의한다면, 귀납적 정의, 재귀적 수열 등으로 부른다.
$$a_{n} = \sum_{i = 1}^{k} w_{i}a_{n-i}$$
이런 형태로 정의되는 대표적인 수열은 피보나치 수열이다.
$$a_{n} = a_{n-1} + a_{n-2}, w_{1} = w_{2} = 1, k = 2$$
이 피보나치 수열의 가장 빠른? 해법중 하나는 행렬을 이용하는 방법이다.
https://deepdata.tistory.com/760
행렬을 이용한 피보나치 수열 문제의 해법
1. 피보나치 수열의 행렬 표현 피보나치 수열의 점화식은 다음과 같다. $a_{n+1} = a_{n} + a_{n-1}$ $a_{n} = a_{n} + 0$ 따라서 행렬로 나타내면 다음과 같다 $$\begin{pmatrix} a_{n+1} \\ a_{n} \end{pmatrix}=\begin{pmatrix}
deepdata.tistory.com
일반적으로 $a_{n} = \sum_{i = 1}^{k} a_{n-i}w_{i}$는 다음과 같은 행렬 표현으로 바꿀 수 있다.
이 방법의 시간복잡도는 $O(k^{3}logN)$으로 알려져있다.
이 방법도 나름 의미있지만, k = 1000정도 일때 세제곱이다보니 상당히 느려진다..
키타마사법의 목표는 $a_{n}$을 $O(k^{2}logN)$에 구하는 것이다.
2. 귀납적 수열의 n번항은 항상 1~k번 항의 선형결합으로 나타낼 수 있다
핵심 아이디어는 "이전의 k개의 항으로 결정되는" 수열의 점화식 $a_{n} = \sum_{i = 1}^{k} w_{i}a_{n-i}$에 대하여,
$a_{n}$은 항상 "최초 k개의 항" $a_{1}, a_{2}, ... , a_{k}$의 선형 결합으로 나타낼 수 있다는 것이다.
$$a_{n} = \sum_{i = 1}^{k} a_{i}c_{i}$$
이 말이 무슨 말이냐면, 피보나치 수열 $a_{n} = a_{n-1} + a_{n-2}$에 대하여,
$$a_{3} = a_{1} + a_{2}, c_{1} = c_{2} = 1$$
$$a_{4} = a_{3} + a_{2} = a_{1} + 2a_{2}, c_{1} = 1, c_{2} = 2$$
$$a_{5} = a_{4} + a_{3} = a_{1} + 2a_{2} + a_{1} + a_{2} = 2a_{1} + 3a_{2}, c_{1} = 2, c_{2} = 3$$
...
이렇게 이전에 구한 항을 이용해서, n-1,n-2,... 항들을 소거하는 방식으로 항상 $c_{i}$를 찾을 수 있다.
만약에 수열 $c_{i}$를 빠르게 찾을 수 있다면, 원하는 $a_{n}$을 빠르게 구할 수 있을 것이다.
3. 다항식의 나눗셈으로의 접근
여러 블로그들을 찾아 읽어봤지만, 가장 쉬운 접근은 다항식의 나눗셈으로 접근하는 것 같다..
https://justicehui.github.io/hard-algorithm/2021/03/13/kitamasa/
키타마사법(Kitamasa Method, きたまさ法)
서론 키타마사법(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)$에 계산하는
justicehui.github.io
https://milkclouds.work/linear-recurrence/
Linear Recurrence, 키타마사법
다음과 같이 정의되는 무한수열 $a_n$이 있다고 하자. \[a_n = c_1 a_{n-1} + c_2 a_{n-2} + \ldots + c_k a_{n-k}\] $c$와 $a$의 $k$개의 초항이 주어졌을 때 $a_n$을 빠르게 구하는 게 하고 싶은 일이다. 1. 행렬곱 행
milkclouds.work
$a_{n}$의 점화식이 n이 k+1이상인 모든 자연수에 대해 다음과 같이 주어지면,
$$a_{n} = w_{1}a_{n-1} + w_{2}a_{n-2} + ... + w_{k}a_{n-k}$$
여기서 재귀적으로 $a_{n-1} = w_{1}a_{n-2} + w_{2}a_{n-3} + ... + w_{k}a_{n-1-k}$을 대입하면서 항을 소거하는 과정이
$a_{n} = x^{n}$으로 치환해서, 다항식처럼 생각한 다음
$$x^{n} = w_{1}x^{n-1} + w_{2}x^{n-2} + ... + w_{k}x^{n-k}$$
이것을 $x^{k} - w_{1}x^{k-1} - w_{2}x^{k-2} - ... - w_{k}x^{0}$으로 나눈 나머지를 구하는 과정과 동일하다는 것이다.
일반적인 식으로 해보면... 이게 되는건지 안되는건지 어려우니까 예를 들어 생각해보자.
가장 간단한 피보나치 수열 $a_{n} = a_{n-1} + a_{n-2}$의 n = 5에 대해 생각해보자.
$$a_{5} = a_{4} + a_{3} = (a_{3} + a_{2}) + a_{3} = 2a_{3} + a_{2}$$
$$a_{5} = 2a_{3} + a_{2} = 2(a_{2} + a_{1}) + a_{2} = 3a_{2} + 2a_{1}$$
$$a_{5} = 3a_{2} + 2a_{1} = 3(a_{1} + a_{0}) + 2a_{1} = 5a_{1} + 3a_{0}$$
만약 $a_{n} = x^{n}$라 하고, $a_{2} - a_{1} - 1 = x^{2} - x - 1$로 나눠보면...
$$x^{5} = (x^{3} + x^{2} + 2x + 3)(x^{2} - x - 1) + 5x + 3$$이다.
나머지 5x+3이 $a_{5} = 5a_{1} + 3a_{0}$와 동일하다.
그러므로, $a_{n}$을 구하는 문제는 $f(x) = x^{k} - w_{1}x^{k-1} - ... - w_{k}x^{0}$에 대하여, $x^{n} mod f(x)$를 구하는 문제가 된다.
4. 구현 예시
곱셈과 나눗셈을 기본적인 구현 방법으로 구현하면, 항 수가 K개일때, $O(K^{2})$에 할 수 있다.
https://deepdata.tistory.com/981
다항식의 곱셈과 나눗셈 기본 컴퓨터 구현 방법 배우기
1. 다항식의 곱셈 두 다항식의 곱셈은 구현하는 방법이 많이 있지만... 당장은 어려우니 일단 $O(k^{2})$으로 naive하게 구현 해보자. 다항식은 각 항의 계수를 배열에 저장하면 되는데 $f(x) = 2x^{2} + x
deepdata.tistory.com
또한 N이 매우 크면 단순한 곱셈으로는 $x^{n}$을 구하는데 시간이 매우 오래걸린다.
하지만 분할 정복을 이용한 거듭제곱을 이용하면 $O(logN)$에 구할 수 있다.
이때, $x^{n} mod f(x)$를 구하는게 목표인데,
$x^{n}$을 먼저 구할려고 배열로 [0,0,0,..........,1]로 만들면 공간 메모리 초과날것 같다
합동식의 성질 a*b mod p = (a mod p * b mod p) mod p로
거듭제곱 $x, x^{2}, x^{4}, ...$을 구하면서 나머지를 구하면 된다.
또한 우리는 다항식의 나눗셈을 하면서 나머지만 필요로 한다. .. 몫은 return하지 않도록 한다.
전체 시간 복잡도가 $O(K^{2}logN)$에 해결할 수 있다.
#naive polynomial multiplication
def multiply(a,b,mod):
result = [0]*(len(a)+len(b)-1)
for i in range(len(a)):
for j in range(len(b)):
result[i+j] += (a[i]*b[j]) % mod
return result
#negative mod
def negative_mod(x,mod):
if x < 0:
return (x+mod)%mod
else:
return x%mod
#naive polynomial division
#a/b
def divide(a,b,mod):
q = [0]*(len(a)-len(b)+1) #몫
#최고차항부터 순회해서..
for i in range(len(q)-1,-1,-1):
q[i] = a[i+len(b)-1]//b[-1]
for j in range(len(b)-1,-1,-1):
a[i+j] -= (q[i]*b[j])
a[i+j] = negative_mod(a[i+j],mod)
while a:
x = a.pop()
if x != 0:
a.append(x)
break
return a
def kitamasa(w,a,n,mod):
c = [1] #a_n = sum(w[i]a_n-i) = sum(c[i]a[i])
x = [0,1] #x^1, x^2, x^4, ...
#w를 이용해 f(x) = x^k - w1x^k-1 - w2x^k-2 - ...을 구한다.
f = [negative_mod(-i,mod) for i in w]
f.append(1)
#분할정복을 이용한 거듭제곱으로 x^n mod f를 구한다.
#합동식의 성질 a*b mod p = (a mod p * b mod p) mod p를 이용
while n:
if n & 1:
c = divide(multiply(c,x,mod),f,mod)
n >>= 1
x = divide(multiply(x,x,mod),f,mod)
#c배열을 찾았다면, an의 값을 구한다.
#find a_n = sum(c[i]a[i])
answer = 0
for i in range(len(a)):
answer += (a[i]*c[i])
answer = negative_mod(answer,mod)
return answer
5. 연습문제
11444번: 피보나치 수 6 (acmicpc.net)
11444번: 피보나치 수 6
첫째 줄에 n이 주어진다. n은 1,000,000,000,000,000,000보다 작거나 같은 자연수이다.
www.acmicpc.net
w = [1,1]이고 a = [0,1]인 경우에 대한 키타마사법을 이용하면 해결할 수 있다.
FFT를 이용한 고속 곱셈을 하거나, 나눗셈도 빠른 나눗셈을 사용하지 않는 이상... 당장 실전성은 없는듯하다...
혹은 최신 알고리즘으로 보스턴 모리 알고리즘을 쓰지 않는 이상은... 필요가 없는것 같다
'대수학' 카테고리의 다른 글
컴퓨터가 실수를 연분수 표현(continued fraction)으로 나타내는 방법 (0) | 2023.09.20 |
---|---|
Fast Fourier Transform을 이용한 빠른 다항식 곱셈 공부하기 (0) | 2023.08.29 |
다항식의 곱셈과 나눗셈 기본 컴퓨터 구현 방법 배우기 (0) | 2023.08.26 |