본문 바로가기
알고리즘/[PRO] 삼성 SW 역량 테스트 B형

B형 필수 : 우선순위 큐 Priority Queue

by 피로물든딸기 2021. 2. 19.
반응형

삼성 B형 전체 링크

 

참고 

- 우선순위 큐 Priority Queue

- 우선순위 큐 응용 (1) - 두 개의 heap을 이용하여 중앙값 찾기

- 우선순위 큐 응용 (2) - 최댓값, 최솟값 동시에 관리하기

- 우선순위 큐 임의 원소 삭제

- 우선순위 큐 임의 원소 삭제 최적화

- 우선순위 큐 임의 원소 갱신, 변경

 

B형에서 Merge Sort는 마지막에 이분 탐색을 위해 필요한 경우가 많다.

 

그리고 '무언가'를 push해주고, 그 중 가장 우선순위가 높은 '무언가'를 pop하면서,

순서를 유지하고 싶을 때는 우선순위 큐를 이용해야된다.

 

요즘 B형에서는 HashTable + 우선순위 큐 모두 사용해야하는 경우가 많으므로, 

우선순위 큐에 대해서 익혀보자.

 

먼저 우선순위 큐 코드를 자유롭게 사용하고 싶으면, 그림을 그려가면서 직접 만들어보는 게 좋다.

 

우선순위 큐는 heap으로 만든다.

heap을 위한 1차원 배열을 선언하고, heap index = hn을 만들자.

int heap[16];
int hn;

hn은 최초로 0이다. 따라서 heap이 비어있는지는 if(hn)으로 확인할 수 있다.

 

heap[16]을 아래의 그림처럼 생각해야 된다. 이때 쉬운 index 관리를 위해 heap[0]은 버린다.

 

이렇게 heap을 관리하면 heap을 내려가거나 올라갈 때가 쉽다.

예를 들어서 heap[ i = 5 ] 라고 가정하자.

 

i = 5에서 자식 node(10, 11)로 이동하려면, i * 2 또는 i * 2 + 1을 하면 된다.

i = 5에서 부모 node(2) 로 이동하려면, i / 2를 하면 된다.

 

마찬가지로 모든 i에 대해서 자식 node는 i * 2, i * 2 + 1이 되고,

부모 node는 i / 2로 하면 된다.

 

이제 heap에 값을 순서대로 입력해보면서 push를 구현해보자.

최초로 hn = 0이므로 hn을 먼저 1로 만들어주고 x = 5를 넣어야 한다.

heap[++hn] = x; /* x = 5 */

 

그리고 6을 넣어보자. 지금 만들려고 하는 heap은 최소 heap이다.

즉, 우선 순위가 높다 = 가장 작은 값이 heap[1]에 있어야 되는 것을 의미한다.

heap[++hn] = x; /* x = 6 */

6을 넣었으니 비교를 해줘야 한다.

6이 부모 node와 비교해서 우선순위를 만족하는 지 체크해야 한다.

 

hn = 2이므로, 6의 부모 node는 hn / 2 = 1이 된다.

heap[hn = 2] = 6 > heap[hn = 1] = 5 이므로, 우선순위를 만족해서 변동이 없다.

 

이제 4를 push해보자.

heap[++hn] = x; /* x = 4 */

hn = 3이 되고, hn의 부모는 hn / 2 = 1이 된다.

그런데 heap[hn = 3] = 4 > heap[hn = 1] = 5 를 만족하지 않는다.

따라서 heap을 갱신해야 된다.

heap을 갱신할 때는, 우선순위를 계속 만족할 때 까지, 부모와 바꿔주면 된다.

 

따라서 for문을 이용하여 i = hn부터 i > 1을 만족할 때 까지 바꾼다.

for (int i = hn; i > 1; i /= 2)
{
	/* 부모 node <= 자식 node */
	if (heap[i / 2] <= heap[i]) break; /* heap을 만족하면 종료 */

	/* 만족할 때 까지 교환 */
	tmp = heap[i / 2];
	heap[i / 2] = heap[i];
	heap[i] = tmp;
}

 

heap[i / 2 = hn / 2 = 1] <= heap[3]을 만족하지 않으므로, tmp를 이용해 교환하게 된다.

 

다시 i /= 2에 의해 i = 1이 되고, 더 이상 비교할 부모가 없으므로 종료된다.

6과 5는 비교할 필요가 없다. heap은 항상 자식과 부모를 비교하기만 하면, 우선순위가 보장이 된다.

 

이제 -1을 push해 보자.

heap[++hn] = x; /* x = -1 */

 

hn = 4가 되고 heap[4] = -1이 된 상태이다.

부모 node는 hn / 2 = 2가 되고 

heap[hn / 2 = 2] <= heap[4] (6 <= -1)을 만족하지 않으므로, tmp를 이용해 다시 교환해야 한다.

for문을 이용해 바꿔보자. i = hn = 4, i / 2 = 2가 된다.

heap에 의해 위치가 변경되었고 i /= 2에 의해 i = 2가 되었다.

 

i = 2의 부모인 i / 2 = 1과 비교를 해보자.

heap[i / 2 = 1] <= heap[2] (4 <= -1)도 여전히 만족하지 않으므로, heap이 갱신된다.

이제 i = 1이 되었으므로 종료하게 된다.

 

마지막으로 3를 push해보자.

heap[++hn] = x; /* x = 3 */

 

hn = 5가 되고 hn의 부모는 다시 hn / 2 = 2가 된다.

heap[hn / 2 = 2] <= heap[5] (5 <= 3) 를 만족하지 않는다.  따라서 갱신이 일어난다.

i = 2가 되고 heap[i / 2 = 1] <= heap[i = 2] (-1 <= 3)을 만족하므로, break에 의해 종료된다.

 

여기에서 heap[3] = 5이고 heap[5] = 4인 것을 확인해보자.

값이 큰 5가 4보다 위의 Level에 있어서, 우선순위가 틀린 것처럼 보일 수도 있다.

 

하지만 heap은 부모 자식 간에만 우선순위를 만족해주기만 하면 된다.

heap[3]과 heap[5]는 부모 자식관계가 아니므로 (i = 3 의 자식은 i * 2 = 6과 i * 2 + 1 = 7이다.)

우선순위는 여전히 유지된다.

 

최종 push 코드는 아래와 같다. 여러 heap을 사용하기 위해 포인터와 참조자를 이용해서 구현하면 편하다.

void push(int* heap, int& hn, int x)
{
	int tmp;

	heap[++hn] = x;
	for (int i = hn; i > 1; i /= 2)
	{
		if (heap[i / 2] <= heap[i]) break;

		tmp = heap[i / 2];
		heap[i / 2] = heap[i];
		heap[i] = tmp;
	}
}

 

이제 pop을 구현해보자.

현재의 heap에서 pop을 하면 -1이 된다. -1을 빼고 나면 우선순위가 어떻게 변해야 될까?

현재 heap에서 -1 다음으로 작은 값은 3이다. 

 

pop은 아래와 같이 구현되는데, 최종적으로 3이 heap[1]에 오게 되는지 확인해보자.

 

1) pop의 return값 heap[1]을 기억해둔다.

2) heap의 배열에서 가장 끝에 값, 즉 heap[hn]을 heap[1]로 변경한다.

3) heap[hn]은 최악의 우선순위값(최소힙이므로 0x7fff0000 정도의 큰 값)으로 변경해둔다. 

4) heap이 1개 줄었으므로 hn 을 1 감소 시킨다.

ret = heap[1];           /* 1) */
heap[1] = heap[hn];      /* 2) */
heap[hn--] = 0x7fff0000; /* 3) ~ 4) */

 

그림으로 구현을 여기까지의 구현을 확인해보자.

0x7fff0000을 무한대 ∞로 표시하였다. 실제 최악의 값은 입력 값의 범위에 따라 바뀔 수 있다.

 

이제 hn = 1부터 시작하여 자식 node를 훑어가면서 heap을 갱신한다.

자식 node 2개 중 우선순위가 더 큰 값(더 작은 값)을 바꿔주면 heap을 유지하게 된다.

for (int i = 1; i * 2 <= hn;)
{
	/* 자식 node 모두 비교해서 자신이 작다면 우선순위를 만족 종료. */
	if (heap[i] < heap[i * 2] && heap[i] < heap[i * 2 + 1]) break;
	else if (heap[i * 2] < heap[i * 2 + 1]) /* 왼쪽 node가 더 작다면 */
	{
		tmp = heap[i * 2];
		heap[i * 2] = heap[i];
		heap[i] = tmp;

		i = i * 2;
	}
	else /* 오른쪽 node가 더 작다면 */
	{
		tmp = heap[i * 2 + 1];
		heap[i * 2 + 1] = heap[i];
		heap[i] = tmp;

		i = i * 2 + 1;
	}
}

 

 

i = 1에서 왼쪽 node = 4와 오른쪽 node = 5 중 왼쪽이 더 작으므로, 왼쪽과 자리를 바꾸게 되고,

i = 2가 된다.

 

i = 2가 되어 자식 node의 index는 4와 5가 되지만, hn은 현재 4가 된다.

그런데 자식 node 중에 hn 이하인 것만 검사하는 코드보다는,  자식 node 모두를 비교하는 것이 편하다.

 

이러한 이유로 최초에 heap[hn] = 0x7fff0000 코드가 들어가게 된다.

index 5에는 이미 최악의 우선순위(∞)가 들어있어서 갱신이 되지 않는다.

 

최종 코드는 아래와 같고, B형에서 속도 향상을 위해 register를 쓰는 것을 잊지말자.

int heap[16];
int hn;

int pop(int* heap, int& hn)
{
	register int tmp, ret;

	ret = heap[1];
	heap[1] = heap[hn];
	heap[hn--] = 0x7fff0000;

	for (register int i = 1; i * 2 <= hn;)
	{
		if (heap[i] < heap[i * 2] && heap[i] < heap[i * 2 + 1]) break;
		else if (heap[i * 2] < heap[i * 2 + 1])
		{
			tmp = heap[i * 2];
			heap[i * 2] = heap[i];
			heap[i] = tmp;

			i = i * 2;
		}
		else
		{
			tmp = heap[i * 2 + 1];
			heap[i * 2 + 1] = heap[i];
			heap[i] = tmp;

			i = i * 2 + 1;
		}
	}

	return ret;
}

void push(int* heap, int& hn, int x)
{
	register int tmp;

	heap[++hn] = x;
	for (register int i = hn; i > 1; i /= 2)
	{
		if (heap[i / 2] <= heap[i]) break;

		tmp = heap[i / 2];
		heap[i / 2] = heap[i];
		heap[i] = tmp;
	}
}

 

이렇게 그림을 그려가면서 heap을 구현해두면 응용하기가 편하다.

heap의 우선순위를 check하는 부분과 heap[hn--] = 최악의 값을 수정하면

최소 힙, 최대 힙, 절댓값 힙, 구조체인 경우 등을 모두 만들 수 있다.

int pop(int* heap, int& hn)
{
	...
    heap[hn--] = 0x7fff0000;
    
	for (register int i = 1; i * 2 <= hn;)
	{
		if (heap[i] < heap[i * 2] && heap[i] < heap[i * 2 + 1]) break;
		else if (heap[i * 2] < heap[i * 2 + 1])
		...
    }
    
    ...
}

void push(int* heap, int& hn, int x)
{
	
    ...
    
	for (register int i = hn; i > 1; i /= 2)
	{
		if (heap[i / 2] <= heap[i]) break;
       
       	...
    }
    
}

연습 문제는 아래 링크를 참고하자.

 

최소 힙                - 위 예시에 대한 문제 연습

최대 힙                - if / else의 조건 변경 + heap[hn]에 최악의 값 넣기 연습.

절댓값 힙             - if / else의 조건 변경 + heap[hn]에 최악의 값 넣기 연습.

가운데를 말해요   - heap 2개를 이용하여 가운데 값을 우선 순위가 높아지도록 만들기.

이중 우선순위 큐  - heap 2개를 이용하여 최솟값최댓값 동시에 관리하기.

국영수                 - 구조체에 대한 if / else 조건 변경 + heap[hn]에 최악의 값 넣기 연습.

 

+ 우선순위 큐에서 임의 원소 삭제 방법은 링크에서 확인해보자.

+ 우선순위 큐에서 임의 원소 갱신 방법은 링크에서 확인해보자.

반응형

댓글