Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

파이썬으로 n의 진약수가 짝수 완전제곱수일 확률 구하기

숫자 n이 주어졌을 때, n의 진약수(proper divisor) 중 하나를 골랐을 때 그 수가 짝수이면서 동시에 완전제곱수일 확률을 구하는 문제입니다.

예를 들어 n = 36이라면 답은 1/8입니다. 36의 진약수는 {1, 2, 3, 4, 6, 9, 12, 18}로 총 8개이고, 이 가운데 짝수이면서 완전제곱수인 수는 4 하나뿐이기 때문입니다.

접근 방법

이 문제의 핵심 아이디어는 다음과 같습니다.

  • 모든 짝수 완전제곱수는 반드시 4의 배수입니다. 따라서 n이 4로 나누어떨어지지 않으면 n의 어떤 약수도 조건을 만족할 수 없으므로 바로 0을 반환하면 됩니다.
  • n을 소인수분해하여 각 소수의 지수를 구합니다.
  • 어떤 약수가 완전제곱수가 되려면 모든 소수의 지수가 짝수여야 하고, 짝수이려면 2의 지수가 2 이상의 짝수여야 합니다.
  • 전체 약수 개수에서 1(n 자기 자신)을 빼면 진약수의 개수가 됩니다. 조건을 만족하는 경우의 수도 마찬가지로, n 자신이 완전제곱수라면 하나를 빼주어야 합니다.
  • 마지막으로 두 수의 최대공약수(gcd)로 나누어 기약분수 형태로 출력합니다.

알고리즘 단계

  1. n mod 4 ≠ 0이면 0을 반환합니다.
  2. nc ← n, ptr ← 2로 초기화하고 빈 리스트 l을 생성합니다.
  3. ptr ≤ √nc인 동안 다음을 반복합니다.
    • a ← 0으로 초기화합니다.
    • nc가 ptr로 나누어떨어지는 동안 a를 1씩 증가시키고, nc ← ⌊nc / ptr⌋로 갱신합니다.
    • a > 0이면 a를 리스트 l에 추가합니다.
    • ptr을 1 증가시킵니다.
  4. 반복이 끝난 뒤 nc > 1이면 1을 l에 추가합니다(마지막으로 남은 소인수 처리).
  5. k ← l[0](소수 2의 지수), d ← k + 1, no ← ⌊k / 2⌋로 설정합니다.
  6. l의 두 번째 원소부터 끝까지 각 i에 대해 d ← d × (i + 1), no ← no × (⌊i / 2⌋ + 1)로 갱신합니다.
  7. d ← d − 1로 만들어 전체 진약수의 개수를 구합니다.
  8. n이 완전제곱수라면 no ← no − 1로 n 자기 자신을 제외합니다.
  9. g ← gcd(d, no)를 구한 뒤 d와 no를 g로 나누어 기약분수로 만듭니다.
  10. no = 0이면 0을 반환하고, 그렇지 않으면 "no/d" 형태의 문자열을 반환합니다.

파이썬 구현 예시

아래 코드를 통해 실제 구현 방법을 확인해 보겠습니다.

from math import gcd

def solve(n):
    # 짝수 완전제곱수는 반드시 4의 배수이므로,
    # n이 4의 배수가 아니면 조건을 만족하는 약수가 없음
    if n % 4 != 0:
        return 0

    nc = n
    ptr = 2
    l = []                      # 소인수들의 지수를 저장할 리스트

    while ptr <= nc ** 0.5:     # 소인수분해 수행
        a = 0
        while nc % ptr == 0:
            a += 1
            nc //= ptr
        if a > 0:
            l.append(a)
        ptr += 1

    if nc > 1:                  # 남은 소인수 처리
        l.append(1)

    k = l[0]                    # 소수 2의 지수
    d = k + 1                   # 약수 개수 계산용
    no = int(k / 2)             # 2의 지수가 2 이상의 짝수인 경우의 수

    for i in l[1:]:
        d = d * (i + 1)
        no *= int(i / 2) + 1    # 지수가 짝수인 경우의 수 누적

    d = d - 1                   # n 자기 자신 제외 → 진약수 개수

    if int(n ** 0.5) ** 2 == n: # n이 완전제곱수면 자기 자신 제외
        no -= 1

    g = gcd(d, no)              # 최대공약수로 약분
    d = d // g
    no = no // g

    if no == 0:
        return 0
    else:
        return str(no) + '/' + str(d)

n = 36
print(solve(n))

n = 36일 때 동작 과정 살펴보기

  • 36 = 2² × 3²이므로 소인수 지수 리스트는 l = [2, 2]가 됩니다.
  • 초기값: k = 2, d = 3, no = ⌊2/2⌋ = 1
  • 두 번째 지수 2에 대해: d = 3 × 3 = 9, no = 1 × (⌊2/2⌋ + 1) = 2
  • d = 9 − 1 = 8 → 진약수는 총 8개
  • 36은 완전제곱수이므로 no = 2 − 1 = 1 → 36 자기 자신은 제외됩니다.
  • gcd(8, 1) = 1이므로 약분 없이 그대로 1/8을 반환합니다.

실행 결과

입력:

n = 36

출력:

1/8

이처럼 소인수분해와 약수 개수 공식을 활용하면, 일일이 모든 약수를 나열하지 않고도 O(√n) 시간 안에 확률을 효율적으로 계산할 수 있습니다.