2023. 7. 7. 14:38ㆍAlgorithm
이번 글에서는, 알고리즘 설계 시 사용되는 접근법들 중 하나인 divide-and-conquer에 대해 살펴볼 것이다.
Divide-and-Conquer
Definition
divide-and-conquer는 instance를 작은 instance(s)로 쪼개 푼 것을 기반으로, original instance를 푼다.
Approach
divide-and-conquer 알고리즘은 다음 3단계 절차를 기반으로 설계돼야 한다.
- (문제의) instance를 (같은 문제의) 작은 instance(s)로 쪼갠다.(즉, 정렬 instance를 작은 정렬 instance로 쪼갠다.)
- 작은 instance(s)를 푼다. (이때, 작은 instance를 더 쪼갤 수 있다면, 재귀를 통해 푼다.)
- 작은 instance(s)의 solution(s)을 기반으로 original instance의 solution을 구한다.
이 접근법은 top-down 방식이다. top-level instance의 solution이 작은 instance(s)의 solution에 의해 구해지기 때문이다.
재귀 알고리즘 설계 시,
- 작은 instance(s)의 solution(s)를 기반으로 original instance의 solution을 구하는 방법을 설계해야 한다.
- terminal condition을 설정해야 한다. 즉, 어느 instance가 terminal condition인지 설정해줘야 한다.
- (instance가) terminal condition인 경우, solution 설정을 해줘야 한다.
이제부터, divide-and-conquer에 대한 이해도를 높이기 위해, 이 접근법을 활용한 유명 알고리즘에 대해 살펴볼 것이다.
1. Binary Search
Problem
전형적인 탐색 문제다. 즉, 크기가 n인 정렬 배열 S에 탐색키 x가 있는지 확인하는 문제다.
Algorithm
만약 배열의 중간 item(=S[mid])과 x가 같다면, 중간 index(=mid) 반환. 그렇지 않다면,
- 배열을 반으로 쪼개, 작은 배열 2개(S[:mid]와 S[mid+1:])를 만든다.
- 작은 배열에서 탐색키 x가 있는지 확인하는 문제를 푼다. (만약, 작은 배열을 더 쪼갤 수 있다면, 재귀를 통해 풀어라)
- x가 S[mid]보다 크다면, S[:mid] 선택
- x가 S[mid]보다 작다면, S[mid+1:] 선택
- 작은 배열의 solution으로 original 배열의 solution을 구한다.
위 알고리즘은 꼬리 재귀(=재귀호출 이후에는 로직이 없는 경우)을 사용했다.
때문에, 꼬리 재귀(=tail-recursion) 버전에서 반복(=iteration) 버전으로 알고리즘을 수정할 수 있다.
이는, 재귀 호출로 생기는 메모리 사용량을 없애, 메모리 사용량을 획기적으로 줄일 수 있다.
Performance
base operation: x와 S[mid]간의 비교 연산
input size: n(=size of array)
worst-case time complexity:
W(n)=W(2n)+1=lgn+1∈Θ(lgn)
2. Mergesort
Problem
정렬 문제다. 즉, 크기가 n인 배열 S을 정렬하는 문제다.
Algorithm
- 배열을 반으로 쪼개, 작은 배열 2개(S[:mid]와 S[mid+1:])를 만든다.
- 작은 배열 2개를 각각 정렬한다. (만약, 작은 배열을 더 쪼갤 수 있다면, 재귀를 통해 정렬한다.)
- 정렬된 작은 배열 2개(U, V)를 합병하여, original 배열을 정렬합니다.
위 알고리즘은 in-place sort 알고리즘이다. 즉, 정렬을 하기 위해 추가 배열이 필요한 알고리즘이다.
배열을 반으로 쪼갤 때, 작은 배열 2개를 생성하는 방식은 크기가 n(1+1/2+1/4+⋯)=2n인 배열을 추가 생성하며,
작은 배열 2개를 합병할 때, 복사본 배열을 생성하는 방식은 크기가 n인 배열을 추가 생성한다.
Performance
base operation: x와 S[mid]간의 비교 연산
input size: n(=size of array)
worst-case time complexity:
W(n)=2W(n2)+n−1=nlgn∈Θ(nlgn)
Determining Thresholds
재귀 알고리즘은 재귀 호출로 상당한 overhead가 생긴다.
때문에, mergesort에서 배열 크기가 1이 될 때까지 재귀 호출을 하는 것보다,
중간 재귀 호출을 중단하고, 삽입 정렬을 사용하는 것이 더 빠르다.
그럼 언제 재귀 호출을 중단해야 하는지 즉, optimal threshold value 구하는 방법에 대해 살펴보겠다.
optimal threshold value는 1). 재귀 알고리즘(=merge-sort), 2). 대체 알고리즘(insertion-sort) 그리고 3). 컴퓨팅 자원에 따라 달라진다.
A 컴퓨터에서의 merge-sort의 time complexity는 W(n)=2W(n2)+32μn=32nlgnμn이며,
insertion-sort의 time complexity는 W(n)=n(n−1)2μn이다.
이때, 대부분 n(n−1)2μn<32nlgnμn→n<591을 통해 optimal threshold value를 구한다. 하지만, 이는 잘못된 방법이다. 이 수식이 의미하는 바는 "n<591일 때, insertion-sort가 merge-sort 빠르다는 것을 의미한다." 즉, n<591은 threshold value지 optimal threshold value는 아니다.
자세히 말하자면, merge-sort를 사용한 후 insertion-sort를 사용할 때와 모두 insertion-sort를 사용할 때를 비교해 optimal threshold value를 구하는게 맞다.
When Not to Use Divide-and-Conquer
1. 크기가 n인 instance가 2개 이상의 크기가 n인 작은 instances로 나뉘는 경우 (dynamic programming)
2. 크기가 n인 instance가 n개의 크기가 n/c인 작은 instances로 나뉘는 경우
'Algorithm' 카테고리의 다른 글
[Algorithm Part 3] 4. Dynamic Programming (0) | 2023.07.13 |
---|---|
[Algorithm Part 3] 3. Divide-and-Conquer algorithms (0) | 2023.07.12 |
[Algorithm Part 3] 1. Introduction (0) | 2023.07.06 |
[Algorithm Part 2] 11. Reduction (0) | 2023.06.29 |
[Algorithm Part 2] 10. Data Compression (0) | 2023.06.27 |