큰 자연수 A, B, M에 대해 AB(modM)의 값을 필요로 하는 문제가 많습니다.
이 카테고리에서는 이런 문제를 몇 가지 풀어보려고 합니다.
이 글에서는 문제를 풀기 이전에 주요 개념을 간략하게 설명해보도록 하겠습니다.
1. Exponentiation By Squaring
xy를 분할 정복을 이용하여 빠르게 구하는 기법입니다.
이때 y의 이진수 꼴을 쓰는 경우 Binary Exponentiation이라고도 합니다.
실제 코드 전에 간단히 예시를 보겠습니다.
(10^13) = (10^8) * (10^4) * (10^1)
13 = 1101
8 = 1000
4 = 0100
1 = 0001
구조가 보이나요?
xy에서 y를 binary로 나타낸 것을 y=dkdk−1⋯d1d0 라고 할 때,
xy=k∏i=0x(2i×di)=∏di=1x2i
입니다.
이때 x2i 간에는 다음 점화식이 성립합니다.
x2i+1=(x2i)2
따라서 xy를 다음과 같이 구현할 수 있습니다.
long long power_modular (long long x, long long y) {
// return x ^ y
long long result = 1; // 1 = x ^ 0
while (y > 0) {
if (y & 1) {
result *= x;
}
x *= x;
y >>= 1;
}
return result;
}
long long 타입의 변수 y는 이미 메모리 상에 이진수의 형태로 저장되어 있으므로, 비트연산자 >>와 &, 또는 산술연산자 %와 /를 이용해도 원하는 결과를 얻을 수 있습니다. 문자열로 저장된 p진수 y의 경우 각 자릿수마다 x를 p제곱해주어야 하는 등 추가 공정이 필요합니다.
이렇게 구현한 함수 power_modular은 y가 p진법으로 저장되었을 때 O(logpy)=O(logy)의 복잡도를 갖게 될 것입니다.
2. 곱셈 과정에서의 오버플로우 방지
1에서 구현한 코드는 오버플로우가 일어나기 매우 쉽습니다.
따라서 많은 문제에서는 xy를 적당한 수 M으로 나눈 나머지를 출력하라고 합니다.
합동식에서 성립하는 다음 성질을 이용하면 이를 쉽게 접근할 수 있습니다.
a×b=(amodM)×(bmodM)(modM)
이를 이용하여 1의 코드를 조금 수정해보겠습니다.
long long power_modular (long long x, long long y, long long M) {
// return x ^ y mod M
long long result = 1; // 1 = x ^ 0
while (y > 0) {
if (y & 1) {
result = result * x % M;
}
x = x * x % M;
y >>= 1;
}
return result;
}
그러나 이 역시 오버플로우가 일어나기 쉽습니다.
그 이유는 6, 8번째 줄의 곱셈 연산 때문입니다.
따라서 M≥√263−1 일 경우, 오버플로우가 일어나지 않도록 두 수를 곱해주는 함수가 필요합니다.
다음 코드는 Exponentiation By Squaring과 같은 원리로 작동합니다. (연산자 우선순위에 의해 괄호가 생겼음에 주의해주세요.)
long long multiply_modular (long long x, long long y, long long M) {
// x * y (mod M)
long long result = 0;
while (y > 0) {
if (y & 1) {
result = (result + x) % M;
}
x = (x + x) % M;
y >>= 1;
}
return result;
}
3. 오버플로우를 피하는 power_modular 함수
이제 multiply_modular을 이용하여, power_modular함수를 수정해보겠습니다.
2번의 multiply_modular을 logy 번 호출하게 되므로, 복잡도는 O(log2y) 가 됩니다.
long long multiply_modular (long long x, long long y, long long M) { // x * y (mod M)
long long result = 0;
while (y > 0) {
if (y & 1) {
result = (result + x) % M;
}
x = (x + x) % M;
y >>= 1;
}
return result;
}
long long power_modular (long long x, long long y, long long M) { // x ^ y (mod M)
long long result = 1;
while (y > 0) {
if (y & 1) {
result = multiply_modular (result, x, M);
}
x = multiply_modular (x, x, M);
y >>= 1;
}
return result;
}
4. 덧셈 과정에서의 오버플로우 방지
위 코드는 많은 상황에서 잘 작동하지만, M이 262 이상으로 커지는 순간 오버플로우가 발생할 수 있습니다.
그 이유는 long long이 최대 263−1 까지 지원하기 때문입니다.
그로 인해 multiply_modular 함수 내에서 result와 x가 262 이상으로 커지게 되더라도 (modulo 연산 시행 전에)
오버플로우를 막을 수 있는 새로운 함수가 필요하게 됩니다.
이는 다음과 같이 구현할 수 있습니다.
long long add_modular (long long x, long long y, long long M) { // x + y (mod M)
if (x >= M - y || y >= M - x) {
return (x - M) + y;
}
else {
return x + y;
}
}
if문의 조건은 결국 x+y≥M 와 동치입니다.
이 경우, 미리 M을 빼주고 다시 y를 더합니다. 합동식의 성질에 따라 이는 나머지에 영향을 끼치지 않습니다.
그러나 이 코드가 verbose하게 느껴진다면, 다음을 쓸 수 있습니다. 두 방식 모두 O(1)이므로 시간 복잡도에 영향은 없습니다.
long long add_modular (long long x, long long y, long long M) {
// x + y (mod M)
return (x - M) + y;
}
따라서 최종적으로, 다음과 같이 power_modular함수를 구현하였습니다.
long long add_modular (long long x, long long y, long long M) {
// x + y (mod M)
return (x - M) + y;
}
long long multiply_modular (long long x, long long y, long long M) {
// x * y (mod M)
// assume x , y < M
long long result = 0;
while (y > 0) {
if (y & 1) {
result = add_modular (result, x, M);
}
x = add_modular (x, x, M);
y >>= 1;
}
return result;
}
long long power_modular (long long x, long long y, long long M) {
// x ^ y (mod M)
// assume x < M
long long result = 1;
while (y > 0) {
if (y & 1) {
result = multiply_modular (result, x, M);
}
x = multiply_modular (x, x, M);
y >>= 1;
}
return result;
}
여기서 소개한 함수 중에, add_modular, multiply_modular 함수는 M의 범위에 따라선 굳이 구현하지 않아도 충분합니다.
마지막으로, __int128 을 지원하는 환경에서는 위 제한을 좀 더 피해갈 수 있을 것입니다.
예를 들어 다음과 같이 쓸 수 있습니다.
long long power_modular (long long x, long long y, long long M) {
// x ^ y (mod M)
long long result = 1;
while (y > 0) {
if (y & 1) {
result = (__int128)result * x % M;
}
x = (__int128)x * x % M;
y >>= 1;
}
return result;
}
이런 casting이 싫은 경우, x와 M을 미리 __int128 로 설정하는 방법도 있습니다.
'이론' 카테고리의 다른 글
PS에서 쓰는 Matrix multiplication의 구현과 최적화 (0) | 2023.01.30 |
---|---|
기본 이론 3 (0) | 2021.07.31 |
기본 이론2 (0) | 2021.01.23 |