Project Euler

프로젝트 오일러 037

프로젝트 오일러 37번, 양쪽에서 숫자를 제거해도 계속 소수인 '양면 소수' 11개의 합을 구합니다. 양면 소수의 숫자 구성 규칙을 알아보고, 소수 판별과 메모이제이션을 활용한 파이썬 풀이를 제시합니다.

2분
#project euler #python

이 문제는 35번 원형 소수 문제와 비슷해보이지만 실제로는 상당히 다른 문제입니다. 우선 가장 큰 차이로 찾아야 하는 소수의 개수는 주어졌지만, 그 범위가 주어지지 않았습니다. 따라서 소수체를 사용할 수 없으니 범위를 좁히는 것에 상당한 노력을 기울여야 합니다.

한자리씩 없애 나가기

‘1234’가 있을 때 이를 10, 100, 1000으로 나눈 몫과 나머지를 나열하면 [123, 4, 12, 34, 1, 234]가 됩니다. 여기에 원래 수를 추가하면 [1, 12, 123, 1234, 234, 34, 4]으로 원래 수와 양쪽을 절단한 수의 목록이 됩니다.

이 원리를 이용하여 정수 n을 양쪽으로 절단한 수의 모음을 만들 수 있습니다.

def parts(n: int) -> list[int]:
    l = int(log10(n)) + 1
    res = [divmod(n, 10**i) for i in range(1, l)]
    res = [n] + list(sum(res, ()))
    return [] if any(x > 3 and (x % 2 == 0 or x % 3 == 0) for x in res) else res

여기서 2, 3이 아닌 짝수나 3의 배수가 포함되어 있으면 빈 배열을 리턴하도록 했습니다. 이렇게하면 불필요하게 소수검사를 수행하는 횟수를 줄일 수 있기 때문입니다.

범위를 줄이기

그 외에 한쪽을 절단한 값이 소수가 되지 않는 경우를 생각해봅시다. 조건을 만족하는 수는 그 자체가 소수이므로 짝수이거나 5의 배수가 되지 않아야 하고 1의 자리에는 한 자리 소수가 와야 합니다. 그러니 1의 자리가 0, 1, 2, 4, 5, 6, 8, 9는 올 수 없습니다. 즉 1의 자리에는 3과 7만 올 수 있습니다. 20이하의 수에서는 이 조건을 만족하는 수가 없기에 23이 조건을 만족하는 가장 작은 수입니다. 이로부터 23, 27, 34, 37… 이렇게 +4, +6을 반복하여 더하면서 검사하면 불필요한 대상을 제외할 수 있습니다.

소수 판별 검사 역시 결과를 캐시해두면 도움이 될 겁니다. (실험해보면 의외로 소수 판정 검사의 캐싱은 5%정도의 성능향상을 보입니다.) 가장 성능에 큰 영향을 주는 부분은 3, 7로 끝나는 수만 검사하는 가드입니다.

def main():
    s = set()
    k = 23
    while len(s) < 11:
        for i in (4, 6):
            xs = parts(k)
            if xs and all(isprime(x) for x in xs):
                s.add(k)
            k += i
    print(sum(s), s)
print(main())