분할 정복(Divide and Conquer)은 큰 문제를 여러 개의 작은 부분 문제로 분할한 다음 각각의 부분 문제를 해결하고, 그 결과를 바탕으로 큰 문제의 답을 구하는 문제 해결 방법이다. 동적 계획법과 설명이 비슷하지만 동적 계획법에서는 중복되는 부분 문제가 있었다. 분할 정복의 부분 문제들은 서로 중복되지 않는다.
분할 정복에도 기저 사례가 존재하며, 기저 사례는 더 작은 부분 문제로 분할하지 않고 바로 해결할 수 있다. 분할 정복 자체가 재귀함수를 이용하여 구현하는 경우가 많기 때문에 코드에 자연스럽게 추가되기도 한다.
지수가 큰 경우의 거듭제곱을 구하는 문제가 분할 정복을 사용하는 대표적인 예시이다. 지수가 크기 때문에 $B$번 반복되는 반복문을 쓰면 시간 초과를 피할 수 없다. 하지만 $x^{2n}=x^n\times x^n,x^{a+b}=x^a \times x^b$라는 성질을 이용하면 $\Theta(\text{lg }B)$번의 곱셈만으로 $A$의 $B$제곱을 구할 수 있다. 원리는 $B$를 $2$의 거듭제곱의 합으로 나타낸 다음 $A$의 $2^n$제곱을 적절히 곱하는 것이다. 예를 들어 $B=11$인 경우, 이 값을 $2$의 거듭제곱의 합으로 나타내면 $8+2+1$이 되고, $A^{11}=A^8\times A^2\times A$로 구할 수 있다. 이 방법이 바텀업을 이용한 것이고, 탑다운 방식의 좀더 직관적인 풀이가 있는데, $B$가 짝수일 경우 $A^B=A^{B/2}\times A^{B/2}$이므로 $A^{B/2}$를 계산한 후 제곱을 하면 되고, $B$가 홀수일 경우 $A^B=A^{B-1}\times A$이므로 $A^{B-1}$을 계산하고 $A$를 곱하면 된다. $B$가 홀수이므로 $B-1$은 짝수가 될 것이다. 이는 재귀호출로 간단하게 구현할 수 있다. 재귀함수를 작성할 때 주의해야 할 점은 $B$가 짝수일 때의 처리를 다음과 같이 하면 안 된다는 것이다.
int pow(int a, int b, int c)
{
if(b == 0)return 1;
else if(b % 2)return pow(a, b-1, c) * a % c;
else return pow(a, b/2, c) * pow(a, b/2, c) % c;
}
$B$가 짝수일 때 함수 호출이 두 번 되기 때문에 분할 정복의 의미가 없어지게 된다. 이 코드를 다음과 같이 수정하면 분할 정복의 의미를 살릴 수 있다.
int pow(int a, int b, int c)
{
if(b == 0)return 1;
else if(b % 2)return pow(a, b-1, c) * a % c;
else
{
int k = pow(a, b/2, c);
return k * k % c;
}
}
$B$가 짝수일 때 함수 호출을 한 번만 하고 같은 결과를 얻게 된다. 위의 문제의 경우 입력값이 크므로 $8$바이트 정수 자료형을 써야 한다.
이외에도 다양한 종류의 분할 정복 기법이 존재한다. 또한 단독으로 쓰이지 않고 다른 알고리즘의 일부로 사용되는 경우도 자주 찾아볼 수 있다.
[연습문제]
BOJ 2447. 별 찍기 - 10 (Silver I)
재귀함수로 패턴을 만드는 문제이다. 중간중간에 생기는 패턴을 저장한 다음 한꺼번에 출력해야 한다.
'Algorithm > B. Basic Algorithms' 카테고리의 다른 글
26. Bitmasks (0) | 2021.02.05 |
---|---|
24. Dynamic Programming & Memoization (0) | 2021.01.31 |
23. Backtracking (0) | 2021.01.30 |
22. Recursion (0) | 2021.01.30 |
21. Greedy (0) | 2021.01.29 |