기본 단계
카라추바 알고리즘의 기본 단계는 큰 두 수 x와 y의 곱을 자릿수가 x, y의 절반인 수들의 곱 3번과 덧셈과 시프트 연산을 이용해 계산하는 것이다.
x와 y를 B진법의 n자리수라고 하자. n보다 작은 양수 m에 대해 다음과 같이 x, y를 쪼갤 수 있다.
- x = x1Bm + x0
- y = y1Bm + y0
(단, x0과 y0는 Bm보다 작다.)
- z2 = x1y1
- z1 = x1y0 + x0y1
- z0 = x0y0
라고 할 때, x와 y의 곱은
- xy = (x1Bm + x0)(y1Bm + y0)
- = z2 B2m + z1 Bm + z0
이 식은 4번의 곱셈을 해야한다. 카라추바는 덧셈을 몇 번 함으로써, xy를 3번의 곱셈을 통해 구할 수 있다는 걸 알았다.
- z2 = x1y1 라 하자.
- z0 = x0y0 라 하자.
- z1 = (x1y1 + x1y0 + x0y1 + x0y0) - x1y1 - x0y0 = x1y0 + x0y1
이므로
- z1 = (x1 + x0)(y1 + y0) − z2 − z0 라 할 수 있다.
예
B = 10 이라하고, m = 2 라 하자. 1234와 5678의 곱을 구하고 싶으면,
- 12 34 = 12 × 102 + 34
- 56 78 = 56 × 102 + 78
- z2 = 12 × 56 = 672
- z0 = 34 × 78 = 2652
- z1 = (12 + 34)(56 + 78) − z2 − z0 = 46 × 134 − 672 − 2652 = 2840
- 결과 = z2 × 102×2 + z1 × 102 + z0 = 672 × 10000 + 2840 × 100 + 2652 = 7006652
우선, 우리 개발자의 영원한 친구 위키백과에서 확인해봅시다!
위의 식을 어떻게 코드로 구현하면 될까요?
역시 분할 정복인 만큼 '재귀 함수'로 구현하는 것이 편합니다.
카라츠바 알고리즘에 의하면, 큰 수 A가 m자리 수라고 하고 하면, 다음과 같이 표현될 수 있습니다.
즉, 카라츠바 알고리즘에서 가장 중요한 것은 자리수를 이등분하여 x1, x2, y1, y2를 정해서
z0, z1, z2를 다시 카라츠바 알고리즘을 이용해서 구하는 것입니다!
이를 위해서는 각 자리수마다 덧셈과 뺄셈을 만들어 주는 것이 필요합니다.
참고로, add() 함수의 뒤에 int k 를 넣은 이유는 z2, z1과 같은 경우는 half 만큼 자리수의 위치를 스위칭해서 더해줘야 하기 때문입니다.
또한, 기저 사례를 위해서
의 시간복잡도를 가지는 보통의 곱셈 방식을 구현합니다.
아래는 전체적인 코드입니다!
참고로 각 자리의 올림은 하지 않은 상태입니다.
이는 마지막 결과에서 벡터마다 10으로 나눈 값을 다음 자리수에 더해주면서 올림을 시행하면 됩니다.
반대로 자리수에 남아있는 값은 벡터마다 %10 으로 모듈라 연산을 해주면 됩니다.
참고로 이 코드에선 보이지 않지만, main문에서 reverse를 수행하고 카라츠바를 수행했습니다.
(리버스를 해 놓고 실행해야 0000 과 같이 앞에 0이 오는 경우를 pop_back으로 처리하기 쉽기 때문입니다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
typedef vector<int> VEC;
VEC multi(VEC& A, VEC& B)
{
int s1 = A.size();
int s2 = B.size();
VEC res;
res.resize(s1 + s2 - 1, 0);
for (int i = 0; i < s1; i++)
{
for (int j = 0; j < s2; j++)
{
res[i + j] += A[i] * B[j];
}
}
return res;
}
void add(vector<int>& vec1, vector<int>& vec2, int k)
{
int s = max(vec2.size() + k, vec1.size());
vec1.resize(s, 0);
vec2.resize(s, 0);
for (int i = 0; i < s - k; i++)
{
vec1[i + k] += vec2[i];
}
}
void sub(vector<int>& vec1, vector<int>& vec2)
{
int s = max(vec1.size(),vec2.size());
vec1.resize(s, 0);
vec2.resize(s, 0);
for (int i = 0; i < s; i++) vec1[i] -= vec2[i];
}
VEC Karatsuba(VEC& A, VEC& B)
{
if (A.size() < 50)
{
VEC res;
res = multi(A,B);
return res;
}
int half = A.size() / 2;
if (A.empty() || B.empty()) return VEC();
VEC a1 = VEC(A.begin(), A.begin() + half);
VEC a0 = VEC(A.begin() + half, A.end());
VEC b1 = VEC(B.begin(), B.begin() + min(half, B.size()));
VEC b0 = VEC(B.begin() + min(half, B.size()), B.end());
VEC z0, z1, z2;
z0 = Karatsuba(a0, b0);
z2 = Karatsuba(a1, b1);
add(a0, a1, 0);
add(b0, b1, 0);
z1 = Karatsuba(a0, b0);
sub(z1, z0);
sub(z1, z2);
VEC res;
add(res, z2, 0);
add(res, z1, half);
add(res, z0, half + half);
return res;
}
|
cs |
그럼 카라츠바 알고리즘의 시간복잡도를 계산해봅시다.
기저 사례가 자리수 50으로 되어있지만, 자리수가 1이 되어야 한다고 가정하겠습니다.
각 자리수를 2 등분 하므로, 둘 중 자리수가 큰 값을 n 이라고 한다면, 단계는 총
입니다.
그리고, 단계마다 3개로 나누어지기 때문에, 시간 복잡도는
이 됩니다.
여기서, 예전에 중학교에서 배웠던 로그 지식을 약간 활용해서
을 얻을 수 있습니다! (프로그래밍의 새계에선 log밑이 2인 것이 default!)
따라서 시간복잡도는
입니다.
이는
보다 빠른 알고리즘임을 알 수 있습니다.
'공부 > 알고리즘' 카테고리의 다른 글
[알고리즘] 동적 계획법(dynamic programming) - 2 (0) | 2018.06.14 |
---|---|
[알고리즘] 동적 계획법(dynamic programming) - 1 (0) | 2018.06.12 |
[분할 정복] 분할 정복에 대해서 알아보자 (0) | 2018.06.11 |
[정렬] 병합 정렬 ( Merge Sort) (0) | 2018.06.08 |
[완전탐색/부분집합] 비트를 이용하여 모든 부분집합을 구하자! (0) | 2018.06.08 |