카테고리 없음

(정수론/파이썬) 완전수, 부족수, 초과수 세기

미친토끼 2021. 9. 15. 19:13
# 연습문제 15.7.b
# sigma(n) = 2n을 만족하는 n을 완전수라 한다.
# sigma(n) > 2n 이라면 초과수, sigma(n) < 2n 이라면 부족수라 하자.
# 2와 100사이에 얼마나 많은 완전수, 부족수, 초과수가 존재하는지 세어보라.
# 100과 200사이에서도 초과수와 부족수를 찾아보아라.


# 소수 판정 함수. 소수면 True, 아니면 False 반환
def isPrime(num):
  root_val = round(num ** 0.5) # 제곱근 값을 구한다
  for i in range(2, root_val+1): # 제곱근값+1 까지만 약수를 조사해도 충분하다
    if num % i == 0:  # 중간에 나눠지면 소수가 아니다
      return False
  return True
  

# 소인수분해 하는 함수  
def factorize2(value):
  num = value  # 원래값을 보존한다.
  divisor_primes = [] # 분해된 인수를 차곡차곡 저장하는 리스트
  factorize_dict = {} # 리스트의 원소 갯수를 세어 넣는 용도
  
  root_val = int(num ** 0.5)+1
  i = 2
  found = False
  while True:
      # 소수 검증 때처럼 num까지 모두 나누어볼 필요는 없고 제곱근값 까지만 나누어보면 된다.
      if i >= root_val: break
    
      # i가 소수인지 검사해서 소수라면 num을 i로 나눈다
      # i로 나눠서 나머지가 0이라면, 인수분해가 한번 된 것이다.
      if isPrime(i) and num % i == 0:
          divisor_primes.append(i) # 나누는 수를 저장
          num = num // i   # 몫, 가령 24//2 = 12
          root_val = int(num ** 0.5)+1
          if found == False:
            found = True  # 찾았다는 표시를 함
          # 12는 소수가 아니므로 반복문 계속. 소수가 나오면 그 소수를 저장하고 반복문 종료.
          if isPrime(num):
              divisor_primes.append(num)
              break  # return divisor_primes
      else:
          i += 1 # 인수분해가 되지 않으면 젯수(나누는 수)를 1증가
  
  for n in divisor_primes:
      if n in factorize_dict:
          factorize_dict[n] += 1   
      else:  
          factorize_dict[n] = 1      
  
  if found == False:  # 소인수를 못 찾았다면 소수이므로
      factorize_dict[value] = 1
  
  return factorize_dict
  
  
  # 약수 구하는 함수: 향상된 버전
# 소인수분해 결과를 가지고 약수를 만듦
# 숫자가 클 경우, 속도 면에서 단순한 방식(버전1)보다 훨씬 빠름
# 50234902430546664의 약수를 구했을 때 버전2가 1초 걸리고, 버전1이 35초 걸림.
def getDivisor2(num):
  divisors = []
  res = factorize2(num) # 예) {2:3, 3:2, 7:1} 형태
  
  for key, value in res.items():
    divisor = []
    for i in range(value+1):
      divisor.append(key**i)
    divisors.append(divisor)
  # divisors : [[2**0, 2**1, 2**2, 2**3], [3**0, 3**1, 3**2], [7**0, 7**1]]
  # divisors : [[1, 2, 4, 8], [1, 3, 9], [1, 7]]
  
  while True:
    tmp = [] # 리스트 2개 원소를 서로 곱해서 약수를 만들어 임시 저장

    # 리스트를 하나씩 줄여나가는데 리스트가 하나만 남았으면 그것이 약수 집합이므로 종료한다
    if len(divisors) < 2:
      break
    # 리스트 2개의 각 원소를 서로 곱해서 약수를 만들어 저장
    for n in divisors[0]:  
      for m in divisors[1]:
        tmp.append(n * m)
        
    # divisors 안의 리스트는 예에서 3개이고, 리스트[0]과 [1]을 곱해서 약수를 만들고(tmp),
    # 기존 리스트[0]과 리스트 [1]을 삭제하고 1차 약수 리스트인 tmp를 리스트[0] 자리에 끼워넣는다.
    # 한번 실행을 거치면 이제 리스트 목록은 총 2개가 되었다.
    del divisors[0] # 원래 divisors[0] 삭제
    del divisors[0] # 원래 divisors[1] 삭제
    divisors.insert(0, tmp) # 생성된 tmp를 divisors 제일 앞에 끼워넣기
    
  return sorted(divisors[0]) # 리스트의 리스트이므로 바깥 대괄호를 제거하고 정렬해서 반환


# 시그마 함수: 자신을 포함한 모든 약수의 총합을 돌려주는 함수
# 예) 12의 시그마 함수값: 1, 2, 3, 4, 6, 12 을 모두 더한 값
# '친절한 수론 길라잡이' 105~106에서 소개
def sigma_fun(num):
  divisors = getDivisor2(num) #   # 소인수분해 방식으로 약수를 모두 구한다
  total = 0   # 합 초기화
  for d in divisors: # 약수들을 모두 합한다
    total += d
  return total


over_count = 0   # 초과수 개수
under_count = 0 # 부족수 개수
perfect_count = 0  # 완전수 개수

for n in range(2, 101):  # 2~100
  res = sigma_fun(n)
  if res < 2*n:  # 부족수
    under_count += 1
  elif res > 2*n: # 초과수
    over_count += 1
  else:
    perfect_count += 1
                                      # 2~100   # (101~200) # (201~300)
print(f"완전수 개수: {perfect_count}")  # 2      # 0         # 0  
print(f"초과수 개수: {over_count}")     # 22     # 24        # 23
print(f"부족수 개수: {under_count}")    # 75     # 76        # 77