백준 11689 'GCD(n, k) = 1'

Bonwoong Ku·2024년 12월 26일
0

알고리즘 문제풀이

목록 보기
110/110

아이디어

  • 문제에 나온 "GCD(n, k) = 1을 만족하는 자연수 1 ≤ k ≤ n 의 개수"를 오일러 피(phi) 함수 φ(n)\varphi(n)이라 한다. 이에 대한 설명은 나무위키 등에 자세하게 나와 있지만, 주요한 성질을 정리해보고 가자.

    • i) 소수 pp에 대해 φ(pn)=pn1(p1)\varphi(p^n) = p^{n-1}(p-1)이다.
      • pnp^npp의 거듭제곱만을 인수로 가지므로, 어떤 자연수 mm에 대해 gcd(m,pn)\gcd(m, p^n)mmpp의 배수일 때 pp가 되고, 그렇지 않을 때는 11이 될 것이다. 따라서 φ(pn)\varphi(p^n)pnp^n에서 pp의 배수의 개수를 뺀 pnpnp=pn1(p1)p^n - \frac{p^{n}}{p}=p^{n-1}(p-1)임을 알 수 있다.
    • ii) 서로소인 자연수 aa, bb에 대해 φ(ab)=φ(a)φ(b)\varphi(ab) = \varphi(a) \varphi(b)이다.
  • 그러므로 nn을 소인수분해한 다음에, 각 소인수와 지수에 대해 i)를 적용하고, 이를 모두 곱하면 된다.

  • 소인수분해를 하기 위해서는 소수 리스트가 필요한데, nn의 최댓값이 101210^{12}이므로 nn 이하의 소수를 모두 구해놓는 것은 불가능하다. 그러나, nn이 소수가 아닐 때 n\sqrt{n} 이하의 소인수를 1개 이상 가진다는 점을 이용하면 n\sqrt{n}의 길이의 리스트만 보는 것으로 소수를 판별할 수 있다. 그러므로 1012=106\sqrt{10^{12}}=10^6 이하의 범위에서만 소수를 찾아놓자.

    • 만약 리스트 내의 어떤 소수로도 nn이 나뉘어지지 않았다면 nn은 소수이므로 i)에 의해 φ(n)=n1\varphi(n)=n-1을 반환한다.

    • 적당한 소인수로 나눈 뒤에도 10610^6보다 큰 자연수 RR이 남아버려 더 이상 소인수분해가 불가능할 때에는, 성질 ii)를 이용해 재귀적으로 φ(R)\varphi(R)를 구해 구하던 결과에 곱하기로 하자. 이 재귀는 많아야 1번만 실행될 것이다.

코드

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;

public class Main {
    static BufferedReader rd = new BufferedReader(new InputStreamReader(System.in));

    static final int M = 1_000_000;   // sqrt of max N(10^12)
    static boolean[] isPrime = new boolean[M+1];
    static long n;

    public static void main(String[] args) throws Exception {
        // use Eratosthenes' sieve
        Arrays.fill(isPrime, true);
        isPrime[0] = false;
        isPrime[1] = false;
        for (int i=2; i <= M; i++) {
            if (isPrime[i]) {
                for (int j=i*2; j <= M; j += i) isPrime[j] = false;
            }
        }

        n = Long.parseLong(rd.readLine());
        System.out.println(phi(n));
    }

    // 1. for prime number p: phi(p^n) = p^n - p^(n-1) = p^(n-1) * (p-1)
    // 2. for coprime natural numbers a, b: phi(ab) = phi(a) * phi(b)
    static long phi(long n) {
        if (n == 1) return 1;

        long ret = 1;
        long m = n;
        boolean isNPrime = true;
        for (int i=2; i<=Math.min(n, M); i++) {
            if (!isPrime[i]) continue;
            int x = 0;
            while (m % i == 0) {
                x++;
                m /= i;
                isNPrime = false;
            }
            if (x > 0) {
                // multiply p^(x-1) * (p - 1)
                for (int j = 0; j < x-1; j++) ret *= i;
                ret *= i - 1;
            }
        }
        if (isNPrime)
            return n-1;
        return ret * phi(m);
    }
}

메모리 및 시간

  • 메모리: 16484 KB
  • 시간: 184 ms

리뷰

  • 수 학 시 러
  • 테스트케이스를 가능한 많이 찾아봐야했다. 특히 디버깅에 유용했던 테스트케이스 몇 개를 남겨놓는다.
    • 1 000 0031\ 000\ 003: 10610^6보다 큰 최소의 소수
    • 2 000 0062\ 000\ 006: 10610^6보다 큰 최소의 소수와 다른 소수의 곱
    • 9 999 999 9839\ 999\ 999 \ 983: 범위 한계에 가까운 수, 10610^6보다 큰 소인수를 포함한다.
profile
유사 개발자

0개의 댓글