Project Euler 5: 1부터 20까지 모두로 나누어떨어지는 가장 작은 수를 최소공배수로 바로 만들기

1부터 20까지의 모든 수로 나누어떨어지는 가장 작은 양의 정수를, 하나씩 찾아보는 대신 유클리드 호제법과 lcm(a, b) = a / gcd(a, b) × b를 거듭 적용해 만듭니다. 이 방법이 왜 정확히 최소공배수를 주는지 증명하고, 정수 범위와 C++17·Python 3 구현까지 다룹니다.

Project Euler알고리즘수학최소공배수

스포일러 주의 · 이 글에는 풀이 과정과 정답이 그대로 나옵니다. 직접 먼저 풀어 보고 싶다면 Project Euler 5번 문제부터 보세요.

Project Euler 시리즈의 다섯 번째 문제입니다. “모든 수로 나누어떨어진다”는 조건은 바로 공배수의 정의이고, 그중 가장 작은 것은 최소공배수입니다. 그래서 이번에는 답을 찾아 헤매는 대신 답을 직접 만듭니다. 그 과정에서 유클리드 호제법과 두 수의 최소공배수 공식이 왜 맞는지를 처음부터 증명해 둡니다. 둘 다 뒤의 문제에서 자주 다시 나옵니다.

스포일러 주의: 이 글에는 풀이 전체와 정답이 들어 있습니다. 직접 먼저 풀어 보고 싶다면 원래 문제부터 보세요. 이 시리즈의 글에는 광고를 싣지 않습니다. 문제 본문의 라이선스(CC BY-NC-SA 4.0)가 비영리 조건을 두기 때문입니다.

문제

문제에 나온 예로, 1부터 10까지의 모든 정수로 나머지 없이 나누어떨어지는 가장 작은 수는 2520입니다. 같은 방식으로 1부터 20까지의 모든 정수로 나누어떨어지는 가장 작은 양의 정수를 구하는 것이 문제입니다.

따로 받는 입력은 없고 상한은 20으로 정해져 있습니다. 구할 것은 그 조건을 만족하는 양의 정수 중 최솟값 하나입니다.

필요한 배경 지식

나누어떨어짐과 최소공배수. 정수 m이 양의 정수 k의 배수일 때, 즉 m = k × q인 정수 q가 있을 때 “k가 m을 나눈다”고 하고 k | m으로 씁니다. 여러 수의 공배수는 그 모든 수의 배수이고, 최소공배수(lcm, least common multiple)는 양의 공배수 중 가장 작은 것입니다. 양의 정수 몇 개를 모두 곱하면 공배수가 하나는 생기므로, 최소공배수는 항상 있습니다.

유클리드 호제법. 두 수의 최대공약수(gcd)는 다음 성질로 구합니다. a를 b(> 0)로 나눈 몫을 q, 나머지를 r(0 ≤ r < b)이라고 하면

gcd(a, b) = gcd(b, r),    r = a − q × b
gcd(a, 0) = a

입니다. 이유는 공약수의 집합이 같기 때문입니다. d가 a와 b를 나누면 a − q × b = r도 나누고, 반대로 d가 b와 r을 나누면 q × b + r = a도 나눕니다. 공약수가 모두 같으니 그중 가장 큰 것도 같습니다. 나머지는 매번 엄격히 작아지므로 언젠가 0이 되고, 그때 남은 수가 최대공약수입니다. 예를 들어 gcd(840, 9)는 840 = 93 × 9 + 3, 9 = 3 × 3 + 0이므로 3입니다.

최대공약수는 두 수의 정수 결합입니다(베주 항등식). 호제법의 각 나머지 r = a − q × b는 직전 두 수의 정수 결합이고, 직전 두 수도 처음의 a, b의 정수 결합이므로, 차례로 따라가면 모든 나머지가 s × a + t × b(s, t는 정수) 꼴입니다. 마지막 0이 아닌 나머지가 최대공약수이므로 gcd(a, b) = s × a + t × b인 정수 s, t가 있습니다. 위 예에서는 3 = 1 × 840 − 93 × 9입니다. 이 사실은 아래 증명에서 한 번 씁니다.

소인수분해로 보는 최소공배수(손 검산용). 각 소수의 지수를 비교하면, 최소공배수는 소수마다 가장 큰 지수를 고른 수입니다. 예를 들어 2520 = 2³ × 3² × 5 × 7이고, 8 = 2³과 9 = 3²은 10 이하에서 가장 큰 2의 거듭제곱과 3의 거듭제곱입니다. 최종 알고리즘은 소인수분해를 쓰지 않습니다. 결과를 손으로 확인할 때만 씁니다.

가장 단순한 방법

m = 1, 2, 3, …을 차례로 시험해서, 2부터 20까지의 모든 k로 나누어떨어지는 첫 m을 고르면 됩니다.

m = 1
while not all(m % k == 0 for k in range(2, 21)):
    m += 1

반드시 끝나기는 합니다. 1부터 20까지를 모두 곱한 수가 조건을 만족하므로, 그 전에 반드시 멈춥니다. 문제는 시험하는 후보의 개수가 정답 그 자체라는 점입니다. 후보 하나에 최대 19번의 나머지 연산이므로 시간은 O(답 × n)입니다(n은 상한). 그런데 상한이 소수 p를 지날 때마다 답은 p배가 되므로(p보다 작은 수는 p를 인수로 갖지 않습니다), n이 커지면 답과 후보의 개수가 빠르게 늘어납니다. n = 10에서는 후보가 2520개뿐이지만 n = 20에서는 훨씬 많아지는데, 정확히 몇 개인지는 “정답과 검증”에서 보겠습니다.

후보를 20씩 건너뛰거나(답은 20의 배수이므로) 2520씩 건너뛰면(답은 1부터 10까지의 공배수, 따라서 아래 보조정리 A에 따라 2520의 배수이므로) 후보가 그만큼 줄어듭니다. 하지만 여전히 답에 비례하는 개수를 봅니다. 답의 크기와 상관없이 끝나는 방법이 필요합니다.

풀이 유도

1단계: 문제를 다시 읽습니다. “1부터 n까지의 모든 정수로 나누어떨어지는 양의 정수”는 1, 2, …, n의 양의 공배수이고, 그중 가장 작은 것은 정의 그대로 lcm(1, 2, …, n)입니다. 이를 L(n)으로 씁니다. 찾을 필요 없이 L(20)을 계산하면 됩니다.

2단계(보조정리 A): 모든 공배수는 최소공배수의 배수입니다. 어떤 수들의 최소공배수를 l, 양의 공배수 하나를 m이라고 합시다. m을 l로 나누어 m = q × l + r(0 ≤ r < l)로 쓰면, 그 수들 각각은 m과 l을 나누므로 r = m − q × l도 나눕니다. 즉 r은 공배수입니다. r > 0이면 l보다 작은 양의 공배수가 되어 l이 최소라는 데 모순이므로 r = 0, 곧 l | m입니다. 반대로 l의 배수는 당연히 공배수입니다. 그래서 공배수의 집합 = l의 배수의 집합입니다.

3단계: 하나씩 접어 넣어도 됩니다. L(k) = lcm(L(k − 1), k)입니다. 양의 정수 m에 대해

  • m이 1, …, k의 공배수이다
  • ⇔ m이 1, …, k − 1의 공배수이고 k | m이다
  • ⇔ (보조정리 A) L(k − 1) | m이고 k | m이다
  • ⇔ m이 L(k − 1)과 k의 공배수이다

이므로 두 공배수 집합이 같고, 따라서 가장 작은 원소도 같습니다. 이제 두 수의 최소공배수만 구할 수 있으면 됩니다.

4단계: 두 수의 최소공배수는 lcm(a, b) = a / gcd(a, b) × b입니다. 양의 정수 a, b에 대해 g = gcd(a, b)로 두고 a = g × a′, b = g × b′로 씁니다.

  • gcd(a′, b′) = 1입니다. 1보다 큰 d가 a′와 b′를 함께 나누면 g × d가 a와 b의 공약수가 되어 g가 최대라는 데 모순입니다.
  • l = g × a′ × b′는 공배수입니다. l = a × b′ = a′ × b이기 때문입니다. 그리고 l = (a / g) × b이고, g | a이므로 a / g는 정수입니다.
  • l이 가장 작습니다. 양의 공배수 m을 m = a × x로 쓰면 m = g × a′ × x이고, b = g × b′가 m을 나누므로 b′ | a′ × x입니다. 베주 항등식으로 s × a′ + t × b′ = 1인 정수 s, t를 잡으면 x = s × a′ × x + t × b′ × x이고, 오른쪽 두 항이 모두 b′로 나누어떨어지므로 b′ | x입니다. x ≥ b′이므로 m = a × x ≥ a × b′ = l입니다.

작은 예로 손 검산. 문제의 예 n = 10을 따라갑니다. L = 1에서 시작해 k = 2, …, 10을 차례로 접어 넣습니다.

k    g = gcd(L, k)    L / g × k
2    gcd(1, 2)   = 1     1 / 1 × 2  = 2
3    gcd(2, 3)   = 1     2 / 1 × 3  = 6
4    gcd(6, 4)   = 2     6 / 2 × 4  = 12
5    gcd(12, 5)  = 1    12 / 1 × 5  = 60
6    gcd(60, 6)  = 6    60 / 6 × 6  = 60
7    gcd(60, 7)  = 1    60 / 1 × 7  = 420
8    gcd(420, 8) = 4   420 / 4 × 8  = 840
9    gcd(840, 9) = 3   840 / 3 × 9  = 2520
10   gcd(2520, 10) = 10   2520 / 10 × 10 = 2520

결과 2520은 문제의 예와 같습니다. 소인수분해로 보면 4는 2의 지수를 1에서 2로, 8은 2에서 3으로 올리고, 9는 3의 지수를 1에서 2로 올립니다. 6과 10은 이미 L의 약수라서 g = k가 되어 L이 바뀌지 않습니다. 최종 2520 = 2³ × 3² × 5 × 7과 맞습니다.

최종 알고리즘

  1. L = 1로 시작합니다.
  2. k = 2, 3, …, n 순서로 g = gcd(L, k)를 유클리드 호제법으로 구하고 L = L / g × k로 바꿉니다.
  3. L을 돌려줍니다. n < 2이면 반복이 돌지 않아 1입니다.
  4. 정답을 출력하기 전에 작은 입력을 확인합니다. gcd(12, 18) = 6, gcd(840, 9) = 3, lcm(4, 6) = 12, L(1) = 1, L(10) = 2520이어야 하고, 가장 단순한 방법으로 n = 10을 직접 찾은 값도 2520이어야 합니다. 하나라도 다르면 0이 아닌 종료 코드로 끝냅니다.

정확성 증명

k번째 반복을 마친 직후마다 다음이 성립합니다(불변식).

L = lcm(1, 2, …, k)이다.

반복 전(k = 1)에는 L = 1 = lcm(1)이므로 성립합니다. k − 1까지 성립한다고 하면, k번째 반복에서 g = gcd(L, k)는 배경 지식의 유클리드 호제법이 정확히 계산하고, 새 값 L / g × k는 4단계에 따라 lcm(L(k − 1), k)이며, 3단계에 따라 이것이 L(k)입니다. 따라서 k = n까지 반복을 마치면 L = L(n)이고, 1단계에 따라 이것이 1부터 n까지의 모든 정수로 나누어떨어지는 가장 작은 양의 정수입니다. 반복은 정확히 n − 1번, 호제법은 나머지가 매번 줄어드므로 매번 끝납니다.

복잡도

시간. gcd를 n − 1번 부릅니다. gcd(L, k)는 첫 나눗셈에서 (k, L mod k)가 되고, 그 뒤로는 k보다 작은 수만 다룹니다. 호제법에서 a ≥ b인 (a, b)가 (b, r) → (r, ·)로 두 단계 지나면 앞의 수는 a의 절반 미만이 됩니다. b ≤ a / 2이면 r < b ≤ a / 2이고, b > a / 2이면 몫이 1이라 r = a − b < a / 2이기 때문입니다. 그래서 gcd(L, k) 한 번은 O(log k)번의 나눗셈이고, 전체는 O(n log n) 번의 산술 연산입니다(값이 기계어 정수에 들어가는 동안). 저장하는 값은 몇 개뿐이라 추가 공간은 O(1) 입니다. 가장 단순한 방법의 O(답 × n)과 달리 답의 크기와 상관이 없습니다.

정수 범위.

  • 반복 중의 값은 모두 최종 결과 이하입니다. L은 반복마다 커지거나 그대로이고(L(k − 1) | L(k)), 중간값 L / g는 L 이하이며, L / g × k는 그 단계의 결과 L(k)입니다. 그래서 최종 결과가 들어가는 타입이면 중간에 넘치지 않습니다.
  • 곱하기 전에 나눕니다. L × k / g로 쓰면 L × k가 결과보다 최대 k배 커집니다. n = 20에서는 문제가 되지 않지만, 예를 들어 L(43) = 2⁵ × 3³ × 5² × 7 × 11 × 13 × 17 × 19 × 23 × 29 × 31 × 37 × 41 × 43 = 9419588158802421600(약 9.42 × 10¹⁸)에 k = 44를 접어 넣을 때, L × 44는 64비트 부호 없는 정수의 최댓값 2⁶⁴ − 1(약 1.84 × 10¹⁹)을 넘지만 L / 44 × 44 = L은 넘지 않습니다(4와 11이 L을 나누므로 g = 44).
  • C++ 구현은 unsigned long long을 씁니다. 표준이 최소 64비트를 보장합니다. 위 계산대로 44, 45, 46은 새 소수의 거듭제곱이 아니어서 L(46) = L(43)이 64비트에 들어가고, L(47) = 47 × L(46)은 넘칩니다. 그래서 코드 주석에 limit ≤ 46이라고 적었습니다. 부호 없는 정수는 넘치면 정의되지 않은 동작 대신 2ᴺ(N은 타입의 비트 수)으로 나눈 나머지로 돌아가는데, 이는 프로그램이 멈추지 않고 조용히 틀린 값을 낸다는 뜻이므로 범위를 지키는 것이 중요합니다.
  • Python의 int는 정밀도에 제한이 없어 넘칠 일이 없습니다. 다만 /는 부동소수점 결과를 내므로 정수 나눗셈 //를 씁니다. g가 L을 나누므로 //는 정확한 몫입니다.

C++17 구현

#include <iostream>

// Greatest common divisor by Euclid's algorithm: gcd(a, b) = gcd(b, a mod b).
unsigned long long gcd(unsigned long long a, unsigned long long b) {
    while (b != 0) {
        const unsigned long long r = a % b;
        a = b;
        b = r;
    }
    return a;
}

// Least common multiple of two positive numbers. Dividing before multiplying
// keeps every intermediate value no larger than the result.
unsigned long long lcm(unsigned long long a, unsigned long long b) {
    return a / gcd(a, b) * b;
}

// lcm(1, 2, ..., limit), folding in one number at a time. Returns 1 when limit < 2.
// Meant for limit <= 46, where the result fits in 64 bits.
unsigned long long lcm_up_to(unsigned long long limit) {
    unsigned long long result = 1;
    for (unsigned long long k = 2; k <= limit; ++k) {
        result = lcm(result, k);
    }
    return result;
}

// Naive search: the first positive m divisible by every k in 1..limit.
// Used only to cross-check the small example.
unsigned long long smallest_by_search(unsigned long long limit) {
    for (unsigned long long m = 1;; ++m) {
        bool divisible = true;
        for (unsigned long long k = 2; k <= limit; ++k) {
            if (m % k != 0) {
                divisible = false;
                break;
            }
        }
        if (divisible) {
            return m;
        }
    }
}

int main() {
    if (gcd(12, 18) != 6 || gcd(840, 9) != 3 || lcm(4, 6) != 12 ||
        lcm_up_to(1) != 1 || lcm_up_to(10) != 2520 ||
        smallest_by_search(10) != 2520) {
        return 1;
    }
    std::cout << lcm_up_to(20) << '\n';
    return 0;
}

smallest_by_search는 가장 단순한 방법을 그대로 옮긴 함수이고, 공식 예인 n = 10에서 개선한 방법과 같은 값을 내는지 대조하는 데만 씁니다. 작은 예의 확인은 assert가 아니라 if로 했습니다. assert는 NDEBUG가 정의된 빌드에서 아무 일도 하지 않지만, if의 비교와 return 1은 프로그램의 관찰 가능한 동작이라 -O2 최적화로도 없어지지 않습니다. 확인이 하나라도 틀리면 0이 아닌 종료 코드를 냅니다.

Python 3 구현

def gcd(a: int, b: int) -> int:
    """Greatest common divisor by Euclid's algorithm: gcd(a, b) = gcd(b, a mod b)."""
    while b != 0:
        a, b = b, a % b
    return a


def lcm(a: int, b: int) -> int:
    """Least common multiple of two positive numbers.

    Dividing before multiplying keeps every intermediate value no larger than the result.
    """
    return a // gcd(a, b) * b


def lcm_up_to(limit: int) -> int:
    """lcm(1, 2, ..., limit), folding in one number at a time. Returns 1 when limit < 2."""
    result = 1
    for k in range(2, limit + 1):
        result = lcm(result, k)
    return result


def smallest_by_search(limit: int) -> int:
    """Naive search: the first positive m divisible by every k in 1..limit.

    Used only to cross-check the small example.
    """
    m = 1
    while True:
        if all(m % k == 0 for k in range(2, limit + 1)):
            return m
        m += 1


def main() -> None:
    if (
        gcd(12, 18) != 6
        or gcd(840, 9) != 3
        or lcm(4, 6) != 12
        or lcm_up_to(1) != 1
        or lcm_up_to(10) != 2520
        or smallest_by_search(10) != 2520
    ):
        raise SystemExit(1)
    print(lcm_up_to(20))


if __name__ == "__main__":
    main()

C++와 같은 알고리즘, 같은 작은 예입니다. a, b = b, a % b는 오른쪽을 먼저 모두 계산한 뒤 대입하므로 C++의 임시 변수 r과 같은 일을 합니다. range(2, limit + 1)은 끝값을 포함하지 않으므로 하나 더 높여 두었습니다. 작은 예가 틀리면 raise SystemExit(1)로 종료 코드 1을 냅니다. assert는 python -O로 실행하면 제거되므로 쓰지 않았습니다.

정답과 검증

1부터 20까지의 모든 정수로 나누어떨어지는 가장 작은 양의 정수는 232792560입니다.

n = 10의 결과 2520에서 이어서 k = 11, …, 20을 접어 넣으면 다음과 같습니다.

k    g = gcd(L, k)    L
11   1      27720
12   12     27720
13   1      360360
14   14     360360
15   15     360360
16   8      720720
17   1      12252240
18   18     12252240
19   1      위의 정답
20   20     위의 정답

손으로 확인할 수 있는 부분은 다음과 같습니다.

  • 소인수분해. 20 이하의 소수는 2, 3, 5, 7, 11, 13, 17, 19이고, 20 이하에서 가장 큰 거듭제곱은 16 = 2⁴, 9 = 3², 그리고 나머지 소수 자신입니다. 16 × 9 = 144, × 5 = 720, × 7 = 5040, × 11 = 55440, × 13 = 720720, × 17 = 12252240, × 19 = 위의 정답입니다.
  • 모두로 나누어떨어짐. 20 이하의 k를 소인수분해하면 각 소수 p의 거듭제곱 pᵉ는 k 이하, 따라서 20 이하이므로 위의 지수를 넘지 않습니다. 그래서 k는 정답을 나눕니다.
  • 가장 작음. 16, 9, 5, 7, 11, 13, 17, 19 중 어느 하나라도 지수를 하나 줄이면 그 수가 나누지 못하게 됩니다. 예를 들어 2⁴를 2³으로 줄이면 16이 나누지 못합니다.
  • 2520과의 관계. 위의 정답 = 2520 × 92378이고 92378 = 2 × 11 × 13 × 17 × 19입니다. n = 10에서 n = 20으로 가며 늘어난 것은 16을 위한 2 하나와 새 소수 네 개뿐입니다.

가장 단순한 방법이 이 값에 이르려면 후보를 그만큼 시험해야 한다는 것도 여기서 알 수 있습니다. 실행 시간은 이 글에서 재지 않았습니다.

검증은 세 겹입니다.

  1. 작은 예. 두 프로그램 모두 답을 출력하기 전에 gcd(12, 18) = 6, gcd(840, 9) = 3, lcm(4, 6) = 12, L(1) = 1, L(10) = 2520을 확인하고, 가장 단순한 방법으로 n = 10을 직접 찾아 2520과 같은지도 확인합니다. 하나라도 다르면 0이 아닌 종료 코드로 끝납니다. n = 10의 과정은 “풀이 유도”에서 손으로도 따라가 보았습니다.
  2. 두 구현의 일치. 이 시리즈의 저장소에서는 검사 스크립트가 C++ 코드를 g++ -std=c++17 -O2 -Wall -Wextra -Werror로 컴파일해 실행하고(경고도 실패로 처리), Python 코드를 python3 -I -B로 실행합니다. 두 출력이 저장해 둔 answer.txt와 바이트 단위로 같아야 하고, 0이 아닌 종료 코드나 시간 초과(C++ 1초, Python 10초)는 실패입니다. 시리즈 규칙상 이 검사를 통과한 코드만 글에 싣습니다.
  3. 증명. 반복적으로 최소공배수를 접어 넣으면 정확히 L(n)이 된다는 것은 “정확성 증명”에서 보였습니다.

경계 사례

  • n = 1: 반복이 한 번도 돌지 않고 1을 돌려줍니다. 1은 1로 나누어떨어지는 가장 작은 양의 정수이므로 맞습니다. 작은 예에 L(1) = 1이 들어 있습니다. n = 0은 문제에 없지만 같은 이유로 1을 돌려주며, 빈 집합의 최소공배수를 1로 두는 관례와 맞습니다.
  • 이미 L의 약수인 k(6, 10, 12, 14, 15, 18, 20): g = gcd(L, k) = k가 되어 L / k × k = L로 그대로입니다.
  • 새 소수 k(7, 11, 13, 17, 19): L의 소인수는 모두 k보다 작으므로 g = 1이고 L은 k배가 됩니다.
  • 소수의 거듭제곱 k(4, 8, 9, 16): 지수가 하나만 늘어납니다. 예를 들어 16에서는 g = 8이라 L은 2배만 됩니다.
  • 곱한 뒤 나누는 실수: 위 “정수 범위”처럼 n이 커지면 L × k가 넘칠 수 있습니다. 나누기를 먼저 하면 중간값이 결과를 넘지 않습니다.

더 생각해 볼 것

  • 소수의 거듭제곱으로 바로 계산하기. 정답 확인에서 쓴 대로, L(n)은 n 이하의 각 소수 p에 대해 n을 넘지 않는 가장 큰 pᵉ를 모두 곱한 것입니다. 에라토스테네스의 체로 소수를 구해 이 곱을 만들도록 바꿔 보세요. gcd를 n − 1번 부르는 방법과 비교하면 어느 쪽이 연산이 적을까요? 소수를 다루는 방법은 이 시리즈의 7번과 10번 문제에서 다시 나옵니다.
  • 순서를 바꿔도 될까? 3단계의 논리는 “1부터 k까지”라는 순서에 기대지 않습니다. 수들을 어떤 순서로 접어 넣어도 같은 최소공배수가 나온다는 것을 같은 방식으로 보여 보세요.
  • 베주 계수 구하기. 호제법을 확장하면 gcd(a, b) = s × a + t × b의 s, t도 함께 구할 수 있습니다. 840과 9에 대해 직접 해 보세요.
  • 최대공약수와 최소공배수의 곱. 4단계에서 gcd(a, b) × lcm(a, b) = a × b가 바로 나옵니다. 세 수 이상에서도 같은 관계가 성립할까요? 작은 반례를 찾아보세요.

출처와 라이선스

  • Project Euler, Problem 5 — 원래 문제. 이 글의 “문제” 절은 그 내용을 직접 요약한 것이고, 원문과 같은 CC BY-NC-SA 4.0으로 공유합니다. 문제의 저작자 표시는 Project Euler입니다.
  • Project Euler, Copyright — 문제 본문의 라이선스와 링크 방식. 비영리 조건에 따라 이 글에는 광고를 싣지 않습니다(ads: false).
  • C++ 정수 타입의 최소 폭과 부호 없는 정수의 산술은 C++ 표준 작업 초안 [basic.fundamental], assert와 NDEBUG는 [assertions.assert]를 따랐습니다. Python의 동시 대입은 Assignment statements, assert와 -O는 The assert statement, int의 정밀도와 /·//의 결과는 Numeric Types를 따랐습니다.
  • 풀이, 증명, 코드는 직접 쓴 것입니다. 유클리드 호제법과 최소공배수의 성질은 위에서 직접 증명했습니다. Project Euler의 해설(overview), 포럼, 회원 전용 자료나 다른 풀이 사이트는 참고하지 않았습니다. Motitory는 Project Euler와 제휴나 후원 관계가 없고, Project Euler가 이 글을 검토하지도 않았습니다.

블로그로