분할 정복(Divide and Conquer)은 큰 문제를 여러 개의 작은 부분 문제로 분할한 다음 각각의 부분 문제를 해결하고, 그 결과를 바탕으로 큰 문제의 답을 구하는 문제 해결 방법이다. 동적 계획법과 설명이 비슷하지만 동적 계획법에서는 중복되는 부분 문제가 있었다. 분할 정복의 부분 문제들은 서로 중복되지 않는다.
분할 정복에도 기저 사례가 존재하며, 기저 사례는 더 작은 부분 문제로 분할하지 않고 바로 해결할 수 있다. 분할 정복 자체가 재귀함수를 이용하여 구현하는 경우가 많기 때문에 코드에 자연스럽게 추가되기도 한다.
지수가 큰 경우의 거듭제곱을 구하는 문제가 분할 정복을 사용하는 대표적인 예시이다. 지수가 크기 때문에 B번 반복되는 반복문을 쓰면 시간 초과를 피할 수 없다. 하지만 x2n=xn×xn,xa+b=xa×xb라는 성질을 이용하면 Θ(lg B)번의 곱셈만으로 A의 B제곱을 구할 수 있다. 원리는 B를 2의 거듭제곱의 합으로 나타낸 다음 A의 2n제곱을 적절히 곱하는 것이다. 예를 들어 B=11인 경우, 이 값을 2의 거듭제곱의 합으로 나타내면 8+2+1이 되고, A11=A8×A2×A로 구할 수 있다. 이 방법이 바텀업을 이용한 것이고, 탑다운 방식의 좀더 직관적인 풀이가 있는데, B가 짝수일 경우 AB=AB/2×AB/2이므로 AB/2를 계산한 후 제곱을 하면 되고, B가 홀수일 경우 AB=AB−1×A이므로 AB−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 |