cubelover의 블로그

Discrete Fourier Transform

수학2016. 11. 17. 14:27

이산 푸리에 변환이란 주기가 \(N\)인 복소수열 \(x_{0},x_{1},\dots,x_{N-1}\)에 대하여 다음과 같이 정의되는 주기가 \(N\)인 복소수열 \(X_{0},X_{1},\dots,X_{N-1}\)로 바꾸는 변환을 말한다.


\[X_{k}=\sum_{n=0}^{N-1}e^{-2\pi ikn/N}x_{n}\]


여기서 \(e^{iz}=\cos z+i\sin z\)이며, 이로부터 \(e^{2\pi i}=e^{-2\pi i}=1\)임을 쉽게 알 수 있다.


~


이산 푸리에 역변환


또한 이렇게 변환된 복소수열 \(X_{k}\)는 다음과 같은 과정을 통하여 복소수열 \(x_{n}\)으로 역변환될 수 있다.


\[x_{n}=\frac{1}{N}\sum_{k=0}^{N-1}e^{2\pi ikn/N}X_{k}\]


~


이산 푸리에 역변환의 증명


\(X_{k}\)의 정의대로 식을 풀어보면 다음과 같다.


\[\sum_{k=0}^{N-1}e^{2\pi ikn/N}X_{k}=\sum_{k=0}^{N-1}\sum_{j=0}^{N-1}e^{2\pi ik(n-j)/N}x_{j}=\sum_{j=0}^{N-1}x_{j}\sum_{k=0}^{N-1}e^{2\pi ik(n-j)/N}\]


\(j=n\)인 경우에는 다음처럼 된다.


\[\sum_{k=0}^{N-1}e^{2\pi ik(n-j)/N}=\sum_{k=0}^{N-1}1=N\]


\(j\neq n\)인 경우에는 다음처럼 된다.


\[\sum_{k=0}^{N-1}e^{2\pi ik(n-j)/N}=\frac{e^{2\pi i(n-j)}-1}{e^{2\pi i(n-j)/N}-1}=0\]


따라서 다음이 성립한다.


\[\frac{1}{N}\sum_{k=0}^{N-1}e^{2\pi ikn/N}X_{k}=\frac{1}{N}Nx_{n}=x_{n}\]


~


이산 푸리에 변환과 Convolution


주기가 \(N\)인 수열 \(a_{n}\)과 \(b_{n}\)을 각각 이산 푸리에 변환한 수열을 \(A_{n}\)과 \(B_{n}\)이라 하자.


또한 새로운 수열 \(C_{n}=A_{n}B_{n}\)을 정의하고, 이를 이산 푸리에 역변환한 수열을 \(c_{n}\)이라 하면 다음과 같은 식이 성립한다.


\[c_{n}=\sum_{k=0}^{N-1}a_{k}b_{n-k}\]


~


이산 푸리에 변환과 Convolution의 증명


\(C_{n}\)의 정의대로 식을 풀어보면 다음과 같다.


\[C_{n}=A_{n}B_{n}=\sum_{j=0}^{N-1}\sum_{k=0}^{N-1}e^{-2\pi i(j+k)n/N}a_{k}b_{j}=\sum_{j=0}^{N-1}e^{-2\pi ijn/N}\sum_{k=0}^{N-1}a_{k}b_{j-k}\]


따라서 이를 역변환한 수열 \(c_{n}\)에 대한 식이 성립한다.


~


고속 푸리에 변환


이산 푸리에 변환을 단순한 반복문으로 계산하면 \(O(N^{2})\)의 시간복잡도를 가지지만, 이를 \(O(N\log N)\)의 시간복잡도에 행할 수 있는 방법이 고속 푸리에 변환이다.


고속 푸리에 변환에는 다양한 알고리즘이 있지만, 여기서는 \(N=2^{M}\) 꼴의 푸리에 변환에 최적화된 Cooley-Tukey 알고리즘에 대해 설명한다.


\(X_{k}\)를 다음처럼 고쳐 적을 수 있다.


\[X_{k}=\sum_{n=0}^{N-1}e^{-2\pi ikn/N}x_{n}=\sum_{n=0}^{N/2-1}e^{-2\pi ik(2n)/N}x_{2n}+\sum_{n=0}^{N/2-1}e^{-2\pi ik(2n+1)/N}x_{2n+1}=\sum_{n=0}^{N/2-1}e^{-2\pi ikn/(N/2)}x_{2n}+e^{-2\pi ik/N}\sum_{n=0}^{N/2-1}e^{-2\pi ikn/(N/2)}x_{2n+1}\]


이제 수열 \(Y_{k}\)와 \(Z_{k}\)를 다음처럼 정의하자.


\[Y_{k}=\sum_{n=0}^{N/2-1}e^{-2\pi ikn/(N/2)}x_{2n}\]

\[Z_{k}=\sum_{n=0}^{N/2-1}e^{-2\pi ikn/(N/2)}x_{2n+1}\]


수열 \(Y_{k}\)와 \(Z_{k}\)를 계산할 수 있다면, 수열 \(Z_{k}\)는 다음과 같은 방법으로 \(O(N)\)의 시간복잡도로 계산할 수 있다.


\[X_{k}=Y_{k}+e^{-2\pi ik/N}Z_{k}\]

\[X_{k+N/2}=Y_{k}-e^{-2\pi ik/N}Z_{k}\]


그런데, 수열 \(Y_{k}\)와 \(Z_{k}\)는 수열 \(x_{2n}\)과 \(x_{2n+1}\)에 대한 이산 푸리에 변환이므로, 분할정복을 통해 총 \(O(N\log N)\)의 시간복잡도로 이산 푸리에 변환을 구할 수 있다.


#include <cstdio>
#include <cmath>
#include <complex>

const int SZ = 20, N = 1 << SZ;
const double PI = acos(-1);

using namespace std;

int Rev(int x) {
	int i, r = 0;
	for (i = 0; i < SZ; i++) {
		r = r << 1 | x & 1;
		x >>= 1;
	}
	return r;
}

void FFT(complex<double> *a, bool f) {
	complex<double> x, y, z;
	double w;
	int i, j, k;
	for (i = 0; i < N; i++) {
		j = Rev(i);
		if (i < j) {
			z = a[i];
			a[i] = a[j];
			a[j] = z;
		}
	}
	for (i = 1; i < N; i <<= 1) {
		w = PI / i;
		if (f) w = -w;
		x = complex<double>(cos(w), sin(w));
		for (j = 0; j < N; j += i << 1) {
			y = complex<double>(1);
			for (k = 0; k < i; k++) {
				z = a[i + j + k] * y;
				a[i + j + k] = a[j + k] - z;
				a[j + k] += z;
				y *= x;
			}
		}
	}
	if (f) for (i = 0; i < N; i++) a[i] /= N;
}

complex<double> X[N];

int main() {
	double x;
	int i, n;
	scanf("%d", &n);
	for (i = 0; i <= n; i++) {
		scanf("%lf", &x);
		X[i] = complex<double>(x, 0);
	}
	FFT(X, false);
	for (i = 0; i < N; i++) X[i] *= X[i];
	FFT(X, true);
	for (i = 0; i <= n + n; i++) printf("%.0f ", X[i].real());
}


~


일반화된 이산 푸리에 변환


지금까지는 함수 \(\mathbb{Z}_{N}\rightarrow\mathbb{C}\)에 대한 이산 푸리에 변환에 대해서만 살펴보았다.


한편 이산 푸리에 변환에 필요한 연산은 덧셈과 복소수곱셈뿐이므로 이 둘이 정의된 모든 집합에 대해서 이산 푸리에 변환이 가능하다.


예를 들어, 함수 \(\mathbb{Z}_{M}\rightarrow\mathbb{C}\)는 덧셈과 복소수곱셈이 가능하므로 \(\mathbb{Z}_{N}\rightarrow\mathbb{Z}_{M}\rightarrow\mathbb{C}\)에 대한 이산 푸리에 변환이 가능하다.


이를 통해 두 \(N\times M\) 행렬 \(A_{i,j}\)와 \(B_{i,j}\)에 대하여 다음을 만족하는 행렬 \(C_{i,j}\)를 구할 수 있다.


\[C_{i,j}=\sum_{k=0}^{N-1}\sum_{l=0}^{M-1}A_{k,l}B_{i-k,j-l}\]


template을 사용하여 구현하면 다음과 같다.


#include <cstdio>
#include <cmath>
#include <complex>

const int SZ = 10, N = 1 << SZ;
const double PI = acos(-1);

using namespace std;

int Rev(int x) {
	int i, r = 0;
	for (i = 0; i < SZ; i++) {
		r = r << 1 | x & 1;
		x >>= 1;
	}
	return r;
}

template <typename T>
struct Func {
	T a[N];

	T &operator [](int x) {
		return a[x];
	}

	Func<T> operator +(Func<T> &x) {
		Func<T> r;
		int i;
		for (i = 0; i < N; i++) r.a[i] = a[i] + x[i];
		return r;
	}

	Func<T> operator -(Func<T> &x) {
		Func<T> r;
		int i;
		for (i = 0; i < N; i++) r.a[i] = a[i] - x[i];
		return r;
	}

	Func<T> operator *(complex<double> &x) {
		Func<T> r;
		int i;
		for (i = 0; i < N; i++) r.a[i] = a[i] * x;
		return r;
	}

	Func<T> operator *=(complex<double> &x) {
		int i;
		for (i = 0; i < N; i++) a[i] *= x;
		return *this;
	}

	Func<T> operator *=(Func<T> &x) {
		int i;
		for (i = 0; i < N; i++) a[i] *= x[i];
		return *this;
	}

	void FFT(bool f) {
		complex<double> x, y;
		T z;
		double w;
		int i, j, k;
		for (i = 0; i < N; i++) {
			j = Rev(i);
			if (i < j) {
				z = a[i];
				a[i] = a[j];
				a[j] = z;
			}
		}
		for (i = 1; i < N; i <<= 1) {
			w = PI / i;
			if (f) w = -w;
			x = complex<double>(cos(w), sin(w));
			for (j = 0; j < N; j += i << 1) {
				y = complex<double>(1);
				for (k = 0; k < i; k++) {
					z = a[i + j + k] * y;
					a[i + j + k] = a[j + k] - z;
					a[j + k] = a[j + k] + z;
					y *= x;
				}
			}
		}
		if (f) {
			x = complex<double>((double)1 / N);
			for (i = 0; i < N; i++) a[i] *= x;
		}
	}
};

Func<Func<complex<double> > > X;

int main() {
	double x;
	int i, j, n, m;
	scanf("%d%d", &n, &m);
	for (i = 0; i <= n; i++) for (j = 0; j <= m; j++) {
		scanf("%lf", &x);
		X[i][j] = complex<double>(x, 0);
	}
	for (j = 0; j < N; j++) X[j].FFT(false);
	X.FFT(false);
	X *= X;
	X.FFT(true);
	for (j = 0; j < N; j++) X[j].FFT(true);
	for (i = 0; i <= n + n; i++) {
		for (j = 0; j <= m + m; j++) printf("%.0f ", X[i][j].real());
		printf("\n");
	}
}


~


XOR Convolution


\(2^{M}\) 크기의 배열 \(A_{i}\)와 \(B_{i}\)에 대하여 다음처럼 정의되는 배열 \(C_{i}\)를 구하는 문제를 생각해 보자. (\(\oplus\)는 Bitwise XOR 연산을 뜻함)


\[C_{i}=\sum_{k=0}^{2^{M}-1}A_{k}B_{i\oplus k}\]


XOR 연산은 각 자리별로 더한 뒤 2로 나눈 나머지를 구하는 것과 같으므로, \(\mathbb{Z}_{2^{M}}\rightarrow\mathbb{C}\)를 다음처럼 펼쳐서 생각해 보자.


\[\mathbb{Z}_{2}\rightarrow\mathbb{Z}_{2}\rightarrow\cdots\rightarrow\mathbb{Z}_{2}\rightarrow\mathbb{C}\]


각각이 이산 푸리에 변환 가능하므로, \(\mathbb{Z}_{2}\)에 대한 이산 푸리에 변환을 \(M\)번 행하는 것임을 알 수 있다.


#include <cstdio>
#include <complex>

const int SZ = 20, N = 1 << SZ;

using namespace std;

int Rev(int x) {
	int i, r = 0;
	for (i = 0; i < SZ; i++) {
		r = r << 1 | x & 1;
		x >>= 1;
	}
	return r;
}

void FFT(int *a, bool f) {
	int i, j, k, z;
	for (i = 0; i < N; i++) {
		j = Rev(i);
		if (i < j) {
			z = a[i];
			a[i] = a[j];
			a[j] = z;
		}
	}
	for (i = 1; i < N; i <<= 1) for (j = 0; j < N; j += i << 1) for (k = 0; k < i; k++) {
		z = a[i + j + k];
		a[i + j + k] = a[j + k] - z;
		a[j + k] += z;
	}
	if (f) for (i = 0; i < N; i++) a[i] /= N;
}

int X[N];

int main() {
	int i, n;
	scanf("%d", &n);
	for (i = 0; i < 1 << n; i++) scanf("%d", &X[i]);
	FFT(X, false);
	for (i = 0; i < N; i++) X[i] *= X[i];
	FFT(X, true);
	for (i = 0; i < 1 << n; i++) printf("%d ", X[i]);
}



'수학' 카테고리의 다른 글

Dividing Polynomials  (0) 2017.01.12
きたまさ法  (0) 2016.11.24
Number Theoretic Fast Fourier Transform  (1) 2016.11.02