SW Expert Academy 1차시~ 5차시 를 참고하여 작성된 글입니다.
다음과 같은 조건일 때 n번째 달의 토끼 수는?
- 첫 달에는 새로 태어난 토끼 한쌍이 존재
- 두 달 이상 된 토끼는 번식 가능
- 번식 가능한 토끼는 매달 새끼 한 쌍 낳음
- 토끼는 죽지 않음
정답
- n번째 달 : a쌍
- n+1번째 달 : b쌍(새로 태어난 토끼 포함)
- n+2번째 달 : a+b쌍(a만큼의 자식을 낳음)
- 결론 : f(n+2) = f(n)+f(n+1)
피보나치 수열
0과 1로 시작하고 이전의 두 수의 합을 다음 항으로 하는 수열
Code 1
def fibo(n):
if n<2:
return n
else :
return fibo(n-1) + fibo(n-2)
BUT! 피보나치 수를 재귀함수로 구현하는 경우, 엄청난 중복 호출 존재
피보나치 함수의 중복 호출이 얼마나 있는지 알기 위해 이용하는 방법
1) 귀납 기본 : 주어진 등식이 n=1(또는 n=0)일 때 성립함을 증명 2) 귀납 가정 : 임의의 n일 때 성립한다고 가정 3) 귀납 단계 : n+1일 때 성립함을 증명 4) 모든 n에 대하여 성립
가정 : 재귀적 알고리즘으로 구성한 재귀 트리의 노드의 수 : 라고 하면
인 모든 에 대하여
1) 귀납 기본 :
2) 귀납 가정 : 인 모든 에 대해서 라고 가정
3) 귀납 단계 :
- 비둘기 : n+1 마리
- 비둘기 집 : n개
- 각각 임의의 비둘기 집을 선택하여 들어감
- 한 개의 집에는 적어도 2마리 이상의 비둘기가 있음
가정 : 개의 물건을 개의 상자에 넣을 때 적어도 한 상자에는 두 개 이상의 물건이 들어 있다
1) 개의 상자와 개의 물건이 있다고 가정
2) 만약 각 상자에 하나 이하의 물건이 들어있다면, 모든 상자 속 물건의 합계는 많아봤다 개 = 가정에 모순되는 결론
3) 따라서 적어도 한 개의 상자에는 두 개 이상의 물건이 있다
가정 : 피보나치 수열의 중복
1) n번째 피보나치 수를 구하기 위해 알아야 할 값 : ~ 까지의 값
2) n번째 피보나치 수를 구하기 위해 재귀 호출로 작성된 함수 호출
- 함수 번 이상 호출
-
3) 위 증명을 통해 피보나치 함수 재귀적 호출이 많은 중복을 야기함을 알 수 있음
컴퓨터 프로그램을 실행할 때 이전에 계산한 값을 메모리에 저장해서 매번 다시 계산하지 않도록 하여 전체적인 실행속도를 빠르게 하는 기술
= 동적 계획법을 적용하기 위해 사용되는 핵심 기술
Code2
# memo 배열을 할당하고, 모두 0으로 초기화
# memo[0]을 0으로 memo[1]은 1로 초기화
def fibo(n):
if n>=2 and memo[n] = 0:
memo[n] = fibo(n-1) + fibo(n-2) # 처음 계산되는 값이라면 값 저장
return memo[n]
BUT! 추가 메모리 공간이 필요하므로 n값이 커질 수록 실행 속도가 저하되며, 스택 오버플로우가 발생할 수 있음
작은 부분에서 큰 부분의 해들을 모두 구하여 최종적으로 원래 주어진 문제를 해결하는 설계 기법
1) 최적화 문제 해결 : 최대값/최소값 구하는 문제, 여러 개의 최적해 중 임의의 최적해 하나를 찾는 것 2) 완전 검색을 좀 더 효율적으로 하는 방법 3) 재귀 + 메모이제이션 4) 점화식을 찾으면 됨 : 문제를 분석하여 재귀적 정의 및 수식 형태 표현 필요
1) 중복 부분문제 구조
- 순환적인 관계(recurrence relation)의 명시적 표현 : 점화식 사용
- 문제의 순환적 성질로 이전에 계산된 작은 문제의 해가 더 큰 문제의 해를 구할 때 중복으로 사용됨
- 메모이제이션 사용
- 이미 해결한 작은 문제의 해가 다시 필요할 때 테이블을 참조하여 중복 계산을 피함
2) 최적 부분문제 구조
- 주어진 문제가 최적화의 원칙을 만족해야 동적 계획법을 효율적으로 적용 가능
- 최적화 원칙 : 어떤 문제에 대한 해가 최적일 대 그 해를 구성하는 작은 문제들의 해 역시 최적이어야 함
- 최장 경로 : 사이클이 없는 단순 경로
특징 | 분할 정복 | 동적 계획 |
---|---|---|
문제 분할 | 주어진 문제를 부분 문제들로 분할 | 부분문제들이 더 작은 부분문제들의 해를 공유 |
부분문제 해의 목적 | 더 큰 부분 문제들의 해를 구할 때 사용 | 더 큰 부분 문제에 중복하여 속할 수 있음 |
부분문제 해의 계산 | 부분문제를 재귀적으로 해결하고 필요시 각각의 해를 결합 | 모든 부분문제를 한번만 계산하여 결과 저장 |
부분문제 해의 활용 | 병합 정렬과 퀵 정렬은 작은 문제의 해가 큰 문제의 해에서 중복해서 사용되지 않음 | 필요시 재사용 |
부분문제 사이 의존적 관계 | 하향식 방법 : 주어진 큰 문제를 위한 작은 문제들이 존재 | 상향식 방법 : 의존성에 위배되지 않게 작은 문제의 해를 구해 더 큰 문제 해를 구함 |
1) 최적해 구조의 특성 파악 : 문제를 더 작은 부분 문제로 나눔
2) 최적해 값의 재귀적 정의 : 부분 문제들의 최적 해를 사용하여 더 큰 문제의 최적해 값 정의(점화식 사용 가능)
3) 상향식 방법으로 최적해 값 계산 : 의존성에 위배되지 않도록 가장 작은 부분문제부터 해를 구하고 테이블 저장, 이를 이용하여 상위 문제 해결
Code 3
f = [] * n
def fibo_dp(n):
f[0] = 0
f[1] = 1
for i in range(2, n):
# n=2인 문제부터 시작해서 큰 문제를 해결해나감
f[i] = f[i-1] + f[i-2]
return f[n] # n번째 인덱스에 n번째 피보나치 수가 저장된 상태
사용할 수 있는 동전이 1원, 4원, 6원일 때 거스름돈 8원에 대한 최소 동전 개수는 몇 개일까?
그리디 방법 접근 : 6 / 1 / 1 원
최적해 : 4 / 4 원
= 3가지 동전 각각을 선택하여 재귀적으로 해결
change = 0 # 거스름돈
coin = [6,4,1]
memo = []*n
def coin_change(change):
memo[0] = 0 # 거스름돈이 0일때는 0개의 동전이 필요
for n in range(1,change):
n_min = 9999999 # 무한대
for i in range(1, len(coin)-1):
if n>=coin[i]:
if memo[n-coin[i]] < n_min :
#현재 선택한 동전의 금액을 차감한 금액이 최소값인지 계산
n_min = memo[n-coin[i]]
memo[n] = n_min +1
return memo[change] # 거스름돈이 change일 때 필요한 최소 동전 갯수를 반환
이항정리 : 을 전개 했을 때 의 계수 값을 구하는 정리
이항계수 : 의 계수는 n개에서 k개를 고르는 조합의 가짓수인
이항 계수를 삼각형 모양의 기하학적 형태로 배열한 것
규칙
- n번째 줄 : n개의 숫자
- 각 줄 양 끝 : 숫자 1
- 삼각형 내부 숫자 : 윗 양 대각선 숫자의 합
n = 정수
k = n보다 같거나 작은 수
def bino(n,k):
if k==0 or n==k:
return 1
else:
return bino(n-1, k-1) + bino(n-1, k)
B | 0 | 1 | 2 | ... | j |
---|---|---|---|---|---|
0 | (0,0) | ||||
1 | (1,0) | (1,1) | |||
2 | (2,0) | (2,1) | (2,2) | ||
... | |||||
i | ?? |
def bino(n,k):
B = [[] for _ in range(N)]
for i in range(0,n):
for j in range(0, min(i,k)):
if j == 0 or j == i:
B[i][j] = 1
else:
B[i][j] = B[i-1][j-1] + B[i-1][j]
return B[n][k]