cubelover의 블로그

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