4. 분할정복

4.2 행렬 곱셈을 위한 스트라센 알고리즘

4.분할정복

4.2 행렬곱셈을 위한 스트라센 알고리즘

행렬의 곱셈은 다음과 같다

n = A.rows
C는 새로운 n x n 행렬이라 하자.
for i - 1 to n
	for j = 1 to n
		c_ij = 0
		for k = 1 to n
			c_ij = c_ij + (a_ik)(b_kj)
return C

3중 반복문이기 때문에 수행시간 Θ(n^3)이다. Ω(n^3)이라고 예상하지만 사실 o(n^3) 방법이 있따.

스트라센 알고리즘(Streassen Algorithm)은 Θ(n^log_2(7))이 소요된다. 2.80 < log_2(7) < 2.81이므로 O(n^2.81) 이라고 할 수 있다.

단순한 분할정복 알고리즘

n = A.rows
C는 n x n 행렬이라 하자.
if n == 1
	c_11 = a_11 x b_11
else A, B, C를 다음과 같이 나눈다.
	C_11 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_11,B_11)
		 + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_12,B_21)
	C_12 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_11,B_12)
		 + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_12,B_22)
	C_21 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_21,B_11)
		 + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_22,B_21)
	C_22 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_21,B_12)
		 + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_22,B_22)
return C

위 코드는 행렬의 곱셈을 분항정복으로 나눠서 생각해본 것이다. 각 원소가 2의 거듭제곱 형태라고 가정하여 계산을 용이하게 한다.

n x n 크기의 행렬을 n/2 x n/2 크기의 행렬 4개로 나누는 것이 원리이다. A 는 A_11, A_12, A_21, A_22 로 나눠진다. B, C도 마찬가지이다. SQUARE-MATRIX-MULTIPLY-RECURSIVE는 행렬에서 곱셈을 나타내는 재귀함수이다. 2X2 크기의 행렬에서

C의 1행 1열 = (A의 1행 1열) x (B의 1행 1열) + (A의 1행 2열) x (B의 2행 1열) 이다. 이걸 나타낸 코드가 위에서 6~7행이다. 재귀인 이유는 저 A_11이 숫자가 아닐 수도 있기 때문이다. n/2 x n/2 크기의 행렬일 수도 있다. 그렇다면 또 그 행렬 내부로 재귀가 호출되어야 계산을 해야한다.

  • 분할 : 행렬 C가 행렬 A와 B의 곱이라고 하자. 각각이 n x n 크기의 행렬이라하면 n/2 x n/2 크기의 행렬이 한 개당 4개가 생성되어 총 12개의 행렬이 생성된다. 행렬을 나누는 것의 수행 시간은 어떻게 될까? 행과 열에 따라 복사하려면 Θ(n^2)의 시간이 걸릴 것이다. 하지만 기존 행렬에서 복사하지 않고 범위를 통해서만 부분행렬을 정의하면 Θ(1)이라는 상수의 수행시간으로 만들 수 있다. 왜냐하면, 위에서 나타난대로, 행렬의 곱셈에서 각 원소는 원소간의 곱의 합으로 나타내게 된다. 위에서 설명했듯 이 원소도 행렬이라고 볼 수 있다.

우리가 2x2크기의 행렬을 곱할때 하나의 숫자끼리 곱해져서 더해지지만 이를 1x1 크기의 행렬이라고 볼 수 있는것!

  • 계산 : n x n 크기의 행렬간의 곱에 걸리는 수행 시간을 T(n)이라 하자. 그렇다면 n/2 x n/2 크기로 나누었을 때 곱셈의 수행시간은 T(n/2)이다. 이 호출은 8번이 필요하다. n x n 크기의 행렬을 4등분 했으므로, 각각 한 부분의 값을 구할때는 두 행렬의 곱으로 이루어진 두개의 값을 더하기 때문이다. 한 개 당 2개를 더해서 이뤄지니 전체 4개에는 8번의 행렬의 곱셈 과정이 필요하다. 따라서 전체 수행시간은 8T(n/2)이다.
  • 조합 : 위에서 8번을 행렬의 곱셈 과정이 8번이 있어야 한다고 했고, 그 결과들을 더하는 데는 얼마나 걸릴까? n/2 x n/2 크기의 행렬에는 (n^2)/4 개의 원소가 있으니, 이는 곧 한 행렬마다 더할 때 걸리는 시간이다. 원소는 4등분을 해서 4개가 나타났으니 4를 곱하면 n^2이다. 따라서 조합에 걸리는 시간은 Θ(n^2)이다.

결국 T(n) = Θ(1) + 8T(n/2) + Θ(n^2) = 8T(n/2) + Θ(n^2)이다. 결론부터 말하자면 이것의 해는 T(n) = Θ(n^3)이다. 자세한 이유는 후에 마스터 방법에서 배우니 결과만 보자. 즉, 분할정복 접근법은 기존의 일반적인 방법보다 빠르지 않다. 이유가 뭘까?

분할에서 행렬을 나눌 때, 나눠야 할 행렬은 A와 B 두개이다. 따라서 하나가 Θ(1) 이니 2를 곱해야 한다. 하지만 Θ-표기법에 의해 상수 2를 생략한 것이다. 조합에서도 Θ((n^2)/4)였지만 Θ-표기법을 근거로 계수 1/4를 생략하였다.

하지만 8T(n/2)에서 8은 생략하지 않았다. 할 수 없다. 8을 생략해서 T(n/2)시간이 걸린다고 말하면 안되고 8T(n/2)라고 해야한다. 이전 2단원에서 병합정렬을 분할정복을 통하여 이해할 때 T(n) = 2T(n/2) + Θ(n) 임을 공부하였다. 이 과정에서 재귀를 보여주는 트리 모양의 형태를 그리면서 각 단계에서 하나하나가 어떤 수행시간을 가지며 그것이 총 몇개있는지 파악하였다. 여기서 2 라는 숫자는 재귀 과정에서 깊숙해 질수록 다음 단계가 몇개의 자식을 가지는 지, 그 깊이에서 몇개를 더해야 하는지에 관여하였다. 결론은

점근적 표기법이 상수 계수를 생략할 수 있다 해도 T(n/2)와 같이 재귀적인 표기에 대해서는 그렇지 않다.

스트라센의 방법

위에 T(n) = 2T(n/2) + Θ(n)에서 T(n/2)의 계수 2를 생략한다면? T(n) = T(n/2) + Θ(n)의 형태가 되고 재귀트리는 점차 갯수가 늘어나는 트리형태가 아니게 되고, 갯수가 유지되는 일직선의 형태를 보일 것이다.

스트라센의 방법은 재귀갯수를 줄이고, 덧셈의 횟수를 늘리는 것이다. 재귀는 생략할 수 없는 계수고, 덧셈은 생략 가능하기 떄문.

스트라센의 방법은 4단계로 이루어진다.

  1. 크기가 n x n 인 행렬 A와 B, C를 n/2 x n/2를 크기로 하는 부분 행렬들로 나눈다. 상술하였듯 이 과정은 Θ(1)의 시간이 걸린다.
  2. 행렬을 10개 만들고 각각 S_1, S_2, …, S_10 이라고 하자. 크기는 n/2 x n/2이며 1단계에서 만든 행렬의 합이거나 차이를 의미한다. 행렬 10개를 만드는데 걸리는 시간은 Θ(n^2)일 것이다. 각 행렬의 원소 갯수는 (n^2)/4개인데 10개므로 10을 곱하지만, Θ-표기법이라 계수가 생략되기 때문이다.
  3. 1단계에서 만든 부분행렬과 2단계에서 만든 10개의 S 행렬을 이용하여 n/2 x n/2 크기의 행렬을 7개 만든다. 각각 P_1, P_2, …, P_7이라고 하자.
  4. P 행렬들의 다양한 조합을 더하거나 빼서, 원래의 목표였던 행렬 C의 부분 행렬 C_11,C_12,C_21, C_22 를 계산한다. 이 계산은 Θ(n^2)시간이 걸린다.

이걸 토대로 점화식을 세워보자.

T(n) = 7T(n/2) + Θ(n^2)

마스터 정리에 의하면 위 식의 해는 T(n) = Θ(n^(log_2(7))) 이다. 기존의 Θ(n^3) 보다 빠르다. 이유를 세부적으로 살펴보자.

2단계

2단계에서 만든 행렬 S는 다음과 같이 쓸 수 있다.

S_1 = B_12 - B_22

S_2 = A_11 + A_12

S_3 = A_21 + A_22

S_4 = B_21 - B_11

S_5 = A_11 + A_22

S_6 = B_11 + B_22

S_7 = A_12 - A_22

S_8 = B_21 + B_22

S_9 = A_11 - A_21

S_10 = B_11 + B_12

각각은 n/2 x n/2 크기이므로 하나의 원소의 갯수는 (n^2)/4 이다. 10개니까 10을 곱하지만 Θ-표기법에 의해 계수는 생략되므로 Θ(n^2)이다.

3단계

1,2 단계에서 만들어진 행렬들의 재귀 곱으로 7개의 P 행렬을 만든다. 다음과 같이도 만든다.

P_1 = A_11 x S_1 = (A_11 x B_12 ) - (A_11 x B_22)

P_2 = B_22 x S_2 = (A_11 x B_22 ) + (A_12 x B_22)

P_3 = B_11 x S_3 = (A_21 x B_11 ) + (A_22 x B_11)

P_4 = A_22 x S_4 = (A_22 x B_21 ) - (A_22 x B_11)

P_5 = S_6 x S_5 = (A_11 x B_11 ) + (A_11 x B_22) + (A_22 x B_11) + (A_22 x B_22)

P_6 = S_8 x S_7 = (A_11 x B_12 ) + (A_11 x B_22) - (A_22 x B_21) - (A_22 x B_22)

P_7 = S_10 x S_9 = (A_11 x B_12 ) + (A_11 x B_22) - (A_21 x B_11) - (A_21 x B_12)

4단계

이렇게 만든 P 행렬을 더하거나 뺴서 조합하여 행렬 곱 C를 n/2 x n/2 크기의 부분행렬 4개를 만들 것이다.

C_11 = P_5 + P_4 - P_2 + P_6 ( 여기 식에서 P를 위의 값을 넣어 전개하여 정리한다.)

​ = (A_11 x B_11) + (A_12 x B_21)

C_12, C_21, C_22도 같은 논리를 적용할 수 있다.

각 행렬은 n/2 x n/2 크기이므로 행렬마다 원소 (n^2)/4 개를 가지니 이걸 8번 더하거나 빼므로 이 단계의 수행시간은 Θ(n^2)이다.

위 과정에서 우리는 8번의 재귀가 아닌 7번의 재귀를 호출하였다. 이 과정으로 우리가 알아낸 것은 스트라센 알고리즘이

T(n) = 7T(n/2) + Θ(n^2)

이라는 점화식으로 표현될 수 있다는 것이다. 여기서 어떻게 log_2(7) 값에 도달하게 되는지는 이후 장에서 나온다고 한다.


연습문제

4.2-1

스트라센 알고리즘을 이용해 다음 행렬 곱을 계산하는 과정을 보여라

(1 3) (6 8)

(7 5) (4 2)

나의 답

image1

img2

4.2-2

스트라센 알고리즘의 의사코드를 작성하라

나의 답

n = A.rows
C는 n x n 행렬이라 하자.
if n == 1
	c_11 = a_11 x b_11
else 행렬 A, B, C 를 각각 n/2 x n/2 크기의 부분행렬 4개로 나눈다.
        S1 = A11 + A22
        S2 = B11 + B22
        S3 = A21 + A22
        S4 = B12 - B22
        S5 = B21 - B11
        S6 = A11 + A12
        S7 = A21 - A11
        S8 = B11 + B12
        S9 = A12 - A22
        S10 = B21 + B22

        P1 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(S1, S2)
        P2 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(S3, B11)
        P3 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A11, S4)
        P4 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A22, S5)
        P5 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(S6, B22)
        P6 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(S7, S8)
        P7 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(S9, S10)

        C11 = P1 + P4 - P5 + P7
        C12 = P3 + P5
        C21 = P2 + P4
        C22 = P1 - P2 + P3 + P6

return C

4.2-6

스트라센 알고리즘을 서브 루틴으로 사용했을 때 kn x n 행렬과 n x kn 행렬의 곱셈을 얼마나 빨리 계산할 수 있는가? 입력 행렬의 순서를 바꾸었을 때 같은 질문에 답하라.

나의 답

행렬 A가 l x m 크기이고 행렬 B가 m x n 크기일 때, 일반적인 행렬 곱셈은 대략 2lmn번의 연산이 필요함 따라서 문제의 상황에서는 2(k^2) x n^3 번의 연산이 필요하다. 비정방행렬이지만 적용할수는 있되, k , n의 값에 따라 크게 좌우된다. 짝수화를 위해 얼마나 추가적인 과정이 필요한지, 2의 거듭제곱형태로 되기 위해 어느정도 추가 작업이 필요한지, 차원을 감소시켜 일반적인 행렬의 곱셈을 도입하는데 까지 얼마나 필요한지 등이 고려되야 한다. 하지만 입력의 순서를 바꾸었을 땐 연산의 횟수와 결과의 차원이 변하진 않으므로 기존 문제와 차이는 없다고 생각한다.

출처