관련 코드는 github에서 찾아볼 수 있다.
이 글의 순서는 다음과 같다.
- 원시근의 정의 및 관련 사실들. 핵심 중의 핵심을 제외하고, 증명 대부분은 생략할 것이다.
- 원시근을 찾는 알고리즘과 위수를 계산하는 알고리즘.
- 이산로그 문제와 Baby Step Giant Step. 그리고 그 확장인 Pohlig-Hellman 알고리즘 (optional!)
- 이산제곱근 문제와 그 해결법. 특수한 경우인 modular square root의 경우. 그리고 Adleman-Manders-Miller (매우 optional!)
- 특히, Adleman-Manders-Miller는 지금까지 나온 경우를 딱 한 번 봤다. (GP of Baltic Sea) 이는 논문 링크 + 구현체 링크로 대체.
- 그리고 마지막으로, FFT와 NTT 사이의 연결고리를 설명한다. 앞서 mobius inversion = 포함-배제를 설명한 것과 같이, FFT = NTT를 설명한다.
원시근이란 무엇인가? 위수란 무엇인가?
- 자연수 n≥2이 있을 때, n과 서로소이며 n 미만인 자연수의 개수는 ϕ(n)이다.
- 만약 g,g2,⋯,gϕ(n)을 각각 n으로 나눈 나머지가 이 ϕ(n)개의 자연수 전부가 된다면, g를 n의 원시근이라 한다.
- 다른 말로 하면, t가 n과 서로소인 자연수라면 적당한 1≤k≤ϕ(n)이 있어 gk≡t(modn)이란 것이다.
- 사실 1. 원시근을 갖는 자연수는 2,4와 홀수 소수 p와 자연수 k≥1에 대하여 pk, 2pk다.
- 사실 2. n이 원시근을 갖는 자연수라면, 원시근은 정확히 ϕ(ϕ(n))개 있다. 다른 말로 하면, "원시근은 꽤 많다".
- 예를 들어, n=p가 소수라면, ϕ(ϕ(p))=ϕ(p−1)이다. 특히, ϕ(n)>n/(eγloglogn+3/loglogn)임이 알려져 있다.
- 자연수 g,n이 있을 때, (단, gcd(g,n)=1) gk≡1(modn)을 만족하는 최소 자연수 k를 g의 위수라고 한다.
- 사실 3. g가 n의 원시근인 것은, g의 위수가 ϕ(n)인 것과 동치이다. (증명 힌트. gi≡gj(modn)이면 gj−i≡1(modn))
- 사실 4. ("학부 대수학의 반의 avatar") gcd(g,n)=1이고 g의 위수가 k라고 하자. gm≡1(modn)이 성립할 필요충분조건은 k|m이다.
- 사실 4 증명. k|m이면 gm≡(gk)m/k≡1m/k≡1(modn)이므로 ok. 만약 gm≡1(modn)인데 k|m이 아니라고 하자. 그러면 m=kt+r인 0<r<k가 있고, gr≡gm−kt≡1(modn)이다. 그런데 0<r<k이므로 이는 위수의 "최소성"에 모순이다. 즉, 애초에 k가 gk≡1(modn)을 만족하는 "최소의" 자연수였는데, gr≡1(modn), r<k이므로 모순. 좋은 아이디어.
- 사실 5. g의 위수가 t라면, gd의 위수는 t/gcd(d,t)가 된다. 사실 4에서 바로 증명할 수 있다.
- 사실 1, 2의 증명은 학부 정수론에서 가장 중요한 부분 중 하나이며, 후에 (사실 1은) 현대대수학에서도 다시 증명하게 된다.
왜 원시근이 강력한가?
- g가 원시근이라 하자. gk≡r(modn)이라면, k를 r의 "로그 값"이라고 생각할 수 있겠다.
- 로그가 강력한 이유가 뭐였더라? 거듭제곱을 곱셈으로, 곱셈을 덧셈으로 바꿔주는 것이었다.
- 우리는 거듭제곱은 잘 모르지만, 앞서 2, 3 단원을 통해서 일차합동식 ax≡b(modn)에 대해서는 안다.
- 그러니 원시근을 사용해서 거듭제곱에 관한 문제를 일차식으로 바꿀 수 있을 것이다. 이 접근을 "이산제곱근"에서 사용한다.
이제부터 원시근을 찾는 방법을 설명한다. n이 원시근을 갖는 자연수, 즉 2,4,pk,2pk 형태라고 가정하자.
- 원시근은 충분히 많으므로, 랜덤하게 g를 잡은 뒤 g가 원시근인지 판별하는 과정을 원시근을 찾을 때까지 반복하면 된다.
- 원시근이 많다는 것은, 한 번 랜덤한 시도를 했을 때 성공적으로 원시근을 찾을 확률이 나쁘지 않다는 것이다.
- g가 원시근인지 판별하려면, 먼저 gcd(g,n)=1인지 확인한다. 그 후, g의 위수가 ϕ(n)인지 확인하면 된다.
그러므로, 원시근을 찾는 방법을 알기 위해서는 위수를 계산하는 알고리즘을 찾으면 된다. 여기서 사실 4가 활용된다.
- ϕ(n)의 소인수분해가 이미 계산되었다고 가정한다. ϕ(n)=pe11⋯pekk라고 가정한다.
- n=pk, n=2pk인 경우 ϕ(n)=pk−1(p−1)임에 유의하라.
- 이제 g의 위수를 찾는 것이 목표다. gcd(g,n)=1임을 가정한다. 오일러의 정리에 의하여, gϕ(n)≡1(modn).
- 이는 사실 4에 의하여, 위수의 값이 ϕ(n)의 약수임을 의미한다.
- 이제 gϕ(n)/p1의 값을 보자. 만약 이 값이 1(modn)이라면, 사실 4에서 위수가 ϕ(n)/p1의 약수임을 얻는다.
- 만약 gϕ(n)/p1≡1(modn)이었다면, gϕ(n)/p21을 보자. (단, p21|ϕ(n)) 이 값 역시 1(modn)이라면, 사실 4에서 위수가 ϕ(n)/p21의 약수.
- gϕ(n)/p21은 1(modn)이 아니었다고 하면, 위수가 ϕ(n)/p1의 약수였으나 ϕ(n)/p21의 약수는 아님을 의미한다.
- 이는 다르게 말하면 위수가 p1을 몇 개 가지고 있는지 파악했다는 이야기다. 이제 p2로 넘어가서 같은 작업을 반복, pk까지 하자.
- 즉, ϕ(n)에서 시작하여, g???≡1(modn)이 성립할 때까지 계속 소인수들을 제거해주면 된다.
- 시간복잡도를 생각해보면, 대강 (e1+e2+⋯+ek)=O(logn)번 g의 거듭제곱을 계산하므로, O(log2n)이다.
- 물론, ϕ(n)의 소인수분해가 필요했음에 주의하라. 필요한 경우, 6단원에서 배운 Pollard-Rho 소인수분해를 활용하자.
이산로그 문제와 Baby Step Giant Step
- 원시근 g와 gcd(h,n)=1을 만족하는 자연수 h가 주어졌을 때, gx≡h(modn)을 만족하는 x를 찾는 문제를 이산로그 문제라 한다.
- 즉, 앞서 언급했던 원시근의 강력함인 "로그"를 실제로 계산하는 문제이다. 이산적인 집합에서 계산하고 있으니 이산로그 문제다.
- 이 문제를 CP/PS에서 보이는 "meet-in-the-middle"을 이용하여 O(√nlogn) 시간에 해결하는 알고리즘이 있다.
- B=⌈√ϕ(n)⌉이라고 하자. 그러면 0과 ϕ(n) 사이의 모든 값은 적당한 0≤u,v<B에 대하여 uB+v라고 쓸 수 있다.
- 그러니 우리는 guB+v≡h(modn)을 만족하는 0≤u,v<B를 찾으면 된다.
- 그런데 이 식을 변형하면 (gB)u≡h⋅g−v(modn)을 얻는다. 이제 meet-in-the-middle 각이 보인다.
- h⋅g−0부터 h⋅g−(B−1)를 C++의 std::map과 같은 자료구조에 넣어놓자. 이때, key 값을 h⋅g−v로 하고, value 값을 v로 하도록 한다.
- 이제 u에 대하여 순회하면서, (gB)u(modn)이 해당 자료구조의 key 값에 있는지 확인하자. 있다면, value 값에서 v를 얻는다.
- 이 방식으로 u,v를 찾을 수 있으며, 시간복잡도는 O(√nlogn)이다. 로그는 C++의 std::map 때문에 붙는다.
- 이 알고리즘은 이렇게 (modn)에 대한 이산로그를 구하는 것에서만 쓰일 수 있는 것이 아니라, finite abelian group 구조를 갖는 모든 문제에서 적용될 수 있다. 애초에 이산로그 문제 자체가 finite abelian group 구조에서 정의되는 문제이며, 지금 우리가 다루고 있는 문제는 그 중 일부인 경우이다.
- 말은 어렵게 했으나, 실제로 문제를 풀면 다른 생각이 들 수도 있다. BOJ의 xorshift32 문제를 참고하라.
- Note. g가 원시근이 아니어도, g의 위수를 k라고 하면 같은 방법으로 O(√klogk) 알고리즘을 생각할 수 있다. 위에서 ϕ(n)을 k로 바꾸면 된다.
Pohlig-Hellman 알고리즘 (optional)
- 위 Baby-Step-Giant-Step 알고리즘을 강화한 것이 Pohlig-Hellman 알고리즘이다. ϕ(n)=pe11⋯pekk라고 하자.
- gx≡h(modn)인 x를 찾는다는 것은, 사실 x(modϕ(n))을 찾는다는 것이다.
- 그러니, 중국인의 나머지 정리를 생각하면 x(modpeii)들을 계산한 다음 합치면 충분하다.
여기서 O(√pilogpi) 시간에 x(modpi)를 찾는 방법을 제시한다.
gx≡h(modn)이면, (gϕ(n)/pi)x≡hϕ(n)/pi(modn) 역시 성립한다. 여기서 gϕ(n)/pi의 위수가 pi임에 주목하자.
이 새로운 이산로그 문제를 풀면 x(modpi)를 얻고, 이를 위해서 소모되는 시간은 O(√pilogpi)가 된다.
이제 이를 확장하여, x(modp2i)을 찾는 방법을 고안하자. 물론, p2i|ϕ(n)이라고 가정한다.
마찬가지로, (gϕ(n)/p2i)x≡hϕ(n)/p2i(modn)을 생각한다. gϕ(n)/p2i의 위수는 p2i이다.
그러니 이 이산로그 문제를 풀면 x(modp2i)을 얻는데, 중요한 점은 우리가 이미 x(modpi)를 안다는 것이다.
이미 계산한 값인 x(modpi)를 r이라고 하자. 이제 x(modp2i)=pik+r이라고 쓸 수 있고, 목표는 k가 된다. 그러면 (gϕ(n)/pi)k≡hϕ(n)/p2i⋅(gϕ(n)/p2i)−r(modn)을 얻는다. 이제 k를 찾기 위해 이산로그 문제를 풀면 O(√pilogpi) 시간에 x(modp2i)을 얻는다.
이를 peii까지 반복하고, 이를 각 prime power에 대해 반복하면 끝. 시간복잡도는 O(∑ei√pilogpi)다.
관련 문제로는 위에 언급한 xorshift32를 강화한 xorshift64가 있다.
그리고 필자도 굉장히 최근에 배운 트릭을 하나 소개한다. 매우 optional인 건 맞지만, 흥미롭고 Pohlig-Hellman 보다 쉽다.
- gx≡h(modp2)를 푼다고 가정하자. 단 g는 p2에 대한 원시근이다.
- 먼저 ga1≡h(modp)를 푼다. 그러면 여기서 얻는 결과는 x≡a1(modp−1)이 될 것이다.
- 이제 ga2≡h(modp2)을 푼다. 이제 a2≡a1(modp−1)을 알고 있으므로, a2(modp)를 알면 된다.
- 이를 위해서, g(p−1)a2≡hp−1(modp2)을 푼다. 그런데, 놀라운 점은 gp−1, hp−1 모두 1(modp)라는 것이다.
- 그래서 사실 (1+pu)a2≡(1+pv)(modp2) 형태의 식을 풀게 되며, 특히 g가 원시근이면 u는 p의 배수가 아니다.
- 또한, 이항정리에서 (1+pu)a2≡1+pua2(modp2)이다. 그러니 결국 ua2≡v(modp)를 풀면 ok.
- 그러니 Baby-Step-Giant-Step을 두 번 돌릴 필요가 없으며, 한 번만 돌려도 된다. 이는 확장이 가능한 아이디어다. 이 링크 참고.
이산제곱근 문제
- 이번에는 x,b가 주어졌을 때, (단, gcd(b,n)=1) ax≡b(modn)을 만족하는 a를 구하는 것이 목표다.
- 중국인의 나머지 정리의 아이디어를 가져오자. n=pe11⋯pekk라 하면, a(modpeii)를 각각 구한 뒤 합치는 전략을 세울 수 있다.
- 그러니 n이 prime power인 경우에서만 해결하면 충분하다.
- 여기서는 n이 홀수 소수의 거듭제곱인 경우만 다루겠다. 이제 원시근 g의 존재를 가정해도 된다.
a=gu, b=gv인 u,v를 이산로그 알고리즘으로 찾자. 목표는 gux≡gv(modn)인 x를 구하는 것이다.
이는 gux−v≡1(modn)과 같으니, 사실 4에 의하여 ux≡v(modϕ(n))을 푸는 것과 같다.
이는 일차합동식이므로, 2단원에서 배운 내용을 통해 해결할 수 있다. u를 알면, 이를 가지고 a(modn) 역시 구할 수 있다.
특히, gcd(x,ϕ(n))=1이라면, tx≡1(modϕ(n))인 t를 찾을 수 있다.
그러면 bt≡atx≡a(modn)이 성립함을 알 수 있고, (오일러 정리) a(modn)을 쉽게 찾을 수 있다.
위 예시에서 2의 거듭제곱이 여러모로 까다롭다는 것을 알 수 있다. 이렇게 2의 거듭제곱을 가지고 귀찮게하는 문제는 적다.
- 하지만, 이 사실이 도움이 될 수 있다 : 모든 x(mod2n)은 (단, x 홀수) 5k 또는 −5k로 나타낼 수 있다.
- 즉, x가 1(mod4)인 경우, 전부 5의 거듭제곱으로 표현이 되며, 3(mod4)인 경우, −x가 5의 거듭제곱으로 표현이 된다.
- 그러니 5를 2n의 일종의 원시근과 비슷한 무언가로 생각할 수 있겠다. 이를 통해 이산로그 등 문제를 접근할 수 있다.
modular square root
- 이 파트에서는 특수한 경우인, x2≡a(modn)을 다룬다. 편의상 gcd(a,n)=1을 가정한다.
- 역시 중국인의 나머지 정리 아이디어를 사용하여, x2≡a(modpk)만 해결하면 된다.
먼저 p가 홀수 소수인 경우부터 보자. 특히, k=1인 경우, 즉 x2≡a(modp)인 경우를 보자.
- 이차잉여. 위 식이 해를 가질 조건은 a(p−1)/2≡1(modp)이다.
- 증명. 스케치만 하자면, 해를 가질 조건이 a가 geven인 것과 동치이며 이건 다시 a(p−1)/2≡1(modp)와 동치이다.
해를 갖는 경우, O(log2p) 시간에 해를 찾는 알고리즘이 있다. Tonelli-Shanks 알고리즘이며, 설명은 이 블로그에 있다.
특히, x가 해면 −x 역시 해임에 주목하자. 해는 정확히 2개 있을 것이다. (이유가 궁금하다면, 이 정리 참고) 이제 문제는 k≥2인 경우이다.
이제부터 각 x2≡a(modpk)의 해를 x2≡a(modpk+1)의 해로 "lift"하는 방법을 생각해보자.
이미 r2≡a(modpk)인 r의 값을 찾았다고 생각하자. 해는 정확히 r,−r 두 개가 있을 것이다. 이제 x2≡a(modpk+1)을 풀자.
x2≡a(modpk) 역시 성립하니, x≡r(modpk) 또는 x≡−r(modpk)가 성립해야 한다.
- 이제 x≡r(modpk)인 경우를 가정하자. 그러면 x(modpk+1)은 r+tpk 형태로 쓸 수 있다. 단, 0≤t<p.
- 이제 x2(modpk+1)을 생각하면, r2+2rtpk+t2p2k≡r2+2rtpk(modpk+1)과 같다.
- 그러니, 문제는 결국 r2+2rtpk≡a(modpk+1)이고, 이를 변형하면 2rtpk≡(a−r2)(modpk+1)이다.
- 여기서 a−r2(modpk+1)이 pk의 배수임에 주목하자.
- 그러니 원하는 것은 (2rt−(a−r2)/pk)⋅pk가 pk+1의 배수가 되는 것, 즉 2rt≡(a−r2)/pk(modp)이다.
- 2와 r이 전부 p와 서로소이므로, 이 합동식을 풀어 t(modp)를 얻는다. 이제 x=r+tpk가 해이다.
- 시작을 x≡−r(modpk)로 얻었다면, −r−tpk를 얻었을 것이다. 그러니 여전히 해는 x,−x 정확히 두 개다.
이러한 과정을 잘 나타내는 정리가 Hensel's Lemma다. 이제 p=2인 경우를 생각해보자.
k=1,2인 경우, 즉 (mod2), (mod4)에서 식을 푸는 것은 brute force로 가능할 것이다. 이 경우는 생략.
이제부터 k≥3을 가정한다. 핵심적인 사실은, x2≡1(mod8)이 모든 홀수 x에 대해서 성립한다는 것이다.
- x2≡a(mod2k)가 해를 가질 필요충분조건은 a≡1(mod8).
- 이때, 해는 정확히 4개 존재한다. ((mod2k)에서) x가 해라면 x,−x,x+2k−1,−x+2k−1이 해가 된다.
역시 "lift"할 각을 재보자. x2≡a(mod23)의 해는 (애초에 a≡1(mod8)이니) x=1,3,5,7(mod8)이다.
해가 4개가 있고, 하나만 알면 나머지 세 개의 해를 쉽게 얻을 수 있다. 그러니 해를 하나만 관리해도 충분할 것이다. x=1로 시작하자.
x2≡a(mod2k)의 해를 하나 찾았다고 하고, 이를 r이라 하자. 그러면 (r+t2k−1)2≡r2+rt2k(mod2k+1)이다.
만약 r2≡a(mod2k+1)이라면, r이 그대로 x2≡a(mod2k+1)의 해가 된다.
그렇지 않다면, r2≡a+2k(mod2k+1)일 것이다. 그러면 x=r+2k−1이 해가 된다. 이제 끝.
Adleman-Manders-Miller (매우 optional)
- 이산제곱근을 더욱 효율적으로 계산하는 알고리즘으로, Tonelli-Shanks의 확장이다.
- 이 글, 이 논문, 그리고 KAIST의 ICPC팀(이었던) 더불어민규당의 팀노트를 (discrete kth root) 참고하자.
- PS/CP 환경에서는 쓰이는 것은 저 오픈컵을 제외하고 본 적이 없다. 개인적으로도 암호학 대회 환경을 제외하면 쓴 적이 없다.
FFT와 NTT
- FFT와 FFT의 원리를 이미 알고 있다고 가정한다. 2n=N이 FFT의 "크기"라고 가정하자.
- FFT에서 사용되는 사실은 ω=e2πi/N이라 할 때, ωN=1이고 1,ω,⋯,ωN−1이 모두 다르다는 것이다.
- 또한, inverse FFT 과정에서는 계산 후에 N을 나누어야 하므로, 복소수에서 N을 나누는 연산이 잘 정의됨을 이용한다.
- NTT도 다르지 않다. p−1=2ab이고, a가 충분히 큰 소수 p와 p에 대한 원시근 g를 준비하자. 2a=N이라 하자.
- ω=gb라 하면, ωN=1이고, 1,ω,⋯,ωN−1이 모두 다르다. (모든 연산은 p로 나눈 나머지로 본다)
- 마찬가지로, N이 p와 서로소이므로, N으로 나누는 연산이 잘 정의된다. (modular inverse가 존재하므로)
- 그러니, FFT와 NTT는 정확히 같은 알고리즘이다. 실제 코드도 별로 다를 것이 없다.