(정수론/파이썬) 수론 관련 함수 모음 (number_util.py)
정수론 함수 목록 2021-9-25 기준
isPrime(num): 소수인지 판정
isPrime2(num): 큰수에 대해 페르마 정리와 카마이클수를 이용해 소수 판정
isPrimeWilson(n): 차례곱을 이용해 소수 판정. 숫자가 조금만 커져도 시간이 많이 걸림.
isPrimeGuess(n): 큰수에 대해 페르마 정리와 카마이클수를 이용해 소수 판정
gcd(a, b): 유클리드 호제법으로 최대공약수 구하기
gcd2(a, b): 최대공약수 정의대로 삽질로 구하는 함수
lcm(a, b): 최소공배수 구하기. 각기 배수들의 교집합에서 제일 작은 수
lcd(a, b): 최소공약수. 교집합 방식으로
lcm2(a, b): 최소공배수. 중학교 1학년 때 배운 식대로
getSquarePrime(): N^2 + 1 형태 소수 구하기
findTwinPrimes(): 쌍둥이 소수 구하기
factorize(num): 소인수분해. 구버전
factorize2(value): 소인수분해. 개선 버전
factorize_list(num): 소인수분해 결과를 목록으로. 지수는 빼고 [2, 3, 7]
goldbach(num): 골드바흐의 추정 검증하기. 4 이상의 모든 짝수는 소수 2개의 합으로 표현된다.
linear_equation_compute(a, b, c): 선형방정식 해 구하기. 내 방식대로.
linear_equation(a, b): 선형방정식 해 구하기, 책에 나온 대로
congruence_equation(a, c, m): 합동방정식 해 구하기
congruence_equation_poly(poly, m): 다항식 합동식의 해를 구한다
dirichlet(a, m, max_num): 디리클레의 등차수열 정리를 확인
euler_phi(num): 서로소인 숫자를 찾아 그 갯수를 반환하는 함수
euler_phi_list(num): 서로소인 숫자를 찾아 그 목록을 반환하는 함수
euler_phi2(m): 공식을 이용해 오일러 파이값을 구하는 함수
euler_phi3(num): 인수분해하여 오일러 파이값을 구하는 함수
crt(b, m, c, n): 중국인의 나머지 정리
crt2(*args): 중국인의 나머지 정리. 가변적인 개수의 인자를 받는다.
getDivisor(num): 일일이 나눠보는 방식으로 약수 구하는 함수, 속도가 느림
getDivisor2(num): 소인수분해를 해서 약수 구하는 함수: 향상된 버전
divisorsCount(num): 약수의 개수만 구하는 함수. 소인수들의 지수 이용
sigma_fun(num): 시그마 함수: 자신을 포함한 모든 약수의 총합을 돌려주는 함수:
예) 12의 시그마 함수값: 1, 2, 3, 4, 6, 12 을 모두 더한 값
isPerfectNumber(num): 자신을 제외한 약수들을 모두 더해서(진약수 합) 돌려준다
get_mersennePrime(): 메르센 소수 구하기, 2^N - 1 = 소수..인 경우 구하기
euclidPerfectNumber(): 유클리드 완전수 공식을 이용해 완전수 찾기
isProductPerfect(num): 곱셈완전수인지 판정하는 함수
isAmicablePair(m, n): 두 수가 친화수인지 확인
isSociableNumbers(numbers, info=False): 숫자 리스트가 사교수인지 확인
getSociableNumbers(start, end, max_try=30): 사교수의 범위를 정해주면 사교수를 찾는다
successiveSquaring(a, k, m): 연속제곱법. 진행 과정을 그대로 구현. 메시지 다수 출력
successiveSquaring2(a, k, m): 연속제곱법. 향상된 버전. 책에서 나온 것.
my_successiveSquaring(a, k, m): 연속제곱법 : 쑤튜브에서 힌트 얻어서 작성
high_congruence_equation(k, b, m): 고차합동방정식 푼다. phi(m)구하기, 일차 합동식 풀기, 연속제곱법 사용해서 합동식 풀기 등 3가지 기법 사용
cipher_book: RSA 암호에 사용하는 암호 딕셔너리
RSA_incode(msg, m, k): RSA 암호화 알고리즘
RSA_decode(ciphered_list, m, k, p, q): RSA 복호화 알고리즘
isCarmichael(m): 카마이클 수 판정. 확인 위해 일일이 나눠보는 함수
isCarmichael2(n): 카마이클 수 판정. 확인 위해 소인수들만 나눠보는 함수
compositeWitness(m): 카마이클 수 관련, m이 합성수라는 증인 목록을 만들어준다
# '친절한 수론 길라잡이' 공부를 하면서 만든 함수들
# 파일명: number_util.py
# 이 파일을 같은 폴더에 넣고, 호출할 때 from number_util import * 로 불러옴
import math
import random
# 소수 판정 함수. 소수면 True, 아니면 False 반환
def isPrime(num):
if num <= 1: return False
if num == 2 or num == 3: return True
if num % 2 == 0: return False # 짝수라면 합성수
root_val = round(num ** 0.5) + 1 # 제곱근 값을 구한다
# 이미 2를 제외했으므로 3부터 2씩 증가시키며 홀수로만 나눈다.
for i in range(3, root_val, 2): # num이 5인 경우부터 시작한다
if num % i == 0: # 중간에 나눠지면 소수가 아니다
return False
return True
# 연습문제 16.4 : 큰수에 대해 페르마 정리와 카마이클수를 이용해 소수임을 판정
# True: 소수일 확률이 높음, False: 합성수
def isPrime2(n):
MAX_NUM = 10
#n = 243034209889348945908458903908324908449804908434389747894389438943
# 2<= a < n, 10개의 수를 고른다.
a_s = []
for i in range(MAX_NUM):
a = random.randrange(2, n)
#if gcd(a, n) != 1: continue # 큰수에 대해서는 gcd를 구하는 행위 자체가 엄청난 부담
a_s.append(a) # 2 ~ n - 1
#print(a_s)
maybe_prime = True
for i in range(MAX_NUM): # 숫자 10개를 채택해서 페르마 정리를 이용해 n을 검사함
if successiveSquaring2(a_s[i], n-1, n) != 1:
maybe_prime = False # composite number
break
#if maybe_prime:
# print(f"{n} 은 아마도 소수")
#else:
# print(f"{n} 은 합성수")
return maybe_prime
# Wilson's Theorem
# p is prime if (p-1)! = -1 (mod p)
def isPrimeWilson(n):
m = n
gop = 1
while m >= 3:
gop = (gop * (m - 1)) % n
m -= 1
return gop == n - 1
# 큰수가 소수인지 아닌지 판정한다. 페르마 소정리를 이용.
# '아마도 소수'에는 아주 가끔 합성수가 끼어 있는 경우가 있다.
def isPrimeGuess(n):
MAX_NUM = 1000 # 난수를 이 수치만큼 발생
# 2와 N - 1 사이에서 10개의 수를 고른다.
# n과 서로소인 수를 고르는 게 좋겠지만 그 자체에 자원이 꽤 소모되므로 단순한 방식으로.
a_s = []
for i in range(MAX_NUM):
a = random.randrange(2, n)
if gcd(a, n) != 1: continue
a_s.append(a) # 2 ~ n - 1
#print(a_s) # 생성된 10개의 난수
maybe_prime = True
for i in range(MAX_NUM):
if successiveSquaring2(a_s[i], n-1, n) != 1: # ai(n-1) mod n 계산
maybe_prime = False # composite number
break
if maybe_prime:
print(f"{n} 은 아마도 소수")
else:
print(f"{n} 은 합성수")
# 두 수 a와 b (단 a > b) 의 최대공약수 구하기: 유클리드 호제법
def 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 gcd2(a, b):
a_set = getDivisor(a) # a의 약수를 모두 구한다
b_set = getDivisor(b) # b의 약수를 모두 구한다
#print(a_set)
#print(b_set)
c_set = set(a_set) & set(b_set) # 두 약수들의 교집합을 구한다. 공약수들이 된다.
return max(list(c_set)) # 리스트로 바꾸어 최대값을 구한다.
# 최소공배수 구하기
# lcm(a, b)라고 할 경우, 최소공배수는 a*b와 같거나 작다. a, b 가 서로소이면 최소공배수는
# a*b 이고, 공약수가 있을 경우, lcm(a, b) < a*b 이다.
# a의 배수를 a*b 이하에서 찾아서 나열하고, b의 약수를 a*b 이하에서 찾아서 나열해서,
# 교집합을 구하면 그것이 a*b 이하 공배수들인데, 그중에서 가장 작은 수가 최소공배수이다.
def 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 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 lcm2(a, b):
if gcd(a, b) == 1: # a와 b가 서로소라면, a*b를 최소공배수로 반환
return a * b
# 이제 a, b의 공약수가 있다는 말이므로, 최소공약수를 구해서 a, b를 나눈다.
divisors = []
while True:
lcd_val = lcd(a, b) # 최소공약수를 구한다.
if lcd_val != 1: # 최소공약수가 2 이상이라면
a = a / lcd_val # 그 수로 나눠서 몫을 새로 a라고 놓는다
b = b / lcd_val # 그 수로 나눠서 몫을 새로 b라고 놓는다
divisors.append(lcd_val) # 나누는 수(최소공약수)를 배열에 저장한다.
else: # 최소공약수가 1이라면 더 이상 나눌 수 없으므로
divisors.append(a) # 최후의 몫들을 배열에 저장한다.
divisors.append(b)
total = 1
for n in divisors: # 배열의 모든 원소들을 곱하여 리턴한다
total *= n
return int(total)
#print(lcm(8, 12))
#print(lcm2(32383800, 8055))
# N^2 + 1 이 소수인 경우가 무한할까? (N = 1, 2, 3...)
# N^2 + 1 형태의 소수. N = 1, 2, 3...을 대입하여 얻은 N2 + 1 형태의 수를 나열해보면,
# 몇몇 소수가 발견된다. N이 홀수이면 N^2 + 1은 짝수이므로 N=1일 때를 제외하고는 N^2 + 1이
# 소수일 리 없다. N에 짝수를 대입하는 경우만 생각해보자.
def getSquarePrime():
i = 2 # 2부터 2씩 증가시켜 대입한다
count = 0 # 소수 등장 횟수
while True:
num = i ** 2 + 1
if isPrime(num):
count += 1
print(f"{i} ^2 + 1 = {num} ({count})")
i += 2 # 홀수에는 관심없고 짝수만 취급한다.
#getSquarePrime()
# 쌍둥이 소수: 인접한 두 수(차이가 2)가 소수인 경우를 찾는다
def findTwinPrimes():
i = 5 # 현재 수
prev_prime = 3 # 이전에 등장한 소수
count = 0 # 쌍둥이 소수 등장 횟수
while True:
if isPrime(i): # 현재 수가 소수라면
if i - prev_prime == 2: # 이전의 소수(홀수)가 인접한 소수(홀수)라면
count += 1
print(f"({prev_prime}, {i}) {count}")
prev_prime = i
i += 2 # 짝수에 대해서는 굳이 소수 여부 조사를 할 필요가 없음. 홀수에 대해서만 조사함
#findTwinPrimes()
# 소인수분해 해주는 함수. 구형 버전. 속도가 다소 느림.
# 가령 인수분해 결과가 2^3 * 3^2 * 7 이라면
# {2:3, 3:2, 7:1} 반환
def factorize(num):
divisor_primes = [] # 분해된 인수를 차곡차곡 저장하는 리스트
factorize_dict = {} # 리스트의 원소 갯수를 세어 넣는 용도
# num이 소수라면 (num, 1)을 딕셔너리에 넣어 반환한다.
# 소수 판정은 숫자의 제곱근까지만 조사하면 되므로 간단하다.
if isPrime(num):
factorize_dict[num] = 1
return factorize_dict
# 인수분해 루틴은 소수 판정 루틴과는 조금 다르게 접근할 필요가 있다.
i = 2
while True:
# i가 소수인지 검사해서 소수라면 num을 i로 나눈다
# i로 나눠서 나머지가 0이라면, 인수분해가 한번 된 것이다.
if isPrime(i) and num % i == 0:
divisor_primes.append(i) # 나누는 수를 저장
num = num // i # 몫, 가령 24//2 = 12
# 12는 소수가 아니므로 반복문 계속. 소수가 나오면 그 소수를 저장하고 반복문 종료.
if isPrime(num):
divisor_primes.append(num)
break
i = 2 # 인수분해를 한번 했으므로 숫자를 다시 2부터 나눠보기로 설정
else:
i += 1 # 인수분해가 되지 않으면 젯수(나누는 수)를 1증가
for n in divisor_primes:
if n in factorize_dict:
factorize_dict[n] += 1
else:
factorize_dict[n] = 1
return factorize_dict
# 소인수분해 해주는 함수, 개선 버전
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
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
# 소인수분해하여 그 목록을 돌려준다
# 가령 소인수분해 결과가 2^3*3^2*7 이라면,
# [2, 3, 7]을 돌려준다
def factorize_list(num):
divisor_primes = [] # 분해된 인수를 차곡차곡 저장하는 리스트
if isPrime(num): # 소수 판정
return divisor_primes
# 인수분해 루틴은 소수 판정 루틴과는 조금 다르게 접근할 필요가 있다.
i = 2
while True:
# i가 소수인지 검사해서 소수라면 num을 i로 나눈다
# i로 나눠서 나머지가 0이라면, 인수분해가 한번 된 것이다.
if isPrime(i) and num % i == 0:
divisor_primes.append(i) # 나누는 수를 저장 (가령, num이 12라면 i = 2)
num = num // i # 몫, 가령 24//2 = 12
#print(f"{i}, {num}")
# 12는 소수가 아니므로 반복문 계속. 소수가 나오면 그 소수를 저장하고 반복문 종료.
if isPrime(num):
divisor_primes.append(num)
break # return divisor_primes
i = 2 # 인수분해를 한번 했으므로 숫자를 2로 다시 나눠보기로 설정
else:
i += 1 # 인수분해가 되지 않으면 젯수(나누는 수)를 1증가
return sorted(set(divisor_primes)) # 중복 제거, 정렬하여 돌려주기
# 골드바흐의 추정 : Goldbach's Conjecture
# 4 이상의 모든 짝수는 소수 두 개의 합으로 표현된다.
# ' 친절한 수론 길라잡이' 95쪽
# 4 = 2 + 2, 6 = 3 + 3, 8 = 3 + 5, 10 = 3 + 7, 12 = 5 + 7,
# 14 = 3 + 11, 16 = 3 + 13, 18 = 5 + 13, 20 = 7 + 13 ...
# 숫자 num을 인자로 받는다.
# 2(=i)부터 시작해서 num/2 까지 소수에 해당되는 걸로만 해서 num - i를 시도한다.
# num - i가 소수라면 num이 i와 num-i 라는 두 개의 소수로 표현되었으므로 이 둘을 반환한다
def goldbach(num):
# 이 변수 계산을 for문의 range 안에서 여러 수천, 수만 번 하지 않고 여기서 한번만 하는 것이 훨씬 효율적이다.
until = int(num/2) + 1
for i in range(2, until):
# 2를 제외하고 짝수면 소수가 아니기 때문에 다음으로 넘어간다.
# 함수 호출해서 검사하면 오버헤드 발생하기 때문에 그냥 여기서...
if i != 2 and i % 2 == 0:
continue
if i != 3 and i % 3 == 0:
continue
if isPrime(i) == False: # 2의 배수도, 3의 배수도 아닌 합성수이면 다음으로 넘어간다
continue
if isPrime(num - i): # 이제 i 는 소수이므로 num - i 의 결과가 소수이면 두 값을 리턴한다
return i, num - i
#print(goldbach(20))
#print(goldbach(200))
# my 선형방정식 해 구하기
# 893 * x - 2432* y = 266
# a * x - b * y = c
def linear_equation_compute(a, b, c):
g = gcd(a, b)
lst = []
if c % g != 0:
#print("정수해가 없습니다.")
return lst
count = 0
for i in range(1, b):
r = (a * i - c) % b
if r == 0:
y = (a * i - c) // b
lst.append((i, y))
#print(i, y)
count += 1
if count >= g:
break
return lst
# 선형방정식 해 구하기, 책에서 시키는 대로
# 3*x + 5*y = 1 (gcd(3, 5) = 1)
# (a, b) = (3, 5)
def linear_equation(a, b): # (3, 5)
g, w = a, b # g = 3, w = 5
x, v = 1, 0
while True: # w == 5 (w != 0)
if w == 0:
while x < 0:
x += b
y = (g - a*x) / b
#print(g, x, y) # g = 1, x = 2, y = (1 - 3 * 2) / 5 = -1 (1, 2, -1)
return x, y
break
# 나머지 구하는 항목
# t = 3 % 5 = 3
t = g % w # t = 3 % 5 = 3; t = 5 % 3 = 2; t = 3 % 2 = 1; t = 2 % 1 = 0
# 몫을 구하는 부분
# q = 5 // 3 = 1
q = g // w # q = 3 // 5 = 0; q = 5 // 3 = 1; q = 3 // 2 = 1; q = 2 // 1 = 2
# ?? 1 -몫*0
s = x - q*v # s = 1 - 0*0 = 1; s = 0 - 1 * 1 = -1; s = 1 - 1*(-1) = 2; s = -1 - 2*2 = -5
# x = v, g = w; x = 0, g = 5
x, g = v, w # x = 0, g = 5; x = 1, g = 3; x = -1, g = 2; x = 2, g = 1
v, w = s, t # v = 1, w = 3; v = -1, w = 2; v = 2, w = 1; v = -5, w = 0
#linear_equation(42, 30) # 6 -2 3
#linear_equation(10, 35) # 5 -3 1
# 합동방정식 해 구하기
# '친절한 수론 길라잡이' 65쪽 연습문제 8.7
# ax = c (mod m)
# 합동방정식의 해가 있다면 gcd(a, m) | c 이므로,
# gcd로 세 값을 모두 나눈 것을 da, gm, dc라 하면,
# da*x = dc (mod dm) 은 단 하나의 해를 가진다.
# 이 해를 x1 이라 하면, 해의 갯수는 모두 g개가 된다.
# 모든 해 = {x | x1 + dm * k, k는 0, 1, 2, ..., g-1}
# linear_equation_compute 도 같은 역할을 하는데, 단순하게 대입해서 계산하므로, 수가 크지면
# congruence_equation 함수가 훨씬 빠르게 작동한다.
def congruence_equation(a, c, m):
g = gcd(a, m) # 7
lst = []
if c % g != 0:
#print(f"gcd({a}, {m}) = {g} : {c}을 나눌 수 없습니다. 정수해가 없습니다.")
return lst
da = a // g # 3
dc = c // g # 2
dm = m // g # 13
for i in range(0, dm):
r = (da * i - dc) % dm
if r ==0:
# 정답은 i
#y = (a * i - c) // m # 정답은 (i, y) => (x, y)
break
for k in range(0, g):
x = i + dm*k
lst.append(x)
return lst
#lst = congruence_equation(943, 381, 2576)
#print("{}개 : {}".format(len(lst), lst))
# 다항식 합동식의 해를 함수를 이용해 구한다. 연습문제 8.8
# f(x) 가 x^4 + 5*x^3 + 4*x^2 - 6*x^1 - 4*x^0 이라면 차수와 계수를 딕셔너리 형태로 넘겨준다.
# m도 같이 넘겨주면, 아래 함수에서는 f(x)가 mod m 으로 0일 때를 계산해서 그 x값 리스트를 돌려준다.
# {4:1, 3:5, 2:4, 1:-6, 0:-4}
# 1* (x ** 4) + 5*(x**3) + 4*(x**2) + -6*(x**1) + -4*(x**0)
def congruence_equation_poly(poly, m):
# poly = {차수:계수, 차수:계수, 차수:계수....}
lst = []
for i in range(0, m): # 0 <= i < m 에서 ...
fx = 0 #함수값은 0으로 초기화하고 높은 차수부터 차례로 누적한다.
for d, c in poly.items(): # 딕셔너리 크기만큼 반복 d: 차수, c: 계수
fx += c * (i ** d) # fx 함수값을 계속 누적해서 구한다.
if fx % m == 0: # 함수값이 구해졌으면 mod m으로 합동인지 검사한다.
lst.append(i) # 합동이면 정답이므로, 목록에 추가한다.
return lst
# X^4 + 5X^3 + 4X^2 -6X -4 = 0 (mod 11) 0 <= X <11
#poly = {4:1, 3:5, 2:4, 1:-6, 0:-4}
#m = 11
#res = congruence_equation_poly(poly, m)
#print("m = {}: {}".format(m, res))
# 디리클레의 등차수열 정리를 확인하기 위해,
# 해당 정리를 만족하는 무수히 많은 소수가 있는지 찾아보는 함수
def dirichlet(a, m, max_num):
# p ≡ a (mod m) 인 p(소수)를 찾기
# 일단 소수를 먼저 찾아서 a (mod m)인지 확인하고 리스트에 저장
# max_num은 한도를 정함
lst = []
for i in range(2, max_num): # 2부터 max_num까지에서 소수를 찾음
if isPrime(i):
if i % m == a: # 소수가 나오면 m으로 나눠서 나머지가 a라면
lst.append(i) # 리스트에 추가
return lst
#res = dirichlet(3, 4, 200)
#print(res)
# 서로소인 숫자를 찾아 그 갯수를 반환하는 함수
def euler_phi(num):
lst = [1]
# 서로소인 숫자를 담는 리스트, 1은 기본
for i in range(2, num): # 2 ~ num-1
if gcd(i, num) == 1:
lst.append(i)
return len(lst)
# num 이하의 서로소인 숫자를 찾아 그 목록을 반환하는 함수
def euler_phi_list(num):
lst = [1]
# 서로소인 숫자를 담는 리스트, 1은 기본
for i in range(2, num): # 2 ~ num-1
if gcd(i, num) == 1:
lst.append(i)
return lst
# phi(m) = m(1-1/p1)(1-1/p2)...(1-1/pr) 공식을 이용해 오일러 파이값을 구하는 함수
# p1, p2... pr 은 m의 소인수
# 24 = 2^3 * 3^1 이고 p1 = 2, p2 = 3
def euler_phi2(m):
factorized_dict = factorize2(m) # num = 24일 경우, {2: 3, 3: 1}
phi_m = m
for key, _ in factorized_dict.items(): # key만 사용함
phi_m *=1 - 1/key
return int(phi_m)
# 인수분해하여 오일러 파이값을 구하는 함수
def euler_phi3(num):
factorized_dict = factorize2(num)
total = 1 # 파이값 즉, 서로소인 숫자들 누적 초기화: 계속 곱해야 하기 때문에 초기값은 1
for key, value in factorized_dict.items():
total *= key ** value - key ** (value - 1)
return total
def crt(b, m, c, n): # Chinese Remainder Theorem
# 예시)
# x = 1 (mod 3)
# x = 2 (mod 5)
mn = m * n
b_lst = []
for i in range(b, mn, m):
b_lst.append(i)
c_lst = []
for i in range(c, mn, n):
c_lst.append(i)
return set(b_lst) & set(c_lst)
#print(crt(1, 3, 2, 5))
#print(crt(3, 7, 5, 9))
#print(crt(3, 37, 1, 87))
#=============================================================
# 정해지지 않은 개수의 인자들을 받아 중국인 나머지 정리의 해를 구한다.
# 예: 5, 7, 2, 12, 8, 13
def crt2(*args): # 가변인자를 받는다
length = len(args) # 인자의 개수를 구한다 # 6
if length % 2 != 0:
print("인자의 개수는 짝수여야 합니다.")
return
r = []
m = []
multi_mn = 1 # 각 m을 모두 곱한 값이다. 이 값 이내에 해가 하나 있다.
for i in range(0, length, 2):
r.append(args[i])
m.append(args[i+1])
multi_mn *= args[i+1]
# mul_mn (= m1*m2*m3...)을 먼저 구해야 각 식의 해를 구할 수 있음. 위에서 구한 이유
answers = [] # 각 방정식마다 해를 구해 모은 집합
for i in range(len(r)):
answer = [i for i in range(r[i], multi_mn, m[i])] # 첫번째 방정식의 해 집합
answers.append(answer)
solution = set(answers[0].copy())
# 교집합을 누적으로 구해야 해서, 0번째 교집합을 해집합1로 둔다.
for i in range(len(answers)-1): # 교집합을 구한다. 해 집합이 2개일 경우, 그 교집합은 1개
solution = solution & set(answers[i+1])
return sorted(solution)
# 연습문제 11.5
#print(crt2(5, 7, 2, 12, 8, 13))
# 연습문제 11.6 <손자산경> 풀이
# x = 2 (mod 3), x = 3 (mod 5), x = 2 (mod 7)
#print(crt2(2, 3, 3, 5, 2, 7))
# 연습문제 11.7 계란을 실은 트럭
# x = 1 (mod 2), x = 1 (mod 3), x = 1 (mod 4), x = 1 (mod 5), x = 1 (mod 6), x = 0 (mod 7)
#print(crt2(1, 2, 1, 3, 1, 4, 1, 5, 1, 6, 0, 7))
# 일일이 나눠보는 방식으로 약수 구하는 함수, 속도가 느림
def getDivisor(num):
divisors = []
root_num = round(num ** 0.5)
for i in range(1, root_num+1):
if num % i == 0:
divisors.append(i)
divisors.append(num // i)
return sorted(list(set(divisors)))
# 소인수분해를 해서 약수 구하는 함수: 향상된 버전
# 숫자가 클 경우, 속도 면에서 이전 버전1 보다 훨씬 빠름
# 50234902430546664의 약수를 구했을 때 개선 버전이 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
# 자신을 제외한 약수들을 모두 더해서(진약수 합) 돌려준다
# 예) isPerfectNumber(28) : (True, 28)
def isPerfectNumber(num):
divisors = getDivisor2(num) # 소인수분방해 식으로 약수를 모두 구한다
divisors.remove(num) # 전체 약수에서 자기 자신을 제외
total = 0 # 합 초기화
for d in divisors: # 약수들을 모두 더한다
total += d
if total != num: # 약수들의 합이 자신이 아니라면 완전수가 아니다
return False, total
else: # 약수들의 합이 자신이라면 완전수다
return True, total
# 메르센 소수 구하기
# 2 ^ N - 1 = 소수 (인 경우 구하기)
def get_mersennePrime():
primes = [] # 소수들
m_primes = [] # 메르센 소수들
MAX_NUM = 200 # 여기까지 소수 구하기
# 일단 소수들 구하기
for i in range(2, MAX_NUM):
if isPrime(i):
primes.append(i)
print(primes, "\n")
for p in primes:
num = 2**p - 1
if isPrime(num):
m_primes.append((p, num))
print(p, num)
return m_primes
#get_mersennePrime()
# 유클리드 완전수 공식을 이용해 완전수 찾기
# 2^p - 1 이 소수면, 2^(p-1)(2^p-1)은 완전수다
# 2^p - 1 은 메르센 소수
def euclidPerfectNumber():
mersenne_prime = [2, 3, 5, 7, 13, 17, 19, 31, 61, 89, 107, 127, 521, 607, 1279, 2203, 2281, 3217, 4253, 4423, 9689, 9941]
for p in mersenne_prime:
num = (2**(p-1)) * (2**p-1)
print(num)
# 숫자가 곱셈완전수인지 판정하는 함수
# 소인수분해 방식으로 약수를 모두 구해 자신을 제외하고 모두 곱한다.
# 그 값이 자기 자신과 동일하면 곱셈완전수로 판정한다.
def isProductPerfect(num):
# 약수를 모두 구하기
divisors = getDivisor2(num) # 약수를 모두 구하기 (소인수분해 방식)
divisors.remove(num) # 전체 약수에서 자기 자신을 제외
gop = 1
for d in divisors: # 약수를 모두 곱한다.
gop *= d
if gop != num: # 원 수와 다르면
return False, gop
else: # 원 수와 같으면
return True, gop
# 약수의 개수만 구하는 함수
# 소인수분해해서 지수를 가지고 갯수를 구한다.
def divisorsCount(num):
divisors = factorize2(num) #{2:3, 3:2, 7:1} 형태
# 인수에는 관심없고 각기 지수에 1을 더해 모두 곱한다.
gop = 1
for _, e in divisors.items():
gop *= e + 1
return gop
# 두 수가 친화수인지 확인하는 함수
# m의 진약수의 합이 n이고 m의 진약수의 합이 n인 두 수 m, n을 친화수라 부른다.
# 예) 220의 진약수의 합: 284
# 284의 진약수의 합: 220
def isAmicablePair(m, n):
hap = m + n
# m의 전체 약수 합이 m+n이고, n의 전체 약수 합이 n + m이면 m ,n은 친화수이다.
return sigma_fun(m) == hap and sigma_fun(n) == hap
# 숫자 리스트를 건네주면 사교수인지 판정하는 함수. info 값을 True로 주면, 진행과정을 출력한다
def isSociableNumbers(numbers, info=False):
length = len(numbers) # 순환하는 이 수들의 개수가 위수(order)다, 물론 사교수라면.
isSociable = True # 사교수 여부를 체크하는 flag
for i, n in enumerate(numbers):
if i == length - 1: # 마지막 항목에 이르렀다면
i = -1 # 대조 항목의 인덱스인 i+1을 0(numbers의 첫번째 항목)으로 만들기 위해.
jinhap = sigma_fun(n) - n # 진합은 전체 약수 합에서 자신을 빼면 된다
if jinhap == numbers[i+1]: # 진합과 다음 항목값을 비교해서 동일하다면
if info:
print(f"{n}의 진약수의 합은 {numbers[i+1]}. 조건 만족!")
else: # 진합과 다음 항목값이 다르다면, 더 검사해볼 필요도 없이 루프를 중단해도 됨
if info:
print(f"{n}의 진약수의 합 {jinhap}가 {numbers[i+1]}와 다릅니다. 중단합니다!")
isSociable = False
break
if info:
if isSociable:
print("상기 수들은 사교수입니다.", f"위수는 {length}")
else: # False면
print("상기 수들은 사교수가 아닙니다.")
return isSociable
# 사교수의 범위를 정해주면 사교수를 찾는다
# max_try: 하나의 시작 수에 대해 진약수 합을 연이어 찾는 횟수
def getSociableNumbers(start, end, max_try=30):
for num in range(start, end):
jinhap = num # num을 다치지 않게 복사해서 사용한다.
lst = [] # 연속적인 진약수의 합(진합)을 저장할 리스트
lst.append(num) # num을 첫번째 원소로 저장한다
while True:
prev = jinhap # 첫 실행이면 jinhap은 num 이므로 prev 는 num 이 되고,
# 두 번째부터는 jinhap은 앞서 계산된 진약수의 합이므로,
# prev에는 앞서의 진합이 저장된다.
jinhap = sigma_fun(jinhap) - jinhap # 이전 진합(jinhap)을 넣어 새 진합(jinhap)을 구한다.
# 진합이 1(막다른 골목)이거나 이전 진합과 같은 값(그러면 완전수)이면 사교수가 아니므로 다음 수 검사로 넘어감
if jinhap < 2 and jinhap == prev:
break
# 이제 목록에 같은 값이 있는지 확인한다. 있다면
# 1) 완전수인 경우: 직전 진합과 진합이 같다. 이 경우는 위의 조건문에서 걸러졌음
# 2) 친화수인 경우: [220, 284]의 예라면, 284의 진합은 220이므로 220이 이미 목록(리스트)에 있다.
# 이때 목록에서 220의 인덱스는 0이므로 목록[0:]으로 슬라이싱하면 [220, 284]를
# 최종 예비 사교수 목록으로 얻을 수 있고 이의 위수는 len(목록)으로 2가 된다.
# 위수가 2라는 소리는, (넓은 의미의) 사교수에서 위수가 2이면 친화수가 된다는 뜻이고,
# 본격적인 사교수는 위수가 3일 때부터 취급한다는 뜻이다.
# 3) 사교수(위수 3이상)인 경우: 가령 [A, B, C, D, E, F, G](A, B,...G는 모두 숫자~)라는
# 진약수 합 목록이 있고 G의 진약수 합이 D라면, D의 인덱스는 3이다.
# 목록[3:]으로 슬라이싱하면 [D, E, F, G]라는 새로운 진합 목록을 얻는데, 이는 순환하는
# 사교수 목록이다. D->E->F->G->D->E.... 가 되니까.. 위수는 목록의 원소 개수로 4가 된다.
if jinhap in lst: # 구한 진약수 합이 목록에 이미 있다면
idx = lst.index(jinhap) # 해당 숫자의 인덱스를 찾아서
lst = lst[idx:] # 순환하는 부분만 잘라낸다
if len(lst) >= 3: # 위수가 해당값 이상이면 목적 사교수로 보고 출력한다.
print(num, lst)
break
else:
lst.append(jinhap) # 목록에 새로운 진약수 합과 같은 값이 없으므로 목록에 추가한다.
# 목록에 진합을 추가하다가 한참 뒤에 사교수(순환)의 첫 원소가 생길 수 있다.
# 가령 진합 목록이 [A, B, C, D, ....., Z, a, b, c, d, e, f, g](모두 숫자~) 이렇게 되어가는데,
# g의 진합이 d가 되어 d의 인덱스를 찾아내어 목록[인덱스:]로 [d, e, f, g]를 슬라이싱할 수 있다.
# 사교수 순환의 첫 원소인 d가 이렇게 한참 뒤에 출현할 수 있기 때문에 목록의 갯수에 약간
# 여유를 두는 게 좋다(찾고자 하는 위수의 2~3배 정도?). 목록의 개수를 지나치게 크게 하면 엉뚱한 데서 한없이 찾을 수 있으므로 주의.
if len(lst) >= max_try: break # 충분히 찾았는데도 사교수를 찾지 못했다면 그 숫자는 건너뛴다
# 연속제곱법
# '친절한 수론 길라잡이' 16장 연습문제 16.2
def successiveSquaring(a, k, m):
expo = [] # 지수 보관 예: [1, 2, 4, 8, 16, 32, 64, 128, 256]
c = [] # 계수 보관 예: [1, 1, 1, 0, 0, 0, 1, 0, 1]
count = int(math.log2(k)) + 1 # 2의 거듭제곱값 중에서 k값을 넘지 않는 마지막 수까지의 갯수
for n in range(0, count + 1):
if 2**n > k:
break
expo.append(2**n)
print(expo)
# [1, 2, 4, 8, 16, 32, 64, 128, 256]
length = len(expo) # 9
# bin_str = format(k, 'b') # 한줄로 표현할 수 있음.
bin_str = bin(k) # 0b101000111
bin_str = bin_str[2:] # 얖의 이진수 표시문자 '0b'를 제거. '0b101000111' -> '101000111'
bin_len = len(bin_str) # 9
if length != bin_len:
print("2진수 개수와 지수 개수가 다릅니다.")
print(f"count = {length}, bin_len = {bin_len}")
c = [0] * bin_len # 계수를 저장할 리스트 초기화 # [0, 0, 0, 0, 0, 0, 0, 0, 0]
for i in range(bin_len): # 0 ~ 8
# bin_str의 값들을 뒤집어 정수로 변환해서 c에 저장하기
# 뒤집는 이유는 지수 순서와 일치시켜 주 기 위해
c[bin_len-1-i] = int(bin_str[i])
print(c) # [1, 1, 1, 0, 0, 0, 1, 0, 1]
# c와 expo의 원소들을 한 쌍씩 곱한 다음, 차례로 더하면 k의 값이 나와야 한다. 점검.
total = 0
for i in range(bin_len):
total += c[i] * expo[i]
print(total) # 327
if total != k:
print("계수와 2의 거듭제곱을 곱해서 모두 더했는데 k의 값과 다릅니다.")
mod_ks = []
# (mod k)를 했을 때 나온 값들(=나머지)을 저장.
# 예에서는 [7, 49, 695, 227, 349, 675, 123, 628, 298]
mod_k = a % m # 첫번째 원소로 '7 % m' 을 저장
mod_ks.append(mod_k)
for i in range(length - 1): # 총 8회만 mod_k 계산하면 됨
mod_k = mod_k ** 2 % m
mod_ks.append(mod_k)
print(mod_ks) # [7, 49, 695, 227, 349, 675, 123, 628, 298]
values = []
# 반환할 값들을 계산한다
for i in range(length): # 0~8
if c[i]: # 계수가 0이 아닌 (즉 1인) 지수의 mod_k만 뽑아낸다.
values.append(mod_ks[i])
print(values) # [7, 49, 695, 123, 298]
# 이 값들을 모두 곱해서 (mod m)을 해주면 원하는 값이 나온다.
val_len = len(values) # 5
# 총 val_len - 1 회만큼 계산한다. 한꺼번에 전부 곱하면 숫자가 너무 커질 수 있기 때문에
# 차례로 둘씩 곱하면서 (mod m)을 해준다.
res = values[0]
for i in range(1, val_len):
print(res, values[i])
res = res * values[i] % m
return res # 최종 값: a**k (mod m)
# 연속제곱법, 향상된 버전
# '친절한 수론 길라잡이 연습문제 16.2.b에 나온 코드
def successiveSquaring2(a, k, m):
b = 1
while k >= 1:
if k % 2 == 1:
b = a*b % m
a = a**2 % m
k = k // 2
return b # a^k (mod m)
# 연속제곱법, 쑤튜브 공식 따라서
def my_successiveSquaring(a, k, m):
# 일단 지수의 이진수 표현을 얻기 k => '100011011'
bin_str = format(k, 'b')
val = 1
for i in bin_str:
c = int(i) # 문자열 '1'이나 '0'을 정수로 변환
val = (val**2 * a**c) % m # a^0 혹은 a^1을 곱함
return val
# 고차 합동식을 푼다 (17장: phi_m 구하면서 3단계식 풀이)
# x^k = b (mod m) x의 값은?
def high_congruence_equation(k, b, m):
# 1. phi_m을 구한다. 소인수분해가 안 되면 이 값을 못 구한다.
phi_m = euler_phi2(m)
# 2. 일차 합동방정식을 푼다.
# k*u - phi_m*v = 1
u, _ = linear_equation(k, phi_m)
# 3. 연속제곱법으로 합동식을 푼다.
# b^u (mod m) = x
x = my_successiveSquaring(b, u, m)
return x
# k, b, m = 329, 452, 1147
# print(high_congruence_equation(k, b, m))
# k, b, m = 113, 347, 463
# print(high_congruence_equation(k, b, m))
# k, b, m = 275, 139, 588
# print(high_congruence_equation(k, b, m))
# 암호책: 문자를 숫자로 바꾼다. 원래의 것에 공백과 문장부호 추가
cipher_book = {'A':11, 'B':12, 'C':13, 'D':14, 'E': 15, 'F':16, 'G':17, 'H':18, 'I':19, 'J':20,
'K':21, 'L': 22, 'M':23, 'N':24, 'O':25, 'P':26, 'Q':27, 'R':28, 'S':29, 'T':30,
'U':31, 'V':32, 'W':33, 'X':34, 'Y':35, 'Z':36, ' ':37, '.':38, ',':39, "'":40, '!':41, '~':42}
# key:value 인 암호북으로 value: key 암호북을 새로 생성
cipher_book2 = {}
for key, value in cipher_book.items():
cipher_book2[value] = key
#print(cipher_book2)
def RSA_incode(msg, m, k):
msg = msg.upper() # 대문자로 바꾼다
len_m = len(str(m)) # 9
len_ = len_m - 1 # 8
new_msg = "" # 메시지를 숫자로 바꾼 문자열
# 문자를 숫자로 변환
for c in msg: #'TOBEORNOTTOBE'
new_msg += str(cipher_book[c])
#print(new_msg) # "30251215252824253030251215"
len_new_msg = len(new_msg) # 26
howmany = math.ceil(len_new_msg / (len_m - 1)) # 배열을 쪼갠 갯수 round(26/(9-1))+1 = round(3.25)+1 = 4
div_new_msg = []
for i in range(howmany): # 0~3
if i == howmany - 1: # 마지막이라면
div_new_msg.append(int(new_msg[i*len_:]))
else:
div_new_msg.append(int(new_msg[i*len_:i*len_+len_]))
#print(div_new_msg) #[30251215, 25282425, 30302512, 15]
ciphered_list = []
for i in range(howmany): # 0~3
#ciphered = div_new_msg[i] ** k % m # 단순한 계산 방식. 수가 클 경우 시간이 많이 걸림.
ciphered = my_successiveSquaring(div_new_msg[i], k, m)
ciphered_list.append(ciphered)
return ciphered_list # [149419241, 62721998, 118084566, 40481382]
# 암호 풀기
def RSA_decode(ciphered_list, m, k, p, q):
# 1. phi_m 을 구한다. 소수 p와 q값을 알고 있기에 쉽게 구함
phi_m = (p-1)*(q-1)
# 2. k*u - phi_m*v = 1 1차 방정식을 푼다
u, _ = linear_equation(k, phi_m)
# 3. b^u (mod m) = x : 연속제곱법으로 구한다
answers = ""
for ciphered in ciphered_list:
answer = my_successiveSquaring(ciphered, u, m) # 어떤 숫자
answers += str(answer)
#print("---------", answers) # '30251215252824253030251215'
count = len(answers) // 2
restored = "" # 복원된 암호문 저장
for i in range(count):
tmp = int(answers[i*2:i*2+2])
#print(i, tmp)
restored += cipher_book2[tmp]
return restored # 복원된 암호문
'''
msg = "I loved her. I didn't know that at that time. Now I regret so much."
ciphered_lst = RSA_incode(msg, m, k)
print(ciphered_lst)
word = RSA_decode(ciphered_lst, m, k, p, q)
print(word)
'''
# 카마이클 수인지 정의대로 판정하는 함수
# 일일이 검사하므로 시간이 많이 걸림
def isCarmichael(m):
if isPrime(m) == True: # 소수는 카마이클 수가 아님.
return False
for a in range(m):
if a ** m % m != a:
return False #print("a는 합성수이지만 카마이클 수는 아닙니다.")
return True
# 소인수분해하여 소인수들에 대해서만 mod n을 검사한다. 따라서 속도가 빠르다.
def isCarmichael2(n):
if isPrime(n) == True: # 소수는 카마이클 수가 아님.
return False
res = factorize2(n) # 소인수분해 결과를 딕셔너리로 받는다
lst = []
for key, value in res.items(): # {7: 1, 19: 1, 67: 1}
if value != 1:
return False # 소인수의 지수가 1이 아니라면 카마이클 수가 아님.
lst.append(key)
for a in lst: # [7, 19, 67]
if a ** n % n != a: # 7 ^ n % n = 7 임을 확인.
return False
return True # 조건을 만족시켰으므로 카마이클 수임
# 카마이클 수 관련해서 m이 합성수라는 증인 목록을 만든다.
# 증인 갯수, 증인 비율, (증인 목록)을 반환한다.
def compositeWitness(m):
witnesses = []
if isPrime(m) == True: # 소수는 카마이클 수가 아님.
return witnesses
for a in range(2, m):
if a ** m % m != a:
witnesses.append(a)
return len(witnesses), len(witnesses)/m, witnesses
'''
for i in range(3, 21):
print(i, compositeWitness(i))
numbers = [287, 190, 314, 586, 935, 808, 728, 291]
for n in numbers:
print(n, compositeWitness(n)[0:2]) # 증인 목록이 너무 길어서 목록 출력은 제외
num = [561, 1105, 1729, 2465, 2821, 6601, 8911, 62745, 423899]
for n in num:
print(n, isCarmichael2(n))
'''