모듈러 «나눗셈»

모듈러 «나눗셈»

올림피아드 문제에서는 어떤 큰 조합론적 값을 소수 모듈러(대개 $10^9 + 7$)로 계산하도록 요구하는 경우가 많습니다. 이렇게 하는 이유는 참가자들이 큰 수 연산을 사용하지 않고 문제 자체에 집중할 수 있도록 하기 위해서입니다.

일반적인 산술 연산은 모듈러에서도 그리 어렵지 않게 수행할 수 있습니다. 모듈러를 취하고 오버플로에만 주의하면 됩니다. 예를 들면 다음과 같습니다.

c = (a + b) % mod;
c = (a - b + mod) % mod;
c = a * b % mod;

하지만 나눗셈에서는 문제가 생깁니다. 그냥 나누기만 해서는 안 됩니다.

예를 들어 $\frac{8}{2} = 4$이지만

$$ \frac{8 \bmod 5}{2 \bmod 5} = \frac{3}{2} \neq 4 $$

$\frac{1}{a} = a^{-1}$처럼 동작하는 어떤 원소를 찾아서, «나눗셈» 대신 그 원소를 곱해야 합니다. 이러한 원소를 모듈로 $m$에 대한 역원이라고 합니다. 일반적인 나눗셈과 마찬가지로, $a = 0$에 대해서는 모듈로 역원이 정의되지 않습니다.

#이진 거듭제곱을 이용한 방법

페르마의 소정리에 따르면, 임의의 소수 $p$와 임의의 정수 $a$에 대하여 다음이 성립합니다.

$$ a^p \equiv a \pmod p $$ 이제 이 잘 알려진 결과를 $a$로 두 번 «나눕시다»: $$ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} $$

결과적으로 $a^{p-2}$는 모듈러 곱셈에 대해 $a^{-1}$처럼 동작하며, 이것이 바로 우리에게 필요한 것입니다.

$a^{p-2}$는 이진 거듭제곱으로 $O(\log p)$ 시간에 계산할 수 있습니다.

const int mod = 1e9 + 7;

// бинарное возведение в степень по модулю
int binpow(int a, int n) {
    int res = 1;
    while (n != 0) {
        if (n & 1)
            res = res * a % mod;
        a = a * a % mod;
        n >>= 1;
    }
    return res;
}

// находит обратный элемент как a^(p-2)
int inv(int x) {
    return binpow(x, mod - 2);
}

이 방법은 간단하고 빠르지만, 소수 모듈러에 대해서만 동작한다는 점을 기억해야 합니다.

합성수 모듈러의 경우, 오일러 정리에 따라 수 $a$를 $(\phi(m)-1)$ 제곱해야 하며, 이를 위해서는 소인수분해를 구해야 합니다.

#확장 유클리드 알고리즘을 이용하는 방법

확장 유클리드 알고리즘을 사용하면 다음과 같은 형태의 방정식을 정수 범위에서 풀 수 있습니다.

$$ Ax + By = 1 $$ $A$와 $B$ 자리에 각각 $a$와 $m$을 대입합시다: $$ ax + my = 1 $$ 방정식의 해 중 하나가 바로 $a^{-1}$입니다. 왜냐하면 방정식을 $m$으로 나눈 나머지를 취하면 다음을 얻기 때문입니다. $$ ax + my = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m $$

거듭제곱을 이용하는 방법에 비해 이 방법이 가지는 장점은 다음과 같습니다.

  • 역원이 존재한다면, 모듈러가 소수가 아니더라도 역원을 찾을 수 있습니다.
  • 이 알고리즘은 손으로 수행하기가 더 쉽습니다.
  • 최적화하면 알고리즘이 조금 더 빠릅니다

하지만 저자 개인적으로는 거의 항상 거듭제곱을 사용합니다.

#간소화된 구현

먼저 구현을 제시하고, 그다음에 이것이 왜 동작하는지 이해해 봅시다:

int inv(int a, int m) {
    if (a == 1)
        return 1;
    return (1 - 1ll * inv(m % a, a) * m) / a + m;
}

이 함수가 실제로 모듈러 역원을 반환한다는 것을 귀납법으로 증명합시다.

기저 사례는 자명합니다: $1 \cdot 1 \equiv 1$.

두 번째 경우에는 공식이 올바른지 확인해 봅시다:

  • $f(m \bmod a, a) \equiv m^{-1} \pmod a$이므로 $(1 - f(m \bmod a, a) \cdot m)$은 $a$로 나누어떨어집니다.
  • $\frac{f(m \bmod a, a) \cdot m}{a}$는 $m$으로 나누어떨어지므로, 최종 식은 $m$을 법으로 $\frac{1}{a} = a^{-1}$과 합동입니다.

답이 $0$부터 $(m - 1)$까지의 범위에 들어오는 이유는 독자의 연습 문제로 남겨 두겠습니다.

#모듈러 역원 전처리

대부분의 경우 모듈러 역원은 조합론의 맥락에서 구해야 합니다.

예를 들어, 특히 이항 계수를 계산해야 하는 경우가 많은데, 이를 위해서는 다시 팩토리얼의 역원을 구할 수 있어야 합니다:

$$ C_n^k = \frac{n!}{(n-k)! k!} $$

간단한 방법은 일반 팩토리얼을 미리 계산해 두고 매번 inv를 한두 번 호출하는 것입니다:

int t[maxn]; // факториалы, можно предподсчитать простым циклом

int c(int n, int k) {
    return t[n] * inv(t[k]) % mod * inv(t[n - k]) % mod;
}

// или, почти в два раза быстрее:
int c(int n, int k) {
    return t[n] * inv(t[k] * t[n - k] % mod) % mod;
}

그러나 이 방법은 어떤 조합 공식이 핫 루프 안에 놓이는, 드물지 않은 경우에 점근적 복잡도에 불필요한 로그를 하나 더 추가하게 됩니다. 따라서 자주 쓰이는 모듈러 역원도 미리 계산해 두는 것이 좋습니다.

#역팩토리얼

이미 inv를 작성해 두었다면, $(a!)^{-1}$을 계산하느라 $O(\log m)$번의 연산을 추가로 쓰는 것은 아깝지 않습니다.

그 후 $(a-1)!$의 역원은 다음 식으로 $O(1)$에 계산할 수 있습니다:

$$ (a-1)!^{-1} = (a!)^{-1} \cdot a \equiv \frac{1}{1 \cdot 2 \cdot \ldots \cdot (a-1)} \pmod p $$

나머지 모든 역팩토리얼도 같은 방식으로 이전 값으로부터 반복적으로 계산할 수 있습니다.

// обычные факториалы:
int f[maxn];

f[0] = 1;
for (int i = 1; i < maxn; i++)
    f[i] = i * f[i - 1] % mod;

// обратные факториалы:
int r[maxn];

r[maxn - 1] = inv(f[maxn - 1])
for (int i = maxn - 1; i >= 1; i--)
    r[i-1] = r[i] * i % mod;

또한 $1$부터 $(p - 1)$까지의 모든 수에 대한 역원을 구하는 방법도 있지만, 보통 모듈러가 크기 때문에 자주 쓰이지는 않습니다.

#왜 $10^9+7$입니까?

몇 가지 이유가 있습니다:

  1. 이 식은 입력하기가 꽤 쉽습니다(1e9+7).
  2. 소수입니다.
  3. 충분히 큰 수입니다.
  4. int는 덧셈할 때 오버플로가 발생하지 않습니다.
  5. long long은 곱셈 시 오버플로가 발생하지 않습니다.

참고로, $10^9 + 9$도 이와 똑같은 성질을 모두 가지고 있습니다. 때로는 이것을 사용하기도 합니다.

때로는 $998244353$도 볼 수 있습니다. 이 수는 첫 번째를 제외한 모든 성질을 갖추고 있으며, 그 대신 고속 푸리에 변환의 변형 중 하나에 활용됩니다. 참가자들에게 문제의 주제를 드러내지 않으려고, 이 수와 관련이 없는 문제에도 일부러 이 수를 쓰는 경우가 있습니다.