알고리즘

FFT (Fast Fourier Transform)

gubshig 2022. 12. 1. 13:53

개요

FFT는 이산적인 영역에서 하는 푸리에 변환인 DFT(Discrete Fourier Transform)을 분할정복 기법을 이용하여 $\Theta (n\,log\, n)$ 시간 내에 수행하는 알고리즘 입니다. 이를 보통 PS에서는 두 다항식의 곱셈을 $\Theta (n\,log\, n)$ 에 처리하기 위해 사용합니다. 이 글을 이해하기 위한 사전지식으로는 기초적인 프로그래밍 지식과 복소수 지수에 대한 이해가 필요합니다.

 

DFT (Discrete Fourier Transform)

우선 DFT의 정의부터 살펴봅시다. 수열 $a_{0}, \,a_{1}, \,a_{2} , \,a_{3},\,...\, a_{n-1}$ 에 대하여,

$\omega_{N} = e^{-2\pi i/N} $,  $\hat{a}_{n} = \sum_{k=0}^{N-1}a_{k}w_{N}^{kn}$ 로 정의되는데,

사실 이 정의가 뜻하는 바는, 같은 수열에 대해 다항식, $P(x) = a_{0}+a_{1}x+a_{2}x^2+a_{3}x^3...\, +a_{n-1}x^{n-1}$에 대하여,

$ \hat{a}_{n} = P(w_{N}^{n}) $입니다.

 

그럼 이걸 가지고 뭘 할 수 있을까요?

우선 두 다항식의 곱셈에 대한 문제를 생각해봅시다.

다항식 $A(x)$와 $B(x)$의 계수들에 대해, $\mathbf{DFT}(A),\,\mathbf{DFT}(B)$을 취한 후, 각 결과값들을 곱해주면, $\mathbf{DFT}(AB)$의 결과값인 두 다항식을 곱한 후 DFT를 한 결과와 같음이 자명합니다.

왜냐하면 $A(x), \,B(x)$에 같은 값을 대입하고, 그 값을 곱해주면, $A(x)B(x)$에 그 값을 대입한 결과와 같은 값이 나오기 때문입니다.

그렇다면 만약 역변환인 IDFT(Inverse Discrete Fourier Transform)을 취할 수 있으면(IDFT에 대한 설명은 나중에 나옵니다),

두 다항식을 곱한 결과값을 구할 수 있게 됩니다. 

즉, $\mathbf{IDFT(}\mathbf{DFT}(A)\cdot\mathbf{DFT}(B))$ 의 결과값은 두 다항식을 곱한 다항식의 계수가 됩니다.

하지만 문제가 있는데, DFT를 정의대로 계산하면, $\Theta (n^2)$ 시간이 걸리게 됩니다.

여기서 우리는 DFT를 $\Theta (n\,log\, n)$에 계산하는 FFT를 사용하게 됩니다.

 

FFT(Fast Fourier Transform)

DFT를 빨리 계산하기 위해서 우리는 우함수와 기함수의 성질을 이용합니다.

짝수 차수만 있는 우함수 $E(x)$와 홀수 차수만 있는 기함수 $O(x)$에 대하여,

$E(x) = E(-x),\, O(x) = -O(-x)$가 성립합니다. 

그렇다면 $P(x)$에서 홀수차수는 홀수차수끼리, 짝수차수는 짝수차수끼리 모아서 $E(x),\,O(x)$를 만든 다음,

$P(x) = E(x^2) + xO(x^2) $

$P(-x) = E(x^2) - xO(x^2) $

가 성립하기 때문에, $E(x^2)$의 값과 $O(x^2)$의 값을 아는것으로 계산을 단축할 수 있습니다. 벌써부터 분할정복의 느낌이 나죠?

위에서 설명했던 DFT로 돌아가자면, $w_{N}^{N/2} = -1$임을 이용하여 

$P(w_{n}) = E(w_{n}^2) + w_{n}O(w_{n}^2) $

$P(w_{n+N/2}) = E(w_{n}^2) - w_{n}O(w_{n}^2) $

을 알 수 있습니다. 

단 이 방식이 재귀적으로 적용되기 위해서는 다항식의 길이가 2의 거듭제곱 꼴 이어야합니다. 그럼으로 부족한 만큼 0을 추가해주면 됩니다.

그럼 우리는 FFT를 $\Theta (n\,log\, n)$에 계산할 수 있게 됩니다.

파이썬 코드를 살펴보자면,

def fft(p, w):
    n = len(p)
    if n == 1: #Base Case
        return p

    even, odd = p[0::2], p[1::2] #짝수차수와 홀수차수로 분할
    yeven, yodd = fft(even, w*w), fft(odd, w*w) #재귀적으로 불러옴 w*w인 이유는 w^2가 들어가기 떄문에

    z = complex(1, 0)
    y = [0] * n
    for j in range(n//2): #위에서 구한 공식
        y[j] = yeven[j] + z * yodd[j]
        y[j + n//2] = yeven[j] - z * yodd[j]
        z *= w
    return y

 

IDFT(Inverse Discrete Fourier Transform)

${a}_{n} = \frac{1}{N}\sum_{k=0}^{N-1}\hat a_{k}w_{N}^{-kn}$이 성립합니다. 이 식은 DFT와 굉장히 유사하게 생겼습니다. 사실 w대신 w의 역수를 대입하고, 모든 결과값을 N으로 나눠주면 됩니다.

이것이 왜 성립하는지 잘 이해가 가지 않을 수 있는데, 

$\begin{bmatrix}
 w_{N}^{0}& w_{N}^{0} &w_{N}^{0}  &w_{N}^{0}  \\
 w_{N}^{0}&  w_{N}^{1}&  w_{N}^{2}& w_{N}^{3} \\
 w_{N}^{0}&  w_{N}^{2}&  w_{N}^{4}& w_{N}^{6} \\
 w_{N}^{0}&  w_{N}^{3}& w_{N}^{6} & w_{N}^{9} \\
\end{bmatrix}
\begin{bmatrix}
a_{0} \\
 a_{1}\\
 a_{2}\\
a_{3} \\
\end{bmatrix}$

이 행렬곱은 N = 4일때 DFT의 정의와 정확히 일치하는것을 알 수 있습니다. 

그렇다면 이 행렬의 역행렬은,

$\frac{1}{N}
\begin{bmatrix}
 w_{N}^{0}& w_{N}^{0} &w_{N}^{0}  &w_{N}^{0}  \\
 w_{N}^{0}&  w_{N}^{-1}&  w_{N}^{-2}& w_{N}^{-3} \\
 w_{N}^{0}&  w_{N}^{-2}&  w_{N}^{-4}& w_{N}^{-6} \\
 w_{N}^{0}&  w_{N}^{-3}& w_{N}^{-6} & w_{N}^{-9} \\
\end{bmatrix}$

임이 자명합니다. 계산해보면 항등행렬이 나옵니다.

 

다항식 곱셈 파이썬 코드를 살펴봅시다.

def mult(a, b):
    n = 1
    while n <= len(a) and n <= len(b):
        n *= 2
    n *= 2 #다항식의 크기를 2의 거듭제곱꼴로 맞춰준다
    a = a + [0] * (n - len(a))
    b = b + [0] * (n - len(b))
    w = complex(math.cos(2*pi/n), math.sin(2*pi/n)) #w계산
    a = fft(a, w)
    b = fft(b, w) #a, b다항식을 각각 fft 취해준다.
    c = [0] * n

    for i in range(n):
        c[i] = a[i] * b[i] #fft의 결과값을 곱해준다.
    c = fft(c, 1/w) #IFFT를 취해준다.

    for i in range(n):
        c[i] /= n
        c[i] = round(c[i].real)
    return c

 

$\omega_{N} = e^{-2\pi i/N} = cos(-2\pi /N) + i \,sin(-2\pi / N)$ 이 성립함이 알려져 있기에, 우리는 $w$를 쉽게 계산할 수 있습니다.

'알고리즘' 카테고리의 다른 글

2023 02 21 problem solving  (0) 2023.02.21
백준 1750 서로소의 개수 파이썬  (0) 2023.02.19
2023 02 17 problem solving  (0) 2023.02.17
백준 25402 트리와 쿼리 파이썬 풀이  (1) 2023.02.17
정올 1차 1, 2번 문제 풀이  (0) 2023.02.16