본문 바로가기
Algorithm/Concepts

분할 정복(Divide and Conquer)

by 컴돈AI 2023. 10. 12.

목차

    분할 정복(Divide and Conquer)

    • 분할 정복은 큰 문제를 독립적인 작은 문제들로 나누어 해결합니다. 각각의 작은 문제들은 독립적으로 해결되며, 결합 단계에서 이러한 작은 문제들의 해답을 결합하여 큰 문제의 해답을 얻습니다.
      • 따라서 재귀형태로 구현하는 경우가 많습니다.
    • 대표적인 예시로는 병합 정렬(merge sort), 이분 탐색(binary search) ,거듭제곱 연산(a^n)등이 있습니다.
      • 병합 정렬
      • 이분탐색
      • 거듭제곱 연산
        • 기본적인 거듭제곱 연산의 시간복잡도는 O(n)입니다. 이는 'a'를 'n'번 곱하는 것을 의미합니다. 분할 정복을 사용하면 이를 O(log n)으로 줄일 수 있습니다.
       

    분할정복과 Top-Down 방식의 DP와 차이점

    • 분할정복은 DP의 Top-Down 방식과 유사합니다. 둘 다 작은 문제로 쪼개서 해결하기 때문입니다. 
    • 그럼 둘 사이에는 어떤 차이점이 존재할까요?
      • 분할정복은 문제를 분할했을 때 겹치는 부분 문제가 없을 때 효율적입니다.
      • 반대로 DP는 겹치는 문제가 발생했을때 사용합니다. DP는 중복되는 문제를 메모이제이션(memoization)기법을 통해 기록해두었다가 시간절약을 할 수 있습니다. 하지만 굳이 분할할때 겹치는 문제가 없을 경우면, 이러한 방법은 메모리를 낭비하는 방법이 됩니다.
      • 예를 들어, 거듭제곱의 경우는 중복되는 연산이 없기때문에 분할정복이 효율적이지만, 피보나치 수열처럼 분할을 하고 연산하는 과정에서 중복되는 연산이 많을 경우는 DP 방식이 효율적입니다.
    • 분할 정복(거듭제곱 예시) vs Top-Down DP(피보나치 수 예시)
      • 거듭제곱 (겹치는 것이 없음)
        •             a^n
                   /        \
              a^(n/2)       a^(n/2)
             /      \       /      \
          a^(n/4) a^(n/4) a^(n/4) a^(n/4)  
          ...  ...  ...
      • 피보나치 수 (겹치는 것이 다수 존재)
    • 정리하면, Top-Down 방식으로 문제를 접근할 수 있을때, 중복되는 연산이 존재한다면 DP, 아닐경우 분할정복으로 해결하면됩니다.
      • 저는 Top-Down 방식의 DP 보다는 Bottom-Up 방식의 DP로 코드를 작성하는 것을 선호하기 때문에 Top-Down 방식의 DP일 경우 Bottom-Up 방식의 DP로 변경하여 문제를 해결합니다.

    예시

    • [백준 10830] 행렬 제곱
      • 행렬은 곱셈에 있어 교환 법칙은 성립하지 않지만, 곱셈의 결합법칙은 성립합니다.
      • 즉, (AB)C = A(BC) 인데 이럴 경우 뒤에 먼저 계산해주더라도 문제가 없습니다.
      • 결국 A^B 를 A^(B/2)로 나누어 나가면서 거듭제곱의 분할 정복방식으로 문제를 풀어나가면 됩니다.
      • 코드
        • import sys
          
          input = sys.stdin.readline
          
          n, b = map(int, input().split())
          
          arr = [list(map(int, input().split())) for _ in range(n)]
          
          
          def matrix_dot(a, b):
              n_row = len(a)
              n_column = len(b[0])
          
              mid = len(a[0])
          
              ans_arr = [[0 for _ in range(n_column)] for _ in range(n_row)]
          
              for row in range(n_row):
                  for column in range(n_column):
                      value = 0
                      for i in range(mid):
                          value += ((a[row][i] % 1000) * (b[i][column] % 1000)) % 1000
                      ans_arr[row][column] = value % 1000
          
              return ans_arr
          
          
          def matrix_square(arr, n):
              if n == 1:
                  return arr
          
              if n % 2 == 1:
                  temp = matrix_square(arr, n // 2)
                  return matrix_dot(arr, matrix_dot(temp, temp))
              else:
                  temp = matrix_square(arr, n // 2)
                  return matrix_dot(temp, temp)
          
          
          answer = matrix_square(arr, b)
          for i in range(n):
              print(*[item % 1000 for item in answer[i]])
    • [백준 1725] 히스토그램
      • 이 문제를 만약 분할정복을 사용하지 않는다면, N개의 히스토그램 칸에 대해서 모든 히스토그램 칸에 대해서 시작지점과 끝지점을 선택해줘야합니다.
        • 이렇게될경우 결국 시간복잡도는 O(N^2)이 될것입니다. N의 최댓값이 100000이기 때문에 O(N^2)이라면 10000000000 으로 100억이 되게 됩니다.
        • 따라서 완전 탐색으로는 해결할 수 없는 문제입니다.
      • N이 100000일경우 O(nlog n)까지 허용됩니다. 모든 히스토그램 칸에 대해서 왼쪽,오른쪽으로 나눠서 각 mid를 포함하는 가장 큰 직사각형의 넓이를 구해보는 방법을 적용해볼 수 있습니다.
        • 각 히스토그램 봉(mid)을 포함하는 가장 큰 직사각형 넓이를 구하면 됩니다.
        • 이렇게 할 경우 체크를 하지 못하는 경우가 있는거 아니야? 할 수 있습니다. 하지만 가운데봉을 포함하면서 가장 큰 직사각형을 모두 체크하기 때문에 포함되지 않는 경우를 생각해보려고 하더라도 이미 아래 과정에서 모두 포함되어져 있을 것입니다.
      • 참고 : 분할정복보다 스택으로 푸는 방식이 O(N)으로 시간효율성이 더 높습니다. 
      • 코드
        • import sys
          
          sys.setrecursionlimit(int(1e6))
          input = sys.stdin.readline
          
          n = int(input())
          
          arr = [int(input()) for _ in range(n)]
          
          
          def find(left, right):
              global max_value
          
              if left > right:
                  return
          
              mid = (left + right) // 2
          
              left_idx = mid
              right_idx = mid
              min_height = arr[mid]
              while True:
                  max_value = max(max_value, min_height * (right_idx - left_idx + 1))
          
                  if left_idx - 1 >= left and right_idx + 1 <= right:
                      if arr[left_idx - 1] >= arr[right_idx + 1]:
                          left_idx -= 1
                          min_height = min(min_height, arr[left_idx])
                      else:
                          right_idx += 1
                          min_height = min(min_height, arr[right_idx])
          
                  elif left_idx - 1 >= left and right_idx + 1 > right:
                      left_idx -= 1
                      min_height = min(min_height, arr[left_idx])
          
                  elif left_idx - 1 < left and right_idx + 1 <= right:
                      right_idx += 1
                      min_height = min(min_height, arr[right_idx])
          
                  else:
                      break
          
              find(left, mid - 1)
              find(mid + 1, right)
          
          
          max_value = 0
          find(0, n - 1)
          print(max_value)

    'Algorithm > Concepts' 카테고리의 다른 글

    동적 계획법(DP, Dynamic Programming)  (0) 2023.10.15
    이분 탐색(Binary Search)  (1) 2023.10.14
    탐욕법(Greedy)  (1) 2023.10.10
    큐(Queue), 스택(Stack)  (1) 2023.10.10
    완전 탐색(Brute-force)  (1) 2023.10.10