모듈러 산술이 사용되는 이산 구조에서의 추가적인 연산에 대해 알아본다.
서로소인 두 정수 a,m와 양의 정수 k에 대해 a^k \equiv 1 \pmod{m}이고 k보다 작은 임의의 양의 정수 k'에 대해 a^{k'} \not\equiv 1 \pmod{m}일 때 k를 모듈러 m에 대한 a의 위수(Order)라고 하며 기호로는 \text{ord}_m(a)로 나타낸다. 오일러 토션트 함수의 성질에 의하면 a^{\phi(m)} \equiv 1 \pmod{m}이므로 a의 위수 k는 \phi(m)보다 커질 수 없으며 조금 더 확장하면 k는 \phi(m)의 약수가 되어야 한다.
모듈러 m에 대한 a의 위수가 \phi(m)일 때 a를 모듈러 m에 대한 원시근(Primitive Root)이라고 한다. a가 모듈러 m에 대한 원시근일 때, a^1, a^2, \ldots, a^{\phi(m)}을 m으로 나눈 나머지는 모두 다르며 각각의 나머지는 m과 서로소이다. 증명은 다음과 같이 할 수 있다.
각각의 나머지가 m과 서로소라는 사실은 a에 m의 소인수가 하나도 곱해지지 않았다는 사실에서 쉽게 알 수 있다. 각각의 나머지가 서로 다르다는 사실은 다음과 같이 증명이 가능하다.
x, y가 \phi(m) 이하의 양의 정수이고 x < y, a^x \equiv a^y \pmod{m}이라고 하면 \gcd(a, m) = 1이므로 a^{y-x} \equiv 1 \pmod{m}이다. 이때 y-x < \phi(m)이고, 이것은 \text{ord}_m(a) = \phi(m)이라는 사실과 모순이다. 따라서 위의 식을 만족하는 쌍 x, y는 존재하지 않으며, 이는 각각의 나머지가 서로 다르다는 것을 의미한다.
원시근을 갖는 정수는 2, 4, p^k이다. (p는 홀수 소수, k는 양의 정수) 이 글에서는 이에 대한 증명 전체를 다루지 않지만, 일부 정수의 경우 원시근의 존재 여부를 확인하는 방법이 어렵지 않으므로 다루었다.
- m = 2인 경우와 m = 4인 경우는 직접 계산하자. m이 4일 때는 3이 원시근이다.
- 임의의 홀수 x에 대해 x^2을 8로 나눈 나머지는 1이므로 m이 8의 배수일 경우 원시근이 존재하지 않는다.
- m이 서로 다른 두 홀수 소수 x, y의 공배수일 경우 다음과 같은 방법으로 원시근이 존재하지 않음을 보일 수 있다. m보다 작은 양의 정수 a가 m의 원시근이라고 가정하면, \text{ord}_x(a) = x-1, \text{ord}_y(a) = y-1이다. 이때 a가 m의 원시근이므로 \text{ord}_{xy}(a) = (x-1)(y-1)이어야 하고, 이것은 \text{lcm}(x-1, y-1) = (x-1)(y-1)을 의미한다. 따라서 x-1과 y-1이 서로소여야 하는데, x-1과 y-1이 모두 짝수이므로 서로소가 될 수 없다. 이것은 a가 m의 원시근이라는 처음의 가정이 잘못되었음을 의미한다.
m이 원시근을 갖는다면 원시근의 개수는 \phi(\phi(m))이다. 이에 대한 증명은 참고 링크의 블로그 2에서 확인할 수 있다.
원시근을 빠르게 찾는 확실한 방법에 대해서는 아직도 연구가 진행중이지만, 원시근이 존재하는 경우 그 개수가 충분히 많다고 할 수 있으므로 m 미만의 임의의 양의 정수를 하나 고른 뒤 그 수가 m의 원시근인지 확인하는 과정을 반복하면 높은 확률로 원시근을 빠르게 찾을 수 있다. 고른 수가 m의 원시근인지 확인하기 위해서는 위수를 계산하면 되며, 위수를 계산하는 방법은 참고 링크의 블로그 1에서 확인할 수 있다.
원시근에 대한 정의를 바탕으로 이산 로그(Discrete Logarithm)를 정의할 수 있다. a가 m의 원시근이고 a^k \equiv p \pmod{m}일 때 k를 나머지 변수에 대해서 나타내면 일반적인 로그 형태가 되는데 모듈러 산술이 사용되는 이산 구조의 로그이므로 이를 이산 로그라고 하고 k = \text{ind}_a p로 나타낸다.
이산 로그를 구하는 방법 중 하나인 아기걸음 거인걸음(Baby-Step Giant-Step Algorithm, BSGS)에 대해서 소개하면서 이번 글을 마무리하려고 한다. 목표는 a^k \equiv p \pmod{m}이고 a, p, m이 주어졌을 때 k를 구하는 것인데, m이 작은 경우 a를 계속 곱해 나가면서 k를 구할 수도 있지만 시간복잡도가 \Theta(m)이므로 m이 10^9 정도로 커지면 속도가 너무 느려진다. Baby-Step Giant-Step Algorithm은 제곱근 분할법과 유사한 중간 만남 기법을 이용해서 시간복잡도를 \Theta(\sqrt{m}\text{ lg }m)으로 줄인다. (로그는 마지막에 서로 같은 값의 쌍을 찾는 과정에서 붙는다.) 먼저 \lceil \sqrt{m} \rceil = v라고 하면, 이산 로그가 존재할 경우 m보다 작은 양의 정수 k가 항상 존재하므로 k = iv + j로 나타낼 수 있다. (0 \le i, j < v) 이것을 처음 식에 대입한 다음 식을 변형하면 다음과 같이 된다.
a^{iv+j} \equiv p \pmod{m}
a^{iv} \equiv pa^{-j} \pmod{m}
(a^v)^i \equiv p(a^{-1})^j \pmod{m}
이제 i가 0, 1, \ldots, v-1일 때 (a^v)^i \,\bmod\, m의 값과 j가 0, 1, \ldots, v-1일 때 p(a^{-1})^j \,\bmod\, m의 값을 모두 구한 다음 양쪽에서 서로 같은 값이 나오는 경우를 찾는다. std::map에 저장한 다음 찾는 것이 알기 쉽다. i가 최대한 작게, i가 같다면 j가 최대한 작은 쌍을 찾으면 k가 될 수 있는 최소의 양의 정수를 구할 수 있다.
구현은 다음과 같다. 만약 구하는 이산 로그가 존재하지 않을 경우 discrete_log 함수는 -1을 반환한다. pow 함수와 phi 함수는 이전 글들에서 정의한 것과 동일하다. (phi 함수의 경우 매개변수의 자료형과 반환형으로 int형 대신 long long형을 사용했으나 나머지는 전부 같다.)
typedef long long LL;
map<LL, LL>M;
LL pow(LL x, LL y, LL p);
LL phi(LL n);
LL discrete_log(LL a, LL p, LL m)
{
LL v = ceil(sqrt(m)), k = pow(a, phi(m) - 1, m);
for(int i = 0; i < v; i++)
{
if(M.find(p) == M.end())M[s] = i;
p = p * k % m;
}
k = pow(a, v, m);
p = 1;
for(int i = 0; i < v; i++)
{
if(M.find(p) != M.end())return i * v + M[p];
p = p * k % m;
}
return -1;
}
이 방법을 이용하면 이산 로그를 빠르게 구할 수 있으며 a가 m의 원시근이 아닌 경우에도 위 식을 만족하는 k를 구할 수 있다. 그밖에도 이산 로그를 구하는 다양한 방법이 존재하며, 이에 대한 내용은 참고 링크의 블로그 3에서 볼 수 있다.
[참고 링크]
[연습문제]
Baby-Step Giant-Step으로 이산 로그를 구하면 되는데, 이산 로그가 존재하지 않는 경우도 입력될 수 있으므로 그런 경우를 처리해 주어야 한다. 마지막에 양변의 값이 같은 쌍이 하나도 나오지 않으면 이산 로그가 존재하지 않는다.
'Algorithm > D. Math & Number Theory' 카테고리의 다른 글
72. Discrete Square Root & Tonelli-Shanks Algorithm (0) | 2021.06.30 |
---|---|
66. Inclusion-Exclusion Principle (0) | 2021.06.24 |
63. Nim Game & Sprague-Grundy Theorem (0) | 2021.06.22 |
62. Game Theory (0) | 2021.06.21 |
61. Sum of Squares (0) | 2021.06.18 |