cubelover의 블로그

きたまさ法

수학2016. 11. 24. 01:51

키타마사법은 다음과 같은 점화식을 가지는 수열의 \(n\)번째 항을 빠르게 계산하는 데 사용된다.


\[a_{n}=\sum_{k=1}^{m}a_{n-k}c_{k}\]


이 점화식을 단순 행렬곱으로 풀면 \(O(m^{3}\log n)\)의 시간이 필요하지만, 키타마사법을 사용하면 \(O(m^{2}\log n)\)의 시간이면 충분하다.


~


임의의 \(a_{n}\)을 다음처럼 \(a_{1}, a_{2}, \dots, a_{m}\)에 대한 식으로 표현할 수 있다.


\[a_{n}=\sum_{k=1}^{m}a_{k}d_{k}\]


키타마사법의 아이디어는, 위 식을 만족하는 \(n\), \(d_{k}\)와 임의의 정수 \(l\)에 대하여 다음이 성립한다는 것이다.


\[a_{n+l}=\sum_{k=1}^{m}a_{k+l}d_{k}\]


이 식이 성립하는 이유는 수학적 귀납법으로 쉽게 보일 수 있으므로 생략한다.


이를 통해 \(a_{2n}\)에 대한 식을 정리하면 다음과 같다.


\[a_{2n}=\sum_{k=1}^{m}a_{n+k}d_{k}=\sum_{k=1}^{m}\sum_{j=1}^{m}a_{j+k}d_{j}d_{k}\]


따라서 \(a_{2n}\)을 \(a_{2}, a_{3}, \dots, a_{2m}\)에 관한 선형식으로 표현할 수 있고, 이들의 계수는 \(O(m^{2})\) 시간 안에 계산할 수 있다.


또한 \(a_{m+1}, a_{m+2}, \dots, a_{2m}\)을 \(a_{1}, a_{2}, \dots, a_{m}\)에 대한 선형식으로 나타내는 데 \(O(m^{2})\)의 시간이면 충분하다.


이를 종합하면, \(a_{n}\)에 대한 식을 \(a_{2n}\)에 대한 식으로 바꾸는 데 \(O(m^{2})\)의 시간밖에 소요되지 않으므로 \(O(m^{2}\log n)\)의 시간복잡도로 \(n\)번째 항을 계산할 수 있다.


#include <cstdio>

const int P = 1000000007;

int a[1001];
int c[1001];
int d[1001];
int t[2002];

void Kitamasa(int n, int m) {
	int i, j;
	if (n == 1) {
		d[1] = 1;
		for (i = 2; i <= m; i++) d[i] = 0;
		return;
	}
	if (n & 1) {
		Kitamasa(n ^ 1, m);
		j = d[m];
		for (i = m; i >= 1; i--) d[i] = (d[i - 1] + (long long)c[m - i + 1] * j) % P;
		return;
	}
	Kitamasa(n >> 1, m);
	for (i = 1; i <= m + m; i++) t[i] = 0;
	for (i = 1; i <= m; i++) for (j = 1; j <= m; j++) t[i + j] = (t[i + j] + (long long)d[i] * d[j]) % P;
	for (i = m + m; i > m; i--) for (j = 1; j <= m; j++) t[i - j] = (t[i - j] + (long long)c[j] * t[i]) % P;
	for (i = 1; i <= m; i++) d[i] = t[i];
}

int main() {
	int i, n, m, r;
	scanf("%d%d", &n, &m);
	for (i = 1; i <= m; i++) scanf("%d", &a[i]);
	for (i = 1; i <= m; i++) scanf("%d", &c[i]);
	Kitamasa(n, m);
	r = 0;
	for (i = 1; i <= m; i++) r = (r + (long long)a[i] * d[i]) % P;
	printf("%d", r);
}

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

Dividing Polynomials  (0) 2017.01.12
Discrete Fourier Transform  (0) 2016.11.17
Number Theoretic Fast Fourier Transform  (1) 2016.11.02

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

Generate Connected Graph

DM2016. 11. 7. 02:03
#include "testlib.h"
#include <vector>
#include <unordered_set>

std::vector<int> P;

int Root(int X) {
	return X == P[X] ? X : P[X] = Root(P[X]);
}

void GenConnectedGraph(int N, int M, std::vector<std::pair<int, int> > &R) {

	if (N < 1 || M + 1 < N || M > (long long)N * (N - 1) / 2) return;

	P.resize(N);
	for (int i = 0; i < N; i++) P[i] = i;

	std::unordered_set<long long> S;

	R.resize(M);
	for (int i = 0; i < N - 1; i++) {
		int u, v;
		do {
			u = rnd.next(N);
			v = rnd.next(N);
		} while (Root(u) == Root(v));
		P[Root(u)] = Root(v);
		R[i].first = u;
		R[i].second = v;
		S.insert((long long)u << 32 | v);
	}

	for (int i = N - 1; i < M; i++) {
		int u, v;
		do {
			u = rnd.next(N);
			v = rnd.next(N);
		} while (u == v || S.find((long long)u << 32 | v) != S.end() || S.find((long long)v << 32 | u) != S.end());
		R[i].first = u;
		R[i].second = v;
		S.insert((long long)u << 32 | v);
	}

	shuffle(R.begin(), R.end());
}

int main(int argc, char *argv[]) {
	registerGen(argc, argv, 1);

	int N = 7;
	int M = 10;

	std::vector<std::pair<int, int> > E;
	GenConnectedGraph(N, M, E);

	printf("%d %d\n", N, M);
	for (int i = 0; i < M; i++) printf("%d %d\n", E[i].first, E[i].second);

	return 0;
}

'DM' 카테고리의 다른 글

Generate Tree  (0) 2016.11.07
Testlib  (0) 2016.11.07

Generate Tree

DM2016. 11. 7. 01:35
#include "testlib.h"
#include <vector>

std::vector<int> P;

int Root(int X) {
	return X == P[X] ? X : P[X] = Root(P[X]);
}

void GenTree(int N, std::vector<std::pair<int, int> > &M) {

	P.resize(N);
	for (int i = 0; i < N; i++) P[i] = i;

	M.resize(N - 1);
	for (int i = 0; i < N - 1; i++) {
		int u, v;
		do {
			u = rnd.next(N);
			v = rnd.next(N);
		} while (Root(u) == Root(v));
		P[Root(u)] = Root(v);
		M[i].first = u;
		M[i].second = v;
	}
}

int main(int argc, char *argv[]) {
	registerGen(argc, argv, 1);

	int N = 7;

	std::vector<std::pair<int, int> > E;
	GenTree(N, E);

	printf("%d\n", N);
	for (int i = 0; i < N - 1; i++) printf("%d %d\n", E[i].first, E[i].second);

	return 0;
}

'DM' 카테고리의 다른 글

Generate Connected Graph  (0) 2016.11.07
Testlib  (0) 2016.11.07

Testlib

DM2016. 11. 7. 01:08

Testlib는 Codeforces에서 만든 라이브러리이다.


Generator, Validator, Interactor 그리고 Checker를 만드는 데 사용할 수 있으며, C++ 표준을 따르는 어떤 컴파일러에서 실행시켜도 동일한 결과를 내놓으므로 컴파일러마다 작동이 다른 stdlib의 rand 함수로 만드는 것보다 데이터를 유지보수하기 좋다.


~


GitHub


Testlib를 사용하려면 공식 GitHub에 들아가서 testlib.h 파일을 클릭한 뒤, 소스를 복사하거나 Raw 버튼을 클릭하여 저장하면 된다. 이후 소스 파일과 같은 폴더에 넣은 뒤, 소스코드의 가장 앞 부분에 #include "testlib.h" 한 줄만 적어주면 Testlib를 사용할 수 있다.


~


Microsoft Visual Studio 2015


Microsoft Visual Studio 2015에서 Testlib를 사용하려고 하면 에러가 두 개 발생한다. (Testlib 0.9.10)


첫 번째 에러는 426번째 줄에서 발생하는 에러로, 407줄~409줄에 있는 다음 부분을 주석처리하면 해결된다.


/*
#ifndef _fileno
#define _fileno(_stream)  ((_stream)->_file)
#endif
*/


두 번째 에러는 va_start와 관련해서 발생하는 에러인데, testlib.h의 가장 앞 부분에 다음을 추가하면 해결된다.


#define _CRT_NO_VA_START_VALIDATION


이 두 가지를 해결하면 Microsoft Visual Studio 2015에서 Testlib를 사용할 수 있다.


~


Generator


generator에서 쓸 수 있는 다양한 함수들이 존재하므로, 직접 testlib.h 파일의 함수들을 하나씩 보거나 많은 예제를 찾아보면서 익히는 것을 권장한다.


아래는 길이가 1 이상 argv[1] 이하인 무작위 수열을 생성하는 generator의 예시이다.


#include "testlib.h"
#include <vector>

int main(int argc, char *argv[]) {
	registerGen(argc, argv, 1);

	int MaxN = atoi(argv[1]);

	int N = rnd.next(1, MaxN);

	std::vector<int> A(N);

	for (int i = 0; i < N; i++) A[i] = i + 1;

	shuffle(A.begin(), A.end());

	printf("%d\n", N);
	for (int i = 0; i < N; i++) {
		if (i) printf(" ");
		printf("%d", A[i]);
	}
	printf("\n");

	return 0;
}


Testlib로 만든 generator는 맨 처음에 registerGen(argc, argv, 1)을 실행해야 한다. 이를 실행하면 인자로 넘어온 값들을 시드로 하여 랜덤함수를 초기화한다. 이 덕분에 generator를 실행할 때 인자를 바꿔주는 것만으로 같은 방법으로 생성된 서로 다른 데이터를 쉽게 만들 수 있다.

'DM' 카테고리의 다른 글

Generate Connected Graph  (0) 2016.11.07
Generate Tree  (0) 2016.11.07

Baekjoon Online Judge 13538

...2016. 11. 3. 22:55

https://www.acmicpc.net/problem/13538


이 문제는 Persistent Segment Tree를 이용하면 효율적으로 해결할 수 있다.


수의 범위가 1~500000이므로 2^19(524288)개의 리프 노드를 만들고, 각 노드는 해당하는 구간에 포함되는 원소의 개수를 들고 있는다.


즉, k번째 Segment Tree의 [l, r) 노드는 1~k번째 원소 중 l 이상 r 미만인 수의 개수이다.


이렇게 트리를 만들면 각 쿼리는 다음과 같은 방법으로 처리할 수 있다.


0. 0번 트리를 만든다. 수열이 비어있으므로 모든 노드는 0을 값으로 갖는다.


1. 새로운 트리를 만든다. 이 트리는 현재까지의 수열에 x를 추가한 트리이고, 이를 N번 트리로 명명한다.


2. R번 트리와 L-1번 트리를 고려한다. 루트로부터 출발해서 x와 xor한 값이 더 큰 방향에 원소가 있으면 해당 방향으로, 없다면 반대 방향으로 움직이는 것을 19번 반복하면 y를 구할 수 있다.


3. N -= k


4. R번 트리와 L-1번 트리를 고려한다. 루트로부터 출발해서 x+1을 찾아가면서, 오른쪽 자식으로 갈 때 R번 트리에서 왼쪽 자식에 포함되는 원소의 개수를 더해주고 L-1번 트리에서 왼쪽 자식에 포함되는 원소의 개수를 빼 주는 것을 19번 반복하면 x보다 작거나 같은 원소의 개수를 구할 수 있다.


5. R번 트리와 L-1번 트리를 고려한다. 루트로부터 출발해서 R번 트리에서 왼쪽 자식에 포함되는 원소의 개수에서 L-1번 트리에서 왼쪽 자식에 포함되는 원소의 개수를 뺀 것이 k 이상이라면 왼쪽 자식으로 이동하고, 아니라면 그 값을 k에서 뺀 뒤 오른쪽 자식으로 이동한다. 이를 19번 반복해서 도착한 노드의 번호가 k번째로 작은 수이다.


전체 시간복잡도는 O(M log x)가 된다.

#include <cstdio>

struct node {
	node *l, *r;
	int x;
} T[11050000], *a[500005];
int Tn, n;

void Add(node *p, node *q, int x, int y) {
	p->x = q->x + 1;
	if (y < 0) return;
	if (x >> y & 1) {
		p->r = T + ++Tn;
		p->l = q->l;
		Add(p->r, q->r, x, y - 1);
	}
	else {
		p->l = T + ++Tn;
		p->r = q->r;
		Add(p->l, q->l, x, y - 1);
	}
}

int Xor(node *p, node *q, int x, int y) {
	if (y < 0) return 0;
	if (x >> y & 1 ? p->l->x == q->l->x : p->r->x != q->r->x) return 1 << y | Xor(p->r, q->r, x, y - 1);
	return Xor(p->l, q->l, x, y - 1);
}

int Sum(node *p, int x, int y) {
	if (y < 0) return 0;
	if (x >> y & 1) return p->l->x + Sum(p->r, x, y - 1);
	return Sum(p->l, x, y - 1);
}

int Kth(node *p, node *q, int x, int y) {
	if (y < 0) return 0;
	if (p->l->x - q->l->x < x) return 1 << y | Kth(p->r, q->r, x - p->l->x + q->l->x, y - 1);
	return Kth(p->l, q->l, x, y - 1);
}

int main() {
	int i, j, k, m;
	scanf("%d", &m);
	for (i = 1; i < 1 << 19; i++) {
		T[i].l = T + (i << 1);
		T[i].r = T + (i << 1 | 1);
	}
	a[n = 0] = T + 1;
	Tn = 1 << 20;
	while (m--) {
		scanf("%d", &i);
		switch (i) {
		case 1:
			scanf("%d", &i);
			a[++n] = T + ++Tn;
			Add(a[n], a[n - 1], i, 18);
			break;
		case 2:
			scanf("%d%d%d", &i, &j, &k);
			printf("%d\n", Xor(a[j], a[i - 1], k, 18));
			break;
		case 3:
			scanf("%d", &i);
			n -= i;
			break;
		case 4:
			scanf("%d%d%d", &i, &j, &k);
			printf("%d\n", Sum(a[j], k + 1, 18) - Sum(a[i - 1], k + 1, 18));
			break;
		case 5:
			scanf("%d%d%d", &i, &j, &k);
			printf("%d\n", Kth(a[j], a[i - 1], k, 18));
			break;
		}
	}
}


'...' 카테고리의 다른 글

ONTAK 2010 Day 7 Generator  (0) 2018.07.17
BOJ Achievements  (0) 2018.05.17
Ubuntu PPTP VPN Server  (0) 2017.12.17
Baekjoon Online Judge 1659  (0) 2016.02.02
1 이상 N 이하의 정수에서 i의 빈도수  (0) 2016.01.27

다음과 같은 문제를 생각해 보자.


"2차원 평면에 N개의 점이 있다. M개의 직사각형이 주어질 때, 각 직사각형 내부에 있는 점의 개수를 구하여라. 단 직사각형이 주어질 때마다 답을 구해야 한다."


모든 직사각형이 주어진 뒤에 답을 구하라고 한다면 점과 직사각형을 모두 정렬한 뒤 Segment Tree 등의 자료구조를 사용해서 O((N + M) log (N + M))으로 처리할 수 있지만, 이 문제에서는 직사각형이 주어질 때마다 답을 구해야 하기 때문에 쿼리를 정렬하는 방법은 사용할 수 없다.


한 가지 관찰할 수 있는 점은 (x1, y1)을 왼쪽 아래 꼭지점으로, (x2, y2)를 오른쪽 위 꼭지점으로 갖는 직사각형 내부의 점의 개수를 Q(x1, y1, x2, y2)라 하면, Q(x1, y1, x2, y2) = Q(x1, 0, x2, y2) - Q(x1, 0, x2, y1 - 1)라는 것이다. 따라서 Q(x1, 0, x2, y) 꼴의 쿼리를 효율적으로 처리할 수 있다면, 원래의 쿼리도 효율적으로 처리할 수 있다.


Segment Tree를 y좌표의 범위만큼 만들고, k번째 Segment Tree의 각 노드는 y좌표가 k 이하인 점들에 대하여 해당 구간에 포함되는 점의 개수를 들고 있으면 쿼리를 O(log N)으로 처리할 수 있다.


다만 이렇게 했을 경우의 문제점은 전처리에 O(N^2)의 시간이 필요하다는 것이다.


이를 위해 존재하는 자료구조가 Persistent Segment Tree이다.


k번째 Segment Tree가 다음처럼 생겼다고 해 보자.





y좌표가 k+1인 점들의 x좌표가 3과 5라고 해 보자. 그러면 k+1번째 Segment Tree는 다음처럼 생겼을 것이다.





이 때 관찰할 수 있는 점은, k+1번째 Segment Tree의 노드의 대부분은 k번째 Segment Tree의 노드와 같고, 바뀌는 노드 개수가 적다는 점이다.


이렇게 바뀌는 노드 개수는 추가되는 점마다 O(log N)개이므로 총 O(N log N)개이다.


따라서 모든 노드를 새로 만들지 말고 바뀌는 노드들만 새로 만든 뒤, 나머지 노드들은 포인터로 관리하여 이전 Segment Tree의 노드를 참조하도록 하면 O(N log N)의 시간 만에 똑같은 동작을 하는 자료구조를 만들 수 있다.


이를 Persistent Segment Tree라고 하며, 대부분의 자료구조가 갱신이 가능한 것과는 다르게 갱신이 불가능하다. 그러나 2차원 쿼리를 O(log N)에 처리할 수 있다는 장점이 있다.


이 자료구조는 예제를 보는 것이 좀 더 이해하기 좋으므로, 이 글을 참고하는 것을 권장한다. (스포일러 주의)

'자료구조' 카테고리의 다른 글

Splay Tree  (20) 2016.09.04

Suffix Array and Longest Common Prefix implementation with Stable Radix Sort


#include <cstdio>

const int MAXN = 128;

int D[MAXN + 1];
void radixSort(const int *A, const int *B, int *C, int N, int M) {
	int i;
	for (i = 0; i <= M; i++) D[i] = 0;
	for (i = 0; i < N; i++) D[B[A[i]]]++;
	for (i = 1; i <= M; i++) D[i] += D[i - 1];
	for (i = N; i--; C[--D[B[A[i]]]] = A[i]);
}

int B[MAXN + 1];
int T[MAXN];
void suffixArray(const char *A, int *C, int N) {
	int i, j, k, l;
	for (i = 0; i < N; i++) {
		B[i] = A[i];
		T[i] = i;
	}
	radixSort(T, B, C, N, 128);
	for (i = k = 1; i < N && k < N; i <<= 1) {
		for (k = 0; k < i; k++) T[k] = k - i + N;
		for (j = 0; j < N; j++) if (C[j] >= i) T[k++] = C[j] - i;
		radixSort(T, B, C, N, N);
		T[C[0]] = k = 1;
		for (j = 1; j < N; j++) {
			if (B[C[j]] != B[C[j - 1]] || B[C[j] + i] != B[C[j - 1] + i]) k++;
			T[C[j]] = k;
		}
		for (j = 0; j < N; j++) B[j] = T[j];
	}
}

void LCP(const int *A, const char *B, int *C, int N) {
	int i, j;
	for (i = 0; i < N; i++) T[A[i]] = i;
	for (i = j = 0; i < N; i++) {
		if (T[i]) {
			while (B[i + j] == B[A[T[i] - 1] + j]) j++;
			C[T[i]] = j;
		}
		if (j) j--;
	}
}

char S[12] = "abracadabra";
int R[11], Q[11];

int main() {
	int i;
	suffixArray(S, R, 11);
	LCP(R, S, Q, 11);
	for (i = 0; i < 11; i++) printf("%d %d %d\n", i, R[i], Q[i]);
}

'문자열' 카테고리의 다른 글

Failure Function  (0) 2016.01.20

2^n 크기의 배열의 FFT를 구할 때, 우리는 다음과 같은 복소수 w를 이용한다.


w = cos(2π/k) + i sin(2π/k)


여기서 우리가 주목할 점은, k는 2의 거듭제곱이며 w^k = 1 이라는 사실이다.


그러므로 우리는 다음과 같은 꼴을 가지는 소수 p를 생각해 볼 수 있다.


p = a * 2^b + 1


이런 p를 잡으면 p의 원시근 x에 대하여 (x^a)^(2^b) mod p = 1 이고, 따라서 w 대신 (x^a)^(2^b/k)를 사용하여 n<=b를 만족하는 모든 2^n 크기의 배열에 대해 법 p로 FFT를 행할 수 있다.


아래는 위를 만족하는 충분히 큰 소수들 목록이다.


 p

 a

 b

 원시근

 덧셈

 곱셈

 3221225473

 3

 30

 5

 64bit signed

 64bit unsigned

 2281701377

 17

 27

 3

 64bit signed

 64bit signed

 2013265921

 15

 27

 31

 32bit unsigned

 64bit signed

 469762049

 7

 26

 3

 32bit signed

 64bit signed


각 수마다 덧셈과 곱셈을 행할 때 사용해야 하는 자료형이 다르므로 상황에 맞게 사용하면 좋다.


#include <cstdio>

const int A = 7, B = 26, P = A << B | 1, R = 3;
const int SZ = 20, N = 1 << SZ;

int Pow(int x, int y) {
	int r = 1;
	while (y) {
		if (y & 1) r = (long long)r * x % P;
		x = (long long)x * x % P;
		y >>= 1;
	}
	return r;
}

void FFT(int *a, bool f) {
	int i, j, k, x, y, z;
	j = 0;
	for (i = 1; i < N; i++) {
		for (k = N >> 1; j >= k; k >>= 1) j -= k;
		j += k;
		if (i < j) {
			k = a[i];
			a[i] = a[j];
			a[j] = k;
		}
	}
	for (i = 1; i < N; i <<= 1) {
		x = Pow(f ? Pow(R, P - 2) : R, P / i >> 1);
		for (j = 0; j < N; j += i << 1) {
			y = 1;
			for (k = 0; k < i; k++) {
				z = (long long)a[i | j | k] * y % P;
				a[i | j | k] = a[j | k] - z;
				if (a[i | j | k] < 0) a[i | j | k] += P;
				a[j | k] += z;
				if (a[j | k] >= P) a[j | k] -= P;
				y = (long long)y * x % P;
			}
		}
	}
	if (f) {
		j = Pow(N, P - 2);
		for (i = 0; i < N; i++) a[i] = (long long)a[i] * j % P;
	}
}

int X[N];

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

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

Dividing Polynomials  (0) 2017.01.12
きたまさ法  (0) 2016.11.24
Discrete Fourier Transform  (0) 2016.11.17