분할 정복을 이용한 거듭제곱 (Exponentiation by Squaring)
정의
분할 정복을 이용한 거듭제곱 (Exponentiation by Squaring) 은 a^n 을 O(log n) 시간에 계산하는 분할 정복 알고리즘. a^n mod m 같은 모듈러 연산에서 특히 필수적.
재귀 정의:
a^n = (a^(n/2))^2 if n is even
a^n = a · (a^(n/2))^2 if n is odd
a^0 = 1
이진수 표현으로 보면, n의 각 비트마다 제곱을 반복하며 1인 비트만 누적 곱하는 방식.
문제 상황과 동기
a^n mod m 을 구해야 한다. n ≤ 10^18.
- naive: a를 n번 곱한다. O(n). n=10^18 이면 절대 불가능.
- exponentiation by squaring: O(log n). 60회 이하의 곱셈으로 완료.
핵심 통찰: n을 절반으로 나누면 재귀 깊이가 log n. 제곱 한 번으로 지수가 절반이 되므로, 곱셈 횟수는 n의 비트 수에 비례.
RSA, Diffie-Hellman 같은 암호학적 연산 (modular exponentiation), 행렬 거듭제곱 (피보나치 O(log N)), 페르마 소정리 (모듈러 역원) 등 수학/정수론 PS에서 필수.
시각화
핵심 아이디어
invariant: 현재까지 본 비트들로 만든 부분 지수의 거듭제곱 = 누적 곱.
n을 이진수로 보면:
n = b_k 2^k + b_{k-1} 2^{k-1} + ... + b_1 2 + b_0
a^n = a^(b_k 2^k) · a^(b_{k-1} 2^{k-1}) · ... · a^(b_0)
각 a^(2^i) 는 이전 값의 제곱이므로, O(log n) 번의 제곱 + 1인 비트마다 곱셈.
재귀 버전:
exp(a, n):
if n = 0: return 1
half = exp(a, n // 2)
if n % 2 = 0: return half * half
else: return a * half * half
반복 버전 (비트마스킹):
result = 1, base = a
while n > 0:
if n & 1: result *= base
base *= base
n >>= 1
알고리즘
재귀 (분할 정복)
exp_mod(a, n, m):
if n = 0:
return 1
half = exp_mod(a, n / 2, m)
half = (half * half) % m
if n is odd:
half = (half * a) % m
return half
반복 (비트마스킹)
exp_mod_iter(a, n, m):
result = 1
base = a % m
while n > 0:
if n & 1:
result = (result * base) % m
base = (base * base) % m
n >>= 1
return result
구현
// 재귀 + 반복 두 가지 구현
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 재귀
ll exp_mod_rec(ll a, ll n, ll m) {
if (n == 0) return 1;
ll half = exp_mod_rec(a, n / 2, m);
half = (half * half) % m;
if (n % 2 == 1) half = (half * a) % m;
return half;
}
// 반복
ll exp_mod_iter(ll a, ll n, ll m) {
ll result = 1, base = a % m;
while (n > 0) {
if (n & 1) result = (result * base) % m;
base = (base * base) % m;
n >>= 1;
}
return result;
}
int main() {
ll a, n, m; cin >> a >> n >> m;
cout << exp_mod_rec(a, n, m) << "\n";
cout << exp_mod_iter(a, n, m) << "\n";
}2 10 10000000071024
1024복잡도
| 항목 | 값 |
|---|---|
| 시간 (최선, 평균, 최악) | O(log n) |
| 공간 (재귀) | O(log n) 스택 |
| 공간 (반복) | O(1) |
| 곱셈 횟수 | ≤ 2 log₂ n |
n의 비트 개수만큼만 곱셈. n ≤ 10^18 일 때 ≤ 120회.
변형 / 활용
1. 행렬 거듭제곱
M^n 을 O(log n) 에 계산. 피보나치, 선형 점화식 등 O(log N) 풀이.
typedef vector<vector<ll>> matrix;
matrix mul(matrix A, matrix B, ll m) {
int n = A.size();
matrix C(n, vector<ll>(n));
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % m;
return C;
}
matrix mat_exp(matrix M, ll n, ll m) {
int sz = M.size();
matrix result(sz, vector<ll>(sz));
for (int i = 0; i < sz; i++) result[i][i] = 1;
while (n > 0) {
if (n & 1) result = mul(result, M, m);
M = mul(M, M, m);
n >>= 1;
}
return result;
}
2. 모듈러 역원 (페르마 소정리)
m이 소수일 때, a^(-1) ≡ a^(m-2) (mod m).
ll inv(ll a, ll m) {
return exp_mod(a, m - 2, m);
}
3. 오일러 정리
a^φ(m) ≡ 1 (mod m) (gcd(a, m)=1). a^n mod m = a^(n mod φ(m)) mod m.
함정
1. 오버플로우
(half * half) % m 에서 half가 m-1 이면 곱이 (m-1)^2, long long 범위 초과 가능.
해법: (__int128) 또는 modular multiplication 함수.
ll mul_mod(ll a, ll b, ll m) {
return (__int128)a * b % m;
}
2. n=0 처리
a^0 = 1 은 a=0 일 때도 1. edge case 확인.
3. 음수 밑 (a < 0)
모듈러 연산에서 음수는 a = ((a % m) + m) % m 로 정규화.
4. m=1
a^n mod 1 = 0 항상. 예외 처리 안 하면 나눗셈 by zero.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 1629 | 곱셈 | 31.2% | kokoa-lab |
| BOJ 11444 | 피보나치 수 6 | 27.8% | kokoa-lab |
| BOJ 13172 | Σ | 38.5% | kokoa-lab |
| BOJ 10830 | 행렬 제곱 | 32.7% | kokoa-lab |
참고
이 글의 용어 (3개)
- 수학 (Mathematics)algorithm
- 정의 수학 (Mathematics) 은 문제 해결에 필요한 모듈러 산술 (modular arithmetic), 최대공약수/최소공배수 (GCD/LCM), 거듭제곱 (exponent…
- 페르마 소정리 (Fermat's Little Theorem)algorithm
- 정의 페르마 소정리 (Fermat's Little Theorem, FLT) 는 소수 p 와 gcd(a, p) = 1 인 정수 a 에 대해 다음이 성립: 1640년 피에르 드 페르…
- Matrix Exponentiation: 선형 점화식 fastalgorithm
- 정의 선형 점화식을 행렬 곱으로 표현한 뒤, 행렬 거듭제곱을 log 시간에 계산하여 N 번째 항을 O(K³ log N) 에 얻는 기법. K = 상태 차원. Fibonacci 예 …
💬 댓글