Lec7 Divide and Conquer
Lec7 Divide and Conquer
1
This Lecture will be covered …(이번 수업에서 배울 점)
분할 정복 알고리즘
분할 정복의 응용: 거듭 제곱(Exponentiation)
분할 정복의 응용: 피보나치(Fibonacci) 수열
분할 정복의 응용: 행렬 곱셈(Matrix Multiplication) 연산
2
분할 정복
분할 정복의 개념:
- 주어진 문제를 재귀 반복(recursion)을 활용하여 해결하려는 전략 방법
- 만약 문제의 크기가 상대적으로 작은 경우(base case) 반복 없이 직접적으로 해결
- 반대의 경우(recursive case), 3 단계로 통해 수행됨
- 분할(Divide) : 해결하기 쉽도록 문제를 여러 개의 하위 문제로 나눔.
- 정복(Conquer) : 나눈 하위 문제를 각각 해결함
- 합병(Combine) : 하위 문제의 결과들을 결합하여 원래 문제에 대한 솔루션을 구성함
3
분할 정복의 응용: 거듭 제곱(Exponentiation)
거듭 제곱(Exponentiation)
- 지수(Exponent)의 크기만큼 곱셈을 수행하는 연산
- 예) C8 = C∙ C∙ C∙ C∙ C∙ C∙ C∙ C 이므로 O(n)의 수행 시간을 소요함
- 분할 정복을 적용하면 C8 = (C4)∙(C4) = (C4)2 과 같이 표현 할 수 있음
- 더욱 분할을 수행하면 C8 = {(C4)2}2 C2를 계산한 뒤 제곱을 두 번 더 반복하여 같은 결과를 얻을 수
있음, 즉 3번의 제곱 재귀(recursion)
4
분할 정복의 응용: 거듭 제곱(Exponentiation)
거듭 제곱(Exponentiation)
- 지수가 홀수(odd)인 경우
- = × × = ×
- = × × -> n이 홀수
- = × -> n이 짝수
- 거듭 제곱을 분할 정복을 이용하여 수행한 시간 복잡도는 O(logn)
5
분할 정복의 응용: 거듭 제곱(Exponentiation)
분할 정복을 이용한 거듭 제곱(Exponentiation)의 파이썬 구현
def exponentiation(c, n):
if n == 0:
return 1
x = exponentiation(c, n//2)
if n % 2 == 0:
return x * x
else:
return x * x * c
print(exponentiation(2,256))
#115792089237316195423570985008687907853269984665640564039457584007913129639936
6
분할 정복의 응용: 피보나치(Fibonacci) 수
피보나치 수
- 예)토끼 번식 : 첫번째 달에 토끼 한상이 태어났다고 가정
- 이 토끼는 두 달이 되면 다 자라서 번식이 가능하고, 매달 새끼 한 쌍을 낳음
- 8달 후에는 몇 마리의 토끼가 있을까? => 답: 21쌍 (42마리)
달수 토끼의 수(단위: 쌍) - 토끼가 다 자라기까지는 두 달이 걸리므로 두 달까지는
1 1
토끼 수의 변화가 없음
2 1
- 세 달 째가 되면 새끼 한 쌍을 낳음
3 2
4 3 - 네 달 째에는 최초의 토끼가 또 한 쌍의 새끼를 낳지만
5 5 세 달 째에 태어난 새끼들은 아직 다 자라지
6 8 않았으므로 토끼 수의 총합은 3쌍
7 13
8 21
7
분할 정복의 응용: 피보나치(Fibonacci) 수
피보나치 수
- 토끼 번식 이야기는 이탈리아의 수학자 레오나르도 피보나치의 저서 “계산의 책(Liber Abbaci)”에
소개된 문제
- 이 문제의 답에 해당하는 1, 1, 2, 3, 5, 8, 13, … 의 수열을 일컬어 ‘피보나치 수열‘이라 칭함
- 피보나치 수열 정의
- 20번까지의 피보나치 수열
8
분할 정복의 응용: 피보나치(Fibonacci) 수
간단한 피보나치 수의 파이썬 구현
def fibonacci(n):
if n == 0:
return 0
elif n == 1 or n == 2:
return 1
else:
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(8))
#21
재귀 호출
- fibonacci(n)을 호출하면 두개의 재귀 호출 fibonacci(n-1) 그리고 fibonacci(n-2) 발생됨
9
분할 정복의 응용: 피보나치(Fibonacci) 수
재귀 호출 계속
- 두 번째 단계에서는 fibonacci() 호출이 네 개로 늘어남
- 그 다음 단계를 전개하면 모두 8개의 fibonacci() 함수 호출이 생기며, 이 반복은 n이 0이 될 때까지
계속됨
- 이 알고리즘이 n번째 피보나치 수를 구하기 위한 작업량, 즉 수행 시간은 O(2n)
- 분할 정복 기법을 이용하면 피보나치 수를 찾는 데에 드는 소요 시간을 O(logn) 수준으로 줄일 수
있음
10
분할 정복의 응용: 피보나치(Fibonacci) 수
분할 정복을 활용한 피보나치 수열
- 1, 그리고 2 번째 피보나치 수를 각각 F1, F2이라고 하면, 2차원 정 사각 행렬로 나타내면 다음과
같음
11
분할 정복의 응용: 피보나치(Fibonacci) 수
분할 정복을 활용한 피보나치 수열
- 분할 정복을 이용하면, n/2 번째 피보나치 수를 구할 때 (n/2)/2번째 피보나치 수를 찾아서
제곱하면 됨
- 즉 n 번째 피보나치 수를 구하기 위해서는 최초의 행렬을 logn회 제곱하면 됨.
- 예) 10번째 피보나치 수를 찾기
1 1 1 1 1 1
- 다음, 10/2 5인데, 5는 홀수 이므로 = 을 활용하여 다음과 같은 식을
1 1 1 1 1 1
이끌어냄
12
분할 정복의 응용: 피보나치(Fibonacci) 수
분할 정복을 활용한 피보나치 수열
- 4 번째 피보나치 수를 구하기 위해 제곱수를 2로 나눔
최종적으로 F10은 55
13
분할 정복의 응용: 피보나치(Fibonacci) 수
분할 정복을 활용한 피보나치 수열의 파이썬 구현
def power(F, n): def multiply(F, M): def fibonacci(n):
병합
if( n == 0 or n == 1): x = (F[0][0] * M[0][0] + F = [[1, 1],
return F[0][1] * M[1][0]) [1, 0]]
M = [[1, 1], y = (F[0][0] * M[0][1] + if (n == 0):
[1, 0]] F[0][1] * M[1][1]) return 0
z = (F[1][0] * M[0][0] + power(F, n - 1)
power(F, n // 2) F[1][1] * M[1][0])
multiply(F, F) w = (F[1][0] * M[0][1] + return F[0][0]
F[1][1] * M[1][1])
if (n % 2 != 0): print(fibonacci(8))
multiply(F, M) F[0][0] = x #21
F[0][1] = y
F[1][0] = z
분할 F[1][1] = w
정복
14
분할 정복의 응용: Multiplying Square Matrices (행렬의 곱셈)
단순한 행렬의 곱셈 알고리즘의 pseudocode:
- 시간 복잡도: 3중으로 중첩된 for 루프는 각각 정확히 N번 반복 실행되고 수도코드의 4행을 실행할
때는 일정한 시간(곱셈)이 걸리기 때문에 행렬 곱셈은 O(n3) 시간에 작동함.
15
Multiplying Square Matrices (행렬의 곱셈)
단순한 행렬의 곱셈 알고리즘의 파이썬 구현:
def Simple_Matrix_Multiplication(A,B):
C = [[0 for i in range(len(B[0]))] for j in range(len(A))]
for i in range(len(A)):
for j in range(len(B[0])):
for k in range(len(A[0])):
C[i][j] = C[i][j] + A[i][k]*B[k][j]
return C
A=[[1,2,3,4],[3,4,5,6],[5,6,7,8],[7,8,9,10]]
B=[[4,1,1,0],[1,0,9,8],[9,8,7,6],[7,6,5,4]]
print(Simple_Matrix_Multiplication(A,B))
#[[61, 49, 60, 50], [103, 79, 104, 86], [145, 109, 148, 122], [187, 139, 192, 158]]
16
Multiplying Square Matrices (행렬의 곱셈)
분할 정복을 이용한 두 N x N 행렬의 곱셈 :
- 분할 정복 적용: 두 행렬 A,B을 4개의 (N/2) x (N/2)의 행렬 A11, A12, A21, A22 그리고 B11, B12,
B21, B22으로 나눔
A11 A12 B11 B12 C11 C12
A= B= C=
A21 A22 B21 B22 C21 C22
- 다음, 서브 (N/2) x (N/2) 행렬인 C11 = A11∙B11 + A12∙B21, C12 = A11∙B12 + A12∙B22,
C21=A21∙B11 + A22∙B21, C22=A21∙B12 + A22∙B22을 계산하여 하나의 N x N 행렬 C로 결합하는
과정을 재귀적으로 반복한다.
C11 C12 A11 A12 B11 B12 A11 B11 + A12 B21 A11 B12 + A12 B22
= =
C21 C22 A21 A22 B21 B22 A21 B11 + A22 B21 A21 B12 + A22 B22
17
Multiplying Square Matrices (행렬의 곱셈)
분할 정복을 이용한 두 N x N 행렬의 곱셈의 pseudocode:
18
Multiplying Square Matrices (행렬의 곱셈)
분할 정복을 이용한 두 N x N 행렬의 곱셈의 pseudocode:
MATRIX-MULTIPLY-
RECURSIVE 함수를 8차례
호출
19
Multiplying Square Matrices (행렬의 곱셈)
분할 정복 기반 행렬의 곱셈 알고리즘의 파이썬 구현:
def merge_matrix(A, B):
def MatAdd(A,B):
def createSubmatrices(A,starting_index,rows,columns):
20
Multiplying Square Matrices (행렬의 곱셈)
분할 정복 기반 행렬의 곱셈 알고리즘의 파이썬 구현:
def MMRecursive(A,B,n):
if(n==1):
return [[A[0][0]*B[0][0]]]
else:
A11,B11 = createSubmatrices(A, (0,0), n//2, n//2), createSubmatrices(B, (0,0), n//2, n//2)
A12,B12 = createSubmatrices(A, (0,n//2), n//2, n//2), createSubmatrices(B, (0,n//2), n//2, n//2)
분할
A21,B21 = createSubmatrices(A, (n//2,0), n//2, n//2), createSubmatrices(B, (n//2,0), n//2, n//2)
A22,B22 = createSubmatrices(A, (n//2,n//2), n//2, n//2), createSubmatrices(B, (n//2,n//2), n//2, n//2)
C = MMRecursive(A, B, 4)
print(C)
#[[61, 49, 60, 50], [103, 79, 104, 86], [145, 109, 148, 122], [187, 139, 192, 158]]
21
행렬 곱셈을 위한 Strassen’s 알고리즘
분할 정복을 이용한 Strassen 알고리즘 기반 두 N x N 행렬의 곱셈 :
- 분할 정복으로 서브 행렬로 나눈 7번의 곱셈으로 행렬 곱을 구하는 방법
- 분할 정복 적용: 두 행렬 A,B을 4개의 (N/2) x (N/2)의 행렬 A11, A12, A21, A22 그리고 B11, B12,
B21, B22으로 나눔
A11 A12 B11 B12 C11 C12
A= B= C=
A21 A22 B21 B22 C21 C22
- 다음, 서브 (N/2) x (N/2) 행렬인 C11 = m1+m4-m5+m7, C12 = m3+m5, C21=m2+m4,
C22=m1+m3-m2+m6을 계산하여 하나의 N x N 행렬 C로 결합하는 과정을 재귀적으로 반복한다.
C11 C12 m1 + m4 – m5 + m7 m3 + m5
=
C21 C22 m2 + m4 m1 + m3 – m2 + m6
(N/2) x (N/2) 행렬에 대해 7개의 곱셈 연산
m1= (A11 + A22)(B11 + B22) m5= (A11 + A12)∙B22
그리고 18개의 덧셈/뺄셈 연산을 수행함
m2= (A21 + A22)∙B11 m6= (A21 – A11)∙(B11 + B12)
행렬의 길이가 기준치(threshold) 이상일 때는
m3= A11∙(B12 - B22) m7= (A12 – A22)∙(B21 + B22)
Strassen 알고리즘이 효과적임 -> O(n2.81)
m4= A21(B21 - B11) log27=2.8073
22
Multiplying Square Matrices (행렬의 곱셈)
Strassen 알고리즘 기반 두 N x N 행렬 곱셈의 파이썬 구현:
import numpy as np
def strassen(A,B):
n = len(A)
if n == 1:
return A * B
else:
a11, b11 = A[:len(A)//2,:len(A)//2], B[:len(B)//2,:len(B)//2]
a12, b12 = A[:len(A)//2,len(A)//2:], B[:len(B)//2,len(B)//2:]
a21, b21 = A[len(A)//2:,:len(A)//2], B[len(B)//2:,:len(B)//2] 분할
a22, b22 = A[len(A)//2:,len(A)//2:], B[len(B)//2:,len(B)//2:]
23
Multiplying Square Matrices (행렬의 곱셈)
Strassen 알고리즘 기반 두 N x N 행렬 곱셈의 파이썬 구현:
c11 = m1 + m4 - m5 + m7
c12 = m3 + m5
c21 = m2 + m4
c22 = m1 + m3 - m2 + m6
result = np.zeros((n,n))
result[:int(len(result)/2),:int(len(result)/2)] = c11
result[:int(len(result)/2),int(len(result)/2):] = c12 병합
result[int(len(result)/2):,:int(len(result)/2)] = c21
result[int(len(result)/2):,int(len(result)/2):] = c22
return result
A=np.array([[1,2,3,4],[3,4,5,6],[5,6,7,8],[7,8,9,10]])
B=np.array([[4,1,1,0],[1,0,9,8],[9,8,7,6],[7,6,5,4]])
C = strassen(A, B)
print(C)
#[[ 61. 49. 60. 50.]
[103. 79. 104. 86.]
[145. 109. 148. 122.]
[187. 139. 192. 158.]]
24
Multiplying Square Matrices (행렬의 곱셈)
분할 정복 기반 행렬의 곱셈 알고리즘과 Strassen 알고리즘의 비교: 랜덤 입력
O(n3)
A = np.random.randint(low=-100,high=100,size=(size,size),dtype=int)
B = np.random.randint(low=-100,high=100,size=(size,size),dtype=int)
O(n2.81)
25