카테고리 없음

(정수론/파이썬) 최소공배수 구하는 두 가지 방법

미친토끼 2021. 9. 3. 11:57
# 최소공배수 구하는 두 가지 방법
# 관련 문헌: '친절한 수론 길라잡이' 35쪽 연습문제 5.4

# 관련함수
# my_lcm(a, b): 각기 a*b 사이의 배수를 구해서 교집합(공배수)을 구한 다음, 가장 작은 수를 돌려준다
# my_lcd(a, b): 각기 약수들을 찾아서 교집합(공약수)을 구한 다음, 그중 가장 작은 수를 돌려준다.
#              약수가 없을 경우, 모든 수의 공통 약수인 1을 돌려준다
# my_lcm2(a, b): 공약수를 찾아서 그것으로 a, b를 계속 나눈다. 공약수가 나오지 않을 때까지.
#                공약수가 없으면(서로소이면), 그때까지의 모든 나누는 수와 마지막 몫 두 개를 곱해서 돌려준다(최소공배수)

# 유클리드 호제법으로 두 수 a와 b (단 a > b) 의 최대공약수 구하기
def my_gcd(a, b):

    if a == b:
        return a
    if a < b:  # a > b 조건을 맞추기 위해 두 값을 맞바꾸기
        tmp = a
        a = b
        b = tmp
    
    r0 = a
    r1 = b
    for i in range(b): # 두 수 증 작은 수만큼 최대 반복
        r2 = r0 % r1 # 몫은 필요 없으므로 나머지만 구함
        
        if r2 == 0: # 나머지가 0이라면
            return r1 # 직전의 나머지가 최대공약수임
        # 값들을 한칸씩 shift 시켜서 다시 반복
        r0 = r1
        r1 = r2
        
# 약수 구하는 함수
def getDivisor(num):
  divisors = []
  root_num = round(num ** 0.5)
  # sqrt(num)까지만 조사하면 된다. 단, round로 하면 소수점 이하는 버리기 때문에 
  # 아래 range에서 돌릴 때는 +1을 해줘야 한다.
  
  for i in range(1, root_num+1):
    if num % i == 0:  # 나눠지면
      divisors.append(i)  # 제수(나누는 수)가 약수가 되고
      divisors.append(num // i)  # 그 몫도 약수가 된다

  # 구한 것을 set 함수를 통해 중복을 제거하고 다시 리스트로 바꾼 다음, 정렬해서 리턴한다.
  return sorted(list(set(divisors))) 


# 최소공배수 구하기
# my_lcm(a, b)라고 할 경우, 최소공배수는 a*b와 같거나 작다. a, b 가 서로소이면 최소공배수는
# a*b 이고, 공약수가 있을 경우, lcm(a, b) < a*b 이다.
# a의 배수를 a*b 이하에서 찾아서 나열하고, b의 약수를 a*b 이하에서 찾아서 나열해서,
# 교집합을 구하면 그것이 a*b 이하 공배수들인데, 그중에서 가장 작은 수가 최소공배수이다.
def my_lcm(a, b):
  ab = a * b
  a_multi = []
  for i in range(1, b+1):
    a_multi.append(a * i)
    
  b_multi = []
  for i in range(1, a+1):
    b_multi.append(b * i)
    
  common = set(a_multi) & set(b_multi) # 배수들의 교집합 즉, 공배수 찾기
  return min(list(common))  # 리스트형으로 바꿔 최소값을 리턴한다.
  

# 최소공약수를 구하는 함수 (1 제외)
def my_lcd(a, b):
  a_ = getDivisor(a)  # a의 약수를 모두 구한다
  b_ = getDivisor(b)  # b의 약수를 모두 구한다
  #print(a_)
  #print(b_)
  c_ = set(a_) & set(b_) # 두 약수들의 교집합을 구한다. 공약수들이 된다.
  
  if len(c_) == 1:  # 공약수가 1뿐이라면, 1을 반환: 최소공약수
    return min(c_)
  elif len(c_) > 1: # 공약수가 1외에도 있다면 1을 제거하고 가장 작은 수를 반환한다: 최소공약수
    return min(list(c_ - {1}))


# 최소공배수 구하기
# a, b 의 최소공배수로 나눠서 두 수가 서로소일 때까지 나누고, 
# 그 제수들(나누는 수)와 최종 몫들을 모두 곱하면 최소공배수가 된다. 중학교 1학년때 배우는...
def my_lcm2(a, b):
  if my_gcd(a, b) == 1: # a와 b가 서로소라면, a*b를 최소공배수로 반환
    return a * b

  # 이제 a, b의 공약수가 있다는 말이므로, 최소공약수를 구해서 a, b를 나눈다.
  divisors = []
  while True:
    lcd = my_lcd(a, b)  # 최소공약수를 구한다.

    if lcd != 1: # 최소공약수가 2 이상이라면
      a = a / lcd  # 그 수로 나눠서 몫을 새로 a라고 놓는다
      b = b / lcd  # 그 수로 나눠서 몫을 새로 b라고 놓는다
      divisors.append(lcd)  # 나누는 수(최소공약수)를 배열에 저장한다.
    else:                 # 최소공약수가 1이라면 더 이상 나눌 수 없으므로
      divisors.append(a)  # 최후의 몫들을 배열에 저장한다.
      divisors.append(b)
      total = 1
      for n in divisors: # 배열의 모든 원소들을 곱하여 리턴한다
        total *= n
      return int(total)
    
  
print (my_lcm(8, 12))
print (my_lcm(3, 7))
print (my_lcm(12, 66))
print (my_lcm(20, 30))
print (my_lcm(51, 68))
print (my_lcm(23, 18))
print(my_lcm(32383800, 8055))

print("")
print (my_lcm2(8, 12))
print (my_lcm2(3, 7))
print (my_lcm2(12, 66))
print (my_lcm2(20, 30))
print (my_lcm2(51, 68))
print (my_lcm2(23, 18))
print(my_lcm2(32383800, 8055))

결과:

24
21
132
60
204
414
5796700200

24
21
132
60
204
414
5796700200