Modular Inverse

모듈러 역원

YEAHx4

YEAHx4

2026-02-26
13 mins

Modular Inverse

모듈러 역원은 모듈로 연산에서 곱셈의 역원을 의미합니다. 덧셈, 곱셈에서의 모듈러 연산은 다음과 같이 단순하게 처리할 수 있습니다.

\[ (a + b) \,\%\, m = ((a \,\%\, m) + (b \,\%\, m)) \,\%\, m \]

\[ (a \times b) \,\%\, m = ((a \,\%\, m) \times (b \,\%\, m)) \,\%\, m \]

나머지 연산을 %로 표현하는 것은 수학적으로 엄일한 표현은 아니지만, 편의를 위해 사용하겠습니다.

여기서 하고 싶은 것은 나눗셈에서 모듈러 연산을 처리하는 것입니다. 나눗셈은 위의 방식처럼 간단히 처리할 수 없습니다. 그래서 모듈러 역원을 사용합니다.

모듈러 연산의 항등원

역원을 정의하기 위해서는 항등원을 먼저 정의해야 합니다. 덧셈에서의 항등원은 0이고 곱셈에서의 항등원은 1입니다. 모듈러 연산은 덧셈과 곱셈의 항등원이 다릅니다. 항등원은 어떤 수 \( a \)가 있을 때 연산 결과를 그대로 \( a \)가 나오게 하는 수입니다. \( a \,\%\, b = c \)\( a \)에 대해 모듈러 합의 항등원은 다음 식을 만족합니다.

\[ (a + e) \,\%\, b = c \]

이를 항상 만족하는 수 \( e \)는 0입니다. 따라서 모듈러 합에서의 항등원은 0입니다. 마찬가지로 모듈러 곱에서는 다음 식을 만족합니다.

\[ (a \times e) \,\%\, b = c \]

이를 항상 만족하는 수 \( e \)는 1입니다. 따라서 모듈러 곱에서의 항등원은 1입니다. 어떤 수의 역원은 그 수와 연산했을 때 항등원이 되는 수입니다. 모듈러 합에서는 연산 결과가 0이 되게 하는 수가 역원입니다. 즉, \( -a \)\( a \)의 모듈러 합의 역원입니다. 모듈러 곱에서는 다음과 같이 표현할 수 있습니다.

\[ ax \equiv 1 \mod N \]

이때의 \( x \)\( a \)의 모듈러 곱의 역원 \( a^{-1} \)입니다. 이 \( x \)를 찾을 수 있다면 나눗셈을 역원의 곱으로 표현할 수 있게 됩니다. 위 식을 살짝 변형하면 \( ax = Ny + 1 \)으로 표현할 수 있습니다. 이 식을 정리하면,

\[ ax + Ny = 1 \]

으로 표현할 수 있습니다. \( y \)는 임의의 상수이기 때문에 편의상 부호를 바꾸어 썼습니다. 이 식은 베주 항등식(Bézout's identity)과 동일한 형태입니다.

Bézout's identity

두 정수 \( a, b \)가 있을 때

\[ ax + by = \gcd(a, b) \]

를 만족하는 정수 \( x, y \)가 항상 존재한다는 것이 베주 항등식입니다. 위 식의 형태와 비슷합니다. 위 식에서 우변이 1이기 때문에 \( \gcd(a, N) = 1 \)이 되어야 합니다. 즉, \( a \)\( N \)이 서로소여야 모듈러 역원이 존재합니다. PS에서 \( N \)이 보통 1,000,000,007로 주어집니다. 이 수가 자주 사용되는 것은 이 수가 소수이기 때문입니다. 즉, 모든 \( a \)에 대해 \( \gcd(a, N) = 1 \)이 됩니다. 이 외에도 998,244,353 같은 소수도 자주 사용됩니다.

이제 이 식을 만족하는 정수해 \( (x, y) \)를 찾을 수 있다면 \( x \)\( a \)의 모듈러 곱의 역원이 됩니다. 이 식의 해를 구하는 방법은 확장 유클리드 알고리즘이 있습니다.

Extended Euclidean Algorithm

확장 유클리드 알고리즘은 유클리드 알고리즘을 확장하여 베주 항등식의 해를 구하는 알고리즘입니다. gcd를 구하면서 동시에 \( x, y \)도 구할 수 있습니다. 두 정수 \( a, b \)에 대해 \( a = qb + r \)로 표현할 수 있습니다. 이때 유클리드 호제법에 의해 다음이 성립합니다.

\[ \gcd(a, b) = \gcd(b, r) \]

즉, \( \gcd = ax + by \)\( \gcd = bx' + ry' \)으로 변형할 수 있습니다. 이때 \( r = a - qb \) 이므로 이를 대입하면

\[ \gcd = bx' + (a - qb)y' = ay' + b(x' - qy') \]

가 됩니다. 따라서 원래 문제 \( (a, b) \)에 대한 베주 계수는

\[ x = y', \qquad y = x' - qy' \]

임을 알 수 있습니다. 이 과정을 재귀적으로 반복해 \( x, y \)를 구할 수 있게 됩니다. 유클리드 호제법의 종료 조건은 \( r = 0 \)이 되는 경우입니다. 식으로 표현하면 \( \gcd(a, 0) = a \)입니다. 이를 베주 항등식의 형태로 표현하면 \( ax + 0y = a \)이므로 이 경우의 계수는 \( x = 1 \)이고 \( y = 0 \)이 됩니다. 이후 재귀 호출이 반환되면서 위의 갱신식에 따라 각 단계의 \( x, y \)가 결정됩니다.

예를 들어, \( a = 30, b = 18 \)인 경우를 생각해봅시다.

\[ \begin{align*} 30 &= 18 \times 1 + 12 \\ 18 &= 12 \times 1 + 6 \\ 12 &= 6 \times 2 + 0 \end{align*} \]

이 과정을 거꾸로 올라가면서 \( x, y \)를 구합니다. \( (12, 6) \to (18, 12) \to (30, 18) \)의 순서로 올라갑니다. 먼저 종료 조건에서

\[ 6 = 12\cdot 0 + 6\cdot 1 \]

이므로 \( x = 0, y = 1 \)입니다. 다음 단계인 \( (18, 12) \)에서는 \( 6 = 18 \,\%\, 12 = 18 - 1\cdot 12 \)이므로 이를 대입하면

\[ 6 = 12\cdot 0 + (18 - 12)\cdot 1 = 18\cdot 1 + 12\cdot (-1) \]

이 되어 \( x = 1 \), \( y = -1 \)입니다. 마지막으로 \( (30, 18) \) 단계에서는 \( 12 = 30 \,\%\, 18 = 30 - 1\cdot 18 \)이므로 이를 대입하면

\[ 6 = 18\cdot 1 + (30 - 18)\cdot (-1) = 30\cdot (-1) + 18\cdot 2 \]

가 됩니다. 따라서 \( x = -1, y = 2 \)이며,

\[ 30\cdot (-1) + 18\cdot 2 = 6 \]

이 성립합니다. 따라서 \( (-1, 2) \)\( 30, 18 \)의 베주 항등식의 해입니다. 확장 유클리드 알고리즘을 코드로 구현하면 다음과 같습니다.

pair<int, pair<int, int>> eea(int a, int b) {
    if (b == 0) return { a, {1, 0} };
    
    auto [g, xy] = eea(b, a % b);
    auto [x, y] = xy;
    return { g, { y, x - (a / b) * y  }};
}

BOJ 14565번: 역원(Inverse) 구하기를 풀어 보겠습니다. 이 문제는 \( N \)에 대한 \( A \)의 모듈러 합과 곱의 역원을 구하는 문제입니다. 모듈러 합의 역원 \( -A \)입니다. 다만, 요구하는 답이 0부터 \( n - 1 \) 사이의 정수이므로 \( N - A \)가 됩니다. 모듈러 곱의 역원은 위의 mod_inv 함수에 통과시키면 됩니다.

#include <bits/stdc++.h>

using namespace std;
void fio() { cin.tie(nullptr); cout.tie(nullptr); ios::sync_with_stdio(false); }

typedef long long ll;

pair<ll, pair<ll, ll>> eea(ll a, ll b) {
    if (b == 0) return { a, {1, 0} };

    auto [g, xy] = eea(b, a % b);
    auto [x, y] = xy;
    return { g, { y, x - (a / b) * y }};
}

ll mod_inv(ll a, ll mod) {
    auto res = eea(a, mod);
    if (res.first != 1) return -1;
    return (res.second.first % mod + mod) % mod;
}

int main() {
    fio();

    ll n, a; cin >> n >> a;
    cout << n - a << ' ' << mod_inv(a, n);
}