cubelover의 블로그

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

Stable Radix Sort

구현2016. 10. 21. 14:32

A[0..N-1]들을 B[A[i]] 오름차순으로 정렬하여 C[0..N-1]에 넣는다. (B[A[i]]들은 0 이상 M 이하의 정수)

만약 B[A[i]]의 값이 같다면 A에서 먼저 나온 순서대로 넣는다.


#include <cstdio>

const int MAXM = 10;

int D[MAXM + 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 A[10] = { 5, 0, 3, 7, 4, 1, 9, 8, 2, 6 };
int B[10] = { 3, 4, 3, 2, 1, 1, 4, 2, 3, 1 };
int R[10];

int main() {
	int i;
	radixSort(A, B, R, 10, 4);
	for (i = 0; i < 10; i++) printf("%d %d %d\n", i, R[i], B[R[i]]);
}


with vector


#include <cstdio>
#include <vector>

using namespace std;

void radixSort(const vector<int> &A, const vector<int> &B, vector<int> &C, int N, int M) {
	int i;
	vector<int> D(M + 1, 0);
	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]);
}

vector<int> A({ 5, 0, 3, 7, 4, 1, 9, 8, 2, 6 });
vector<int> B({ 3, 4, 3, 2, 1, 1, 4, 2, 3, 1 });
vector<int> R(10);

int main() {
	int i;
	radixSort(A, B, R, 10, 4);
	for (i = 0; i < 10; i++) printf("%d %d %d\n", i, R[i], B[R[i]]);
}


with template


#include <cstdio>
#include <vector>

using namespace std;

template <typename _RanItA, typename _RanItB>
void radixSort(_RanItA _AFirst, _RanItA _ALast, _RanItB _B, _RanItA _C, int M) {
	_RanItA _A;
	int i;
	vector<int> D(M + 1, 0);
	for (i = 0; i <= M; i++) D[i] = 0;
	for (_A = _AFirst; _A != _ALast; _A++) D[_B[*_A]]++;
	for (i = 1; i <= M; i++) D[i] += D[i - 1];
	for (_A = _ALast; _A != _AFirst; _C[--D[_B[*_A]]] = *_A) _A--;
}

int A[10] = { 5, 0, 3, 7, 4, 1, 9, 8, 2, 6 };
char B[11] = "cdcbaadbca";
int R[10];

int main() {
	int i;
	radixSort(A, A + 10, B, R, 128);
	for (i = 0; i < 10; i++) printf("%d %d %d\n", i, R[i], B[R[i]]);
}

'구현' 카테고리의 다른 글

Indexed Tree with template  (0) 2017.03.22

Splay Tree

자료구조2016. 9. 4. 00:39

Splay Tree는 Binary Search Tree의 한 종류이다.


삽입, 삭제, 검색 등의 쿼리를 amortized O(log n)에 처리 가능하며 Splay 연산을 이용해서 구간에 대한 쿼리가 자유롭고 AVL Tree나 Red-Black Tree와 같은 다른 Binary Search Tree보다 구현이 단순한 편이기 때문에 알아두면 좋다.


~


Splay Tree는 쿼리로 들어온 노드에 대해 splay 연산을 행해서 amortized O(log n) 시간에 동작하는 자료구조이다.


Splay 연산은 임의의 노드 x를 루트로 만드는 연산으로, 아래와 같은 과정을 통해 이루어진다. 여기서 Rotate(x)는 다음처럼 x를 x의 부모 위치로 올리는 연산이다.



1. x가 루트이면, 루트를 만드는 데 성공했으므로 종료한다.

2. x의 부모 p가 루트이면, Rotate(x)를 행하고 종료한다. (Zig Step)

3. x의 조부모를 g라고 하면, 다음 두 가지 경우가 있다.

3-1. g→p의 방향과 p→x의 방향이 같은 경우, Rotate(p) 이후 Rotate(x)를 행한다. (Zig-Zig Step)

3-2. g→p의 방향과 p→x의 방향이 다른 경우, Rotate(x)를 두 번 행한다. (Zig-Zag Step)

4. 1로 돌아가서 루트가 될 때까지 반복한다.


Zig-Zig Step과 Zig-Zag Step이 어떻게 동작하는지는 다음 그림을 보면 알 수 있다.


Zig-Zig Step



Zig-Zag Step



어떻게 이런 단순한(?) 과정을 통해 amortized O(log n)의 시간복잡도가 나오는지는 여기에 설명되어 있다.


~


노드 구조체


먼저 가장 기본이 되는 노드 구조체를 만든다.


struct node {
	node *l, *r, *p;
} *tree;


Splay Tree를 만들기 위해서는 부모를 가리키는 포인터, 자식들을 가리키는 포인터 총 세 개만 있으면 된다.


삽입, 삭제, 검색 쿼리를 위해서는 key가 필요하지만 그 부분은 아래에서 다시 다룬다.


~


회전


다음으로 회전을 만든다.


void Rotate(node *x) {
	node *p = x->p;
    node *b;
	if (x == p->l) {
		p->l = b = x->r;
		x->r = p;
	} else {
		p->r = b = x->l;
		x->l = p;
	}
	x->p = p->p;
	p->p = x;
    if (b) b->p = p;
	(x->p ? p == x->p->l ? x->p->l : x->p->r : tree) = x;
}


~


Splay


이제 Splay 연산을 만든다.


void Splay(node *x) {
    while (x->p) {
        node *p = x->p;
        node *g = p->p;
        if (g) Rotate((x == p->l) == (p == g->l) ? p : x);
        Rotate(x);
    }
}


~


삽입, 삭제, 검색


이로써 Splay Tree를 완성했다(?) 임의의 노드에 대해 Splay 연산을 행하면 해당 노드가 루트가 되고, 이 때 inorder 순서가 유지된다. 이제 남은 것은 key를 추가하는 것이다.


먼저 노드 구조체에 key라는 변수를 추가한다. 다음처럼 될 것이다.


struct node {
	node *l, *r, *p;
	int key;
} *tree;


이제 가장 단순한 Insert 함수를 만들자. 다음처럼 될 것이다.


void Insert(int key) {
	node *p = tree, **pp;
	if (!p) {
		node *x = new node;
		tree = x;
		x->l = x->r = x->p = NULL;
		x->key = key;
		return;
	}
	while (1) {
		if (key == p->key) return;
		if (key < p->key) {
			if (!p->l) {
				pp = &p->l;
				break;
			}
			p = p->l;
		} else {
			if (!p->r) {
				pp = &p->r;
				break;
			}
			p = p->r;
		}
	}
	node *x = new node;
	*pp = x;
	x->l = x->r = NULL;
	x->p = p;
	x->key = key;
	Splay(x);
}


마지막에 Splay 연산이 들어갔음에 유의하라. Splay Tree는 이처럼 삽입, 삭제, 검색한 노드에 대해 Splay 연산을 행함으로써 amortized O(log n) 시간복잡도로 만든다.


다음으로 Find 함수를 만들자. Find 함수를 호출한 뒤 루트가 해당 key를 가진 노드가 되므로 다른 부가적인 연산을 행하기 쉽다.


bool Find(int key) {
	node *p = tree;
	if (!p) return false;
	while (p) {
		if (key == p->key) break;
		if (key < p->key) {
			if (!p->l) break;
			p = p->l;
		} else {
			if (!p->r) break;
			p = p->r;
		}
	}
	Splay(p);
	return key == p->key;
}


마지막으로 Delete 함수이다. 삭제를 위해서 해당하는 노드를 Splay한 뒤, 자식이 0개 또는 1개인 경우 그냥 삭제하고 2개인 경우 두 서브트리를 붙여준다.


void Delete(int key) {
	if (!Find(key)) return;
	node *p = tree;
	if (p->l) {
		if (p->r) {
			tree = p->l;
			tree->p = NULL;
			node *x = tree;
			while (x->r) x = x->r;
			x->r = p->r;
			p->r->p = x;
			Splay(x);
			delete p;
			return;
		}
		tree = p->l;
		tree->p = NULL;
		delete p;
		return;
	}
	if (p->r) {
		tree = p->r;
		tree->p = NULL;
		delete p;
		return;
	}
	delete p;
	tree = NULL;
}


~


K번째 원소 찾기


사실 삽입, 삭제, 검색 연산은 set과 map에서도 지원하는 기능이기 때문에 이것을 위해 Binary Search Tree를 구현할 필요는 없다. 다만 Binary Search Tree를 직접 구현하면 K번째 원소를 O(log n)에 찾는 것이 가능하다는 것이 큰 장점이다.


K번째 원소를 찾기 위해서 노드 구조체에 서브트리에 있는 노드 개수를 저장하는 변수를 만들 필요가 있다. key가 없어도 동작하므로 여기서는 제외하고 설명한다.


struct node {
	node *l, *r, *p;
	int cnt;
} *tree;


cnt 변수의 갱신을 위해 Update 함수를 만든다. 이렇게 따로 분리해두면 K번째 원소 찾기 외에 구간 합 구하기 등 다른 기능들을 추가하기 편하다.


void Update(node *x) {
	x->cnt = 1;
	if (x->l) x->cnt += x->l->cnt;
	if (x->r) x->cnt += x->r->cnt;
}


그리고 Rotate 함수의 맨 마지막 부분에 다음을 추가하자.


void Rotate(node *x) {
	// ...
	Update(p);
	Update(x);
}


이로써 K번째 원소를 찾을 준비가 완료되었다. 이제 K번째 원소를 찾는 함수를 만들자.


void Find_Kth(int k) {
	node *x = tree;
	while (1) {
		while (x->l && x->l->cnt > k) x = x->l;
		if (x->l) k -= x->l->cnt;
		if (!k--) break;
		x = x->r;
	}
	Splay(x);
}


이 함수를 호출하면 K번째 원소가 루트가 된다. 여기서 K는 0-based이다.


~


구간 합 구하기


이제 본격적으로 Splay Tree를 문제 풀이에 사용해보자. 먼저 가장 간단한 구간 합 구하기부터 한다.


먼저 노드 구조체와 Update 함수를 고쳐서 합을 구할 수 있도록 한다. 또한 K번째를 찾는 기능도 필요하므로 이 또한 구현되어 있어야 한다.


struct node {
	node *l, *r, *p;
	int cnt;
	int sum, value;
} *tree;

void Update(node *x) {
	x->cnt = 1;
	x->sum = x->value;
	if (x->l) {
		x->cnt += x->l->cnt;
		x->sum += x->l->sum;
	}
	if (x->r) {
		x->cnt += x->r->cnt;
		x->sum += x->r->sum;
	}
}


처음에 노드를 구간 길이만큼 만들고 적당히 연결해 주면 초기화가 끝난다. 다음처럼 구현할 수 있다.


void Initialize(int n) {
	node *x;
	int i;
	tree = x = new node;
	x->l = x->r = x->p = NULL;
	x->cnt = n;
	x->sum = x->value = 0;
	for (i = 1; i < n; i++) {
		x->r = new node;
		x->r->p = x;
		x = x->r;
		x->l = x->r = NULL;
		x->cnt = n - i;
		x->sum = x->value = 0;
	}
}


먼저 i번째 원소에 값 z를 더하는 것을 구현하자. i번째 원소를 찾고 sum과 value에 z를 더해주면 된다.


void Add(int i, int z) {
	Find_Kth(i);
	tree->sum += z;
	tree->value += z;
}


이제 구간에 대한 합을 구해보자. 먼저 L부터 R까지의 구간(inclusive)을 한 노드로 모아주기 위해 다음 과정을 거친다.



즉, L-1번째 원소에 Splay 연산을 행한 뒤 오른쪽 서브트리에서 R-L+1번째 원소(전체에서 R+1)번째 원소에 Splay 연산을 행하면 L부터 R까지의 구간이 한 노드에 모이게 된다.


void Interval(int l, int r) {
	Find_Kth(l - 1);
	node *x = tree;
	tree = x->r;
	tree->p = NULL;
	Find_Kth(r - l + 1);
	x->r = tree;
	tree->p = x;
	tree = x;
}


Splay를 하는 과정에서 Rotate 함수를 호출하고, Rotate 함수에서 Update 함수를 호출하므로 해당하는 노드의 sum 값이 곧 구간의 합이 된다.


L이 구간 왼쪽 끝이거나 R이 구간 오른쪽 끝인 경우 예외처리를 해 줘야 하는데, 이는 더미노드를 2개 더 만듦으로써 쉽게 해결할 수 있다.


int Sum(int l, int r) {
	Interval(l, r);
	return tree->r->l->sum;
}


~


Lazy Propagation


Splay Tree에서 구간을 한 노드로 만들 수 있으므로 Lazy Propagation 또한 가능하다. 먼저 노드 구조체에 해당 구간에 더해진 값을 저장할 변수를 하나 추가하자.


struct node {
	node *l, *r, *p;
	int cnt;
	int sum, value, lazy;
} *tree;


그리고 lazy를 뿌려주는 함수를 작성한다.


void Lazy(node *x) {
	x->value += x->lazy;
	if (x->l) {
		x->l->lazy += x->lazy;
		x->l->sum += x->l->cnt * x->lazy;
	}
	if (x->r) {
		x->r->lazy += x->lazy;
		x->r->sum += x->r->cnt * x->lazy;
	}
	x->lazy = 0;
}


Lazy는 자식으로 내려갈 때마다 호출을 해 주어야 한다. 이 경우에는 Find_Kth 함수에만 추가해주면 된다.


void Find_Kth(int k) {
	node *x = tree;
	Lazy(x);
	while (1) {
		while (x->l && x->l->cnt > k) {
			x = x->l;
			Lazy(x);
		}
		if (x->l) k -= x->l->cnt;
		if (!k--) break;
		x = x->r;
		Lazy(x);
	}
	Splay(x);
}


이제 L부터 R까지의 구간에 값 z를 더해주는 함수를 만들자. L부터 R까지의 구간을 한 노드로 모아주고 값을 더해주면 된다.


void Add(int l, int r, int z) {
	Intrerval(l, r);
	node *x = tree->r->l;
	x->sum += x->cnt * z;
	x->lazy += z;
}


~


구간 뒤집기


Splay Tree를 이용하면 구간 뒤집기를 할 수 있다. 또한 구간 뒤집기를 하면서 구간 쿼리도 가능하다(!)


L부터 R까지의 구간을 뒤집는다는 것은 다음처럼 바뀐다는 뜻이다.


..., L-1, L, L+1, ..., R-1, R, R+1, ... → ..., L-1, R, R-1, ..., L+1, L, R+1, ...


L부터 R까지의 구간을 한 노드로 모으면 L과 R 사이에 있는 어떤 원소 K가 해당 구간을 나타낼 것이다.


..., L-1, L, L+1, ..., K-1, K, K+1, ..., R-1, R, R+1, ... → ..., L-1, R, R-1, ..., K+1, K, K-1, ..., L+1, L, R+1, ...


이를 주의깊게 보면, [L, K-1](K의 왼쪽 서브트리)과 [K+1, R](K의 오른쪽 서브트리)을 바꾼 뒤 각 구간을 다시 뒤집어 준 것과 같다는 것을 알 수 있다. 따라서 Lazy Propagation으로 이를 처리할 수 있다.


먼저 해당 구간이 뒤집혔는지 아닌지를 나타내는 변수를 만든다.


struct node {
	node *l, *r, *p;
	bool inv;
} *tree;


그리고 Lazy 함수를 다음처럼 만든다.


void Lazy(node *x) {
	if (!x->inv) return;
	node *t = x->l;
	x->l = x->r;
	x->r = t;
	x->inv = false;
	if (x->l) x->l->inv = !x->l->inv;
	if (x->r) x->r->inv = !x->r->inv;
}


마지막으로 구간을 뒤집어주는 함수를 만든다.


void Reverse(int l, int r) {
	Interval(l, r);
	node *x = tree->r->l;
	x->inv = !x->inv;
}


~


Splay Tree 연습문제


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

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

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

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

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

Persistent Segment Tree  (1) 2016.11.03

Baekjoon Online Judge 1659

...2016. 2. 2. 04:47

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


정수로 이루어진 두 집합 S와 T가 있다. S와 T 간에 원소를 연결해서 S의 각 원소가 적어도 하나의 T의 원소와 연결되고, T의 각 원소가 적어도 하나의 S의 원소와 연결되어야 한다. a와 b를 연결할 때의 비용은 |a-b|이고, 전체 비용의 합을 최소화시키는 것이 목적이다.


앞으로 적어도 하나의 다른 원소와 연결되는 것을 '연결 조건을 만족한다'고 하자.


소정리 1. 만약 a와 b를 연결하고 c와 d를 연결하는 최적해가 존재하고 a<c이며 b>d라면, a와 d를 연결하고 b와 c를 연결하는 최적해가 존재한다.


이는 두 쌍의 연결 방법을 바꿈으로써 다른 원소가 영향받지 않고, 비용이 증가하지 않음에서 쉽게 알 수 있다.


이 정리 하나만으로 쉽게 O(nm) 풀이를 만들 수 있다. D[i][j] = (S에서 오름차순으로 i개, T에서 오름차순으로 j개가 연결 조건을 만족할 때 최소 비용)으로 정의하면, 다음과 같은 점화식을 통해 계산이 가능하다.


D[i][j] = min { D[i-1][j-1], D[i-1][j], D[i][j-1] } + |S[i]-T[j]|


그러나 이 방법으로는 제한이 500,000이기 때문에 쉽게 시간 초과가 발생한다.


최적해가 가지는 다른 특징을 찾아보기 위해 예제를 다음과 같이 그림으로 나타내 보았다.



여기서 우리는 다음과 같은 소정리를 하나 얻는다.


소정리 2. S의 원소 a<c와 T의 원소 b<d가 있을 때 a와 b를 연결하고 c와 d를 연결하는 최적해가 존재한다면, a와 d를 연결하거나 b와 c를 연결하지 않는 최적해가 존재한다.


이게 무슨 소리인지를 그림으로 나타내면 아래와 같다.



즉, 그림처럼 a와 b, c와 d가 연결되어 있다면 여기서 a와 d를 연결하거나 b와 c를 연결하는 것은 이득이 되지 않는다는 것이다. 당연한 소정리지만 덕분에 우리는 모든 연결을 두 종류로 나눌 수 있다.



빨간색 실선을 강한 연결, 회색 화살표를 약한 연결이라고 하자. 강한 연결은 S와 T에서 하나씩 선택해 연결한 것이며, 약한 연결은 S 또는 T의 원소를 가장 가까운 다른 원소에 연결한 것이다. 이로부터 O(n+m) 풀이를 이끌어낸다.


S와 T의 원소들을 통째로 오름차순으로 정렬하고, D[i] = (i번째 원소까지 연결 조건을 만족했을 때 최소 비용)이라고 정의하자. 그러면 두 가지 경우가 생김을 알 수 있다.


첫 번째 경우는 i번째 원소가 약한 연결로 연결될 때이다. 이 경우는 i번째 원소에서 가장 가까운 다른 원소를 연결한다.


두 번째 경우는 어떤 j에 대하여 j+1~i번째 원소가 모두 강한 연결로 일대일 대응될 때이다. 이 경우 j+1~i번째 원소 중 S에 속하는 원소의 개수와 T에 속하는 원소의 개수가 같아야 한다. 그런 j가 여러 개일 수 있는데, 이 때 가장 큰 j만 고려해도 충분함을 알 수 있다. 만약 더 작은 j'에 대해서 j'+1~i까지 모두 일대일 대응시키는 것이 최적해라면, 이는 D[j]를 계산할 때 j'+1~j까지 일대일 대응시키는 경우가 포함되어 있고, D[i]를 계산할 때 j+1~i까지 일대일 대응시키는 경우가 포함되어 있으므로 가장 큰 j만 고려해도 충분하다.


첫 번째 경우는 O(1)에 처리할 수 있지만, 두 번째 경우는 다소 어려워 보인다. 우리가 고려하는 j가 j+1~i번째 원소 중 S에 속하는 원소의 개수와 T에 속하는 원소의 개수가 같은 최대의 j이므로, j+1~i-1에서는 항상 S에 속하는 원소의 개수가 T에 속하는 원소의 개수보다 많거나, 아니면 그 반대이다. 따라서 j+1~i번째 원소를 일대일 대응하는 최소 비용은 |(S에 속하는 원소들의 합)-(T에 속하는 원소들의 합)|이 되므로, 이 또한 O(1)에 처리할 수 있다.


입력으로 S와 T의 원소가 오름차순으로 주어지므로 이를 병합하면서 위 과정을 수행하면 O(n+m)에 해결이 가능하다.

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

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 13538  (3) 2016.11.03
1 이상 N 이하의 정수에서 i의 빈도수  (0) 2016.01.27

int frq(int n, int i) {
	int j, r = 0;
	for (j = 1; j <= n; j *= 10) if (n / j / 10 >= !i) r += (n / 10 / j - !i) * j + (n / j % 10 > i ? j : n / j % 10 == i ? n % j + 1 : 0);
	return r;
}

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

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 13538  (3) 2016.11.03
Baekjoon Online Judge 1659  (0) 2016.02.02

Failure Function

문자열2016. 1. 20. 03:47
for (F[i = 0] = j = -1; i < n; j < 0 || S[i] == S[j] ? F[++i] = ++j : j = F[j]);

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

Suffix Array and Longest Common Prefix  (1) 2016.11.02