Post

거듭제곱, 모듈러 연산, 페르마의 소정리(백준 13977)

백준 13977을 해결하기 위해서는 3가지 테크닉을 알아야한다.

  1. 분할정복을 이용한 거듭제곱
  2. 모듈러 연산 특징(for 이항계수 모듈러 연산을 위한 역원)
  3. 페르마의 소정리(for 이항계수 모듈러 연산)

거듭제곱

1. 일반적인 거듭제곱 : $O(n)$

일반적인 거듭제곱의 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
// 재귀함수를 이용한 거듭제곱
int pow(int a, int n) {
  if(n==0) return 1;
  else return a*pow(a, n-1);
}

// 반복문을 이용한 거듭제곱
int pow(int a, int n) {
  int result = 1;
  for(int i=1; i<=n; i++) result*=a;
  return result;
}

위의 코드는 모두 시간복잡도 $O(n)$이다.

2. 분할정복을 이용한 거듭제곱 : $O(\log n)$

이를 분할정복을 이용하면 시간복잡도 $O(\log n)$으로 계산할 수 있다.

거듭제곱은 2가지 케이스로 나눌 수 있다.

  1. $n$이 짝수일 때 : $a^n = a^{\frac{n}{2}} \times a^{\frac{n}{2}}$
  2. $n$이 홀수일 때 : $a^n = a^{\frac{n-1}{2}} \times a^{\frac{n-1}{2}} \times a$

예시를 보자.

  1. $2^8 = 2^4 \times 2^4$
  2. $2^4 = 2^2 \times 2^2$
  3. $2^{15} = 2^7 \times 2^7 \times 2$
  4. $2^3 = 2^1 \times 2^1 \times 2$
1
2
3
4
5
6
7
8
9
// 분할정복을 이용한 거듭제곱
int pow(int a, int n) {
  if(n == 0) return 1;
  int temp = pow(a, n/2);
  int k = temp * temp;

  if(n%2==0) return k;
  else return k*a;
}


모듈러 연산

음수의 모듈러 연산

정수 mod n의 결과는 항상 $0 \verb|~| (n-1)$의 범위를 가지는 정수이다.
음수 $a$를 모듈러 연산은 $a \bmod n = (a+kn) \bmod n$ ($k$는 자연수)을 해야한다.

예시를 보자.

  1. $5 \bmod 11 = 5$
  2. $23 \bmod 11 = 1$
  3. $-5 \bmod 11 = (-5+1 \times 11) \bmod 11 = (-5+2 \times 11) \bmod 11 = \cdots = 6$
  4. $-23 \bmod 11 = (-23+3 \times 11) \bmod 11 = 10$

모듈러 합동

$a \bmod n = b \bmod n$이면 $a$와 $b$는 모듈러 합동 관계이고,
$a \equiv b \pmod{n}$와 같이 표기한다.

모듈러 연산의 속성

$a, b$가 양수일 때,

  1. $(a+b) \bmod n = \lbrace(a \bmod n)+(b \bmod n)\rbrace \bmod n$
  2. $(a-b) \bmod n = \lbrace(a \bmod n)-(b \bmod n)\rbrace \bmod n$
  3. $(a \times b) \bmod n = \lbrace(a \bmod n) \times (b \bmod n)\rbrace \bmod n$

특히 2번에서 $(a \bmod n)-(b \bmod n)$가 음수인 경우를 조심해야 한다.
따라서 2번은 $\lbrace(a \bmod n)-(b \bmod n) + n\rbrace \bmod n$으로 계산하는 것이 안전하다.

3번의 경우 곱해진 두 수를 자유롭게 모듈러 연산할 수 있다는 것이 특징이다.

\((a \times b) \bmod n = \lbrace(a \bmod n) \times b\rbrace \bmod n\) \(= \lbrace a \times (b \bmod n)\rbrace \bmod n = \lbrace(a \bmod n) \times (b \bmod n)\rbrace \bmod n\)

모듈러 곱셈의 역원

$a$의 역원을 $t$라고 할 때, $(a \times t) \bmod = 1$이다.

예를 들어, $n=11$일 때, $3$의 역원을 구하려면, $(3 \times t) \bmod 11 = 1$을 만족하는 $t$를 구하면 된다.
이때 $t$는 하나로 정해지지 않을 수 있다.($t=4, 15, 26, 37, \cdots$)

모듈러 나눗셈

$b$의 역원을 $t$라고 할 때, $\left(\cfrac{a}{b} \bmod n\right) = (a \times t) \bmod n$이다.(증명 생략)

예시

  1. $n=11$일 때, $3$의 역원은 $4$이므로, $\left(\cfrac{5}{3} \bmod 11\right) = (5 \times 4) \bmod 11 = 9$
  2. $14$의 역원을 $t$라 할 때, $(14 \times t) \bmod 11 = 1$을 만족한다.
    이때 모듈러 연산의 특징으로 인해 $(3 \times t) \bmod 11 = 1$을 찾아도 된다.
    따라서 $t$는 $3$의 역원이기도 하므로 $t=4, 15, \cdots$
    $\cfrac{13}{14} \bmod 11 = (13 \times 4) \bmod 11 = 8$

이항계수 모듈러 연산

\[_{n}\mathrm{C}_{r} \bmod p\]

파스칼의 삼각형 이용 - $O(n^2)$

이항계수의 성질인 $ \binom{n}{r} =\binom{n-1}{r-1}+\binom{n-1}{r}$을 이용해 DP Table을 만든다.

1
2
3
4
5
6
7
8
9
10
11
// bottom up 방식의 DP
int n, r;
cin >> n >> r;
for(int i=1; i<=1000; i++) {
  for(int j=0; j<=i; j++) {
    if(j==0 || j==i) {
      c[i][j] = (c[i-1][j-1]+c[i-1][j]) % MOD;
    }
  }
}
cout << c[n][r];

페르마의 소정리 이용 - $O(n \log p)$

페르마의 소정리는 다음과 같다.
“소수 $p$와 정수 $a$에 대해서 $a$와 $p$가 서로소이면 $a^{p-1} \bmod p = 1$을 만족한다.”

\[_{n}\mathrm{C}_{r} \bmod p = \frac{n!}{r!(n-r)!}\bmod p\]

분수꼴의 모듈러 연산은 역원을 구하는 과정이 필요하다. 따라서 $r!(n-r)!$의 역원을 찾아야 한다.

소수인 $p$는 $r!(n-r)!$와 서로소이다. (단, $p \neq r!(n-r)!$)
페르마의 소정리에 의해 $\lbrace r!(n-r)!\rbrace^{p-1} \bmod p = 1$이고,
변형하면 $\lbrace r!(n-r)!\rbrace^{p-2} \times \lbrace r!(n-r)!\rbrace \bmod p = 1$이다.

따라서 $r!(n-r)!$의 역원은 $\lbrace r!(n-r)!\rbrace ^{p-2}$이므로, 아래와 같은 식이 성립된다.

\[_{n}\mathrm{C}_{r} \bmod p = \frac{n!}{r!(n-r)!}\bmod p = n! \times \lbrace r!(n-r)!\rbrace^{p-2} \bmod p\]

문제 예시

https://www.acmicpc.net/problem/13977

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
#include<iostream>
#define ll long long
const int MOD = (int)1e9 + 7;

using namespace std;

ll fac[4000001], inverse[4000001];

ll factorial(ll a, ll n){
    if(n==0) return 1ll;
    ll temp = factorial(a, n/2);
    ll k = temp*temp%MOD;
    
    if(n%2==0) return k;
    else return k*a%MOD;
}

int main() {
    ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
    ll n, r, m;
    cin>>m;
    
    // 팩토리얼 계산
    fac[0]=1;
    for(int i=1; i<=4000000; i++){
        fac[i]=(fac[i-1] * i)%MOD;
    }
    
    // 역원 계산
    inverse[4000000] = factorial(fac[4000000], MOD - 2);
    for (int i = 4000000 - 1; i >= 0; i--){
      inverse[i] = inverse[i + 1] * (i + 1) % MOD;
    }
    
    while(m--) {
        cin>>n>>r;
        cout<<((fac[n] * inverse[r] % MOD) * inverse[n-r]%MOD)%MOD<<"\n";
    }
}

전역변수

  • frc[n] = $n! \bmod p$
  • inverse[n] = $(n!)^{p-2} \bmod p = $($n!$ 의 역원) $\bmod p$

역원 계산(32번 라인) \(\lbrace (n-1)! \rbrace ^{p-2} \bmod p = n \times (n!)^{p-2} \bmod p\)

nCr mod p 계산(37번 라인) \(_{n}\mathrm{C}_{r} \bmod p = n! \times \lbrace r!(n-r)!\rbrace^{p-2} \bmod p = n! \times (r!)^{p-2} \times \lbrace(n-r)!\rbrace^{p-2} \bmod p\)

This post is licensed under CC BY 4.0 by the author.