2022-11-19, 2022-11-20
동적 프로그래밍 (dynamic programming)
참고: 에드위드 파이썬으로 배우는 알고리즘 기초 | 09. 동적 계획과 이항 계수
에드위드 [MIT] 파이썬을 이용한 알고리즘의 이해 | 동적 프로그래밍 1 : 메모이제이션, 피보나치 수, 최단 경로, 추측
https://ocw.mit.edu/courses/6-006-introduction-to-algorithms-fall-2011/resources/mit6_006f11_lec19/
https://www.youtube.com/watch?v=-G8kDiMAPf8&list=PLsMufJgu5932XYejsOwcUDJ2F75f56nrl&index=22
무엇?
- 기본 개념
- recursion + re-use(memoization)
- 문제를 더 작은 문제로 분할하여 상향식으로 문제 해결
- 알고자 하는 문제를 푸는데 도움이 되는 subproblem의 해를 구하고, 그것을 저장하고, 다시 불러와서 사용하는 것
- 큰 문제의 해 = 작은 문제의 점화식
- 더 작은 문제가 무엇인지 정의하는 것(defining subproblems)이 어려운 부분
- controlled bruteforce (careful bruteforce) ⇒ 모든 추측을 “신중하게” 시도해보고 가장 좋은 것을 취하기
- 일반적으로, 하위 문제를 위상 정렬하여 순서대로 해결 하는 것이 중요 (topological sort of subproblem dependency DAG) 상향식 접근법에서, for loop으로
- 동적 프로그래밍과 메모이제이션이 작동하려면 비순환이어야 함
- 동적 계획법을 이해하면 알고리즘의 5부 능선을 넘은 것
- 이해도 어렵고 응용은 더 어렵다, 하지만 재밌습니다 <- 라고 교수님이 말하심…!!
- 리처드 벨먼이 발명
- 벨먼-포드 알고리즘의 기초
- 동적 계획법의 prgramming이란 '최적화optimization’의 뜻과 가까움 → 최대/최솟값 구하는 문제에서 많이 사용( ex. 최단 경로 구하기)
- “basically it sounded cool …” (멋지게 들려서 그렇게 지었다고…)
- 용어
- Programming : 동적 계획법의 programming은 '계획’을 의미함
- Memoization : 가장 작은 입력 사례의 해답을 테이블에 저장하고 필요할 때 꺼내 쓰는 것(re-use하기)
- DP 테이블: 작은 문제들의 해를 저장하는 테이블
- 분할정복법 vs 동적계획법 비교
- 둘 다 문제를 작은 사례로 분할하여 해결함
- 분할정복은 재귀 호출을 통한 반복 (탑다운)
- 동적계획은 메모이제이션을 통해 상향식으로 정복 + 이미 저장한 답을 재사용함으로써 중복 계산을 피함 (바텀업) → 시간 면에서 더 좋다
- 시간복잡도
- 하위 문제의 개수 * 하나의 하위 문제를 구하는데 걸리는 시간
- 선형 시간
왜?
- 이번 주차 키워드 中 하나!
- 상당히 많은 문제들이 동적 계획법을 이용하여 쉽게 풀린다고 함
- 시간 복잡도가 일반 재귀 알고리즘보다 낮음! (하위 문제의 해를 기억해두기 때문)
어떻게?
- 동적 계획법으로 문제 풀기
- 문제를 해결할 수 있는 재귀 관계식을 구한다
- 이 때 보통, 마지막 답이 구해진 상태에서 한 단계 이전의 단계를 생각하면 실마리가 풀리는 경우가 많은 듯 함
- 가장 작은 입력사례로부터 상향식 방법으로 문제를 해결
동적 프로그래밍; 피보나치 수 구하기
참고: 에드위드 [MIT] 파이썬을 이용한 알고리즘의 이해 | 동적 프로그래밍 1 : 메모이제이션, 피보나치 수, 최단 경로, 추측
무엇?
피보나치 수를 구하는 문제에서 subproblem은 F1, F2, … Fn을 구하는 것.
- 일반적인 재귀 알고리즘으로 해결하는 법
- DP 알고리즘으로 해결하는 법
두 가지를 알아보고 비교하기
왜?
어떻게?
- naive recursive 알고리즘
f = f(n-1) + f(n-2)
를 이용함- 제곱의 시간 복잡도를 가짐 → 정확하지만 좋지 않은 알고리즘
fib(n):
if n <= 2:
f = 1
else:
f = fib(n-1) + fib(n-2)
return f
- Dynamic Programming
- 피보나치 수를 계산할 때마다 결과 값을 딕셔너리에 넣음
- n번째 피보나치 수를 계산할 때마다 딕셔너리에 이미 그것이 있는지 확인함(이미 이 문제를 풀었는지 확인하는 것)
- 풀었다면 답을 반환
- 풀지 않았다면 값을 계산
- ▽ 재귀 함수를 이용하는 방법(memoization)
# 재귀 함수를 이용하는 방법(memoization) memo = {} def fib(n): if n in memo: return memo[n] # 아래 네 줄의 if, else절은 일반 재귀 알고리즘의 그것과 동일함을 주목! if n <= 2: f = 1 else: f = fib(n-1) + fib(n-2) memo[n] = f return f
- ▽ 반복문을 이용하는 법(상향식 접근)
# 반복문을 이용하는 방법(상향식 접근) fib = {} for k in range(1, n+1): if k <= 2: f = 1 else: f = fib[k-1] + fib[k-2] # 이미 앞에서 계산한 것을 더하기 fib[k] = f print(fib[n])
- 상향식 접근법에서는 공간을 절약할 수 있음
- 꼭 필요한 마지막 두 개의 fib 수만 저장하고 나머지는 지워도 됨
- 재귀 호출이 없어서 일반적으로 더 효율적
- 상향식 접근법에서는 공간을 절약할 수 있음
동적계획법; 최단 경로 문제 (플로이드 알고리즘)
참고: https://www.edwith.org/knu-algorithm/lecture/170189/?isDesc=false
무엇?
- 최단 경로 알고리즘 종류
- 다익스트라
- 플로이드
- 벨먼
- A* (A star)
- 주어진 그래프에서 모든 정점의 쌍에 대한 최단 경로를 구하기
- Directed Acyclic Weighted Graph
- 싸이클이 있으면 빙글빙글 돌다가 경로가 늘어남
왜?
브루트포스로 최단 경로 문제를 해결할 시, 모든 정점간의 간선이 존재하는 경우 지수 시간 복잡도를 가짐(모든 경우의 수를 다 구함)
어떻게?
- 재귀 관계식을 찾는다
D
: 각 정점의 쌍이 갖는 최단 경로의 길이를 나타내는 행렬D[i][j]
: vi에서 vj로 가는 최단 경로의 길이- 목표: 인접 행렬 W에서 최단 경로의 행렬 D와의 재귀 관계식 구하기!
- 상향식 방법으로 해답 구하기
- 초기화: D0 = W
- 최종 목표: Dn = D
- 상향식 계산: D0, D1, D2, …, Dn
- D0(= 입력으로 받은 W)로부터 D1을 구하고, D1으로부터 D2를 구하고, … Dn-1로부터 최종 해답인 Dn(= D)을 구하는 것
- 최단 경로 행렬의 이해
- Dk : k개의 중간 노드를 거치는 최단 경로 길이의 행렬
(ex. D1[2][3] : 1개의 노드를 거쳐 2번 노드에서 3번 노드로 가는 최단 경로의 길이) - 따라서 D0는 아무 노드도 거치지 않고 가는 경로의 길이를 말하므로, W와 같음
- Dn은 다른 모든 정점을 지날 수 있는 최단 경로의 길이를 말하므로, D와 같음
- Dk : k개의 중간 노드를 거치는 최단 경로 길이의 행렬
- 그러면 어떻게 Dk-1로부터 Dk를 구할 것인가?
- Dk[i][j]는 다음과 같은 경우를 가짐
- 하나의 정점을 더 지나도 새로운 최단 경로가 없는 경우 ⇒ Dk[i][j] = Dk-1[i][j]
- 하나의 정점을 더 지나면 새로운 최단 경로가 있는 경우 ⇒ Dk[i][j] = Dk-1[i][k] + Dk-1[k][j]
- principle of optimality: “i에서 k 까지의 최단 경로” + "k에서 j까지의 최단경로"가 "i에서 j까지의 최단 경로"를 보장함
- 위의 법칙이 적용되어야 함
- k는 하나가 아니라 여러 개일 수 있음
- Dk[i][j]는 다음과 같은 경우를 가짐
- 최단 경로의 재귀 관계식
- Dk[i][j] = min(Dk[i][j], Dk-1[i][k] + Dk-1[k][j])
INF = 999
W = # 그래프 V의 모든 간선을 나타내는 행렬 W
# 최단 경로 길이 구하기
def floyd(W):
D = W
n = len(W)
P = [[-1] * n for _ in range(n)]
for k in range(n):
for i in range(n):
for j in range(n):
D[i][j] = min(D[i][j], D[i][k] + D[k][j])
return D
D = floyd(W)
for i in range(len(D)):
print(D[i])
# 최단 경로 구하기
def floyd2(W):
D = W
n = len(W)
P = [[-1] * n for _ in range(n)]
for k in range(n):
for i in range(n):
for j in range(n):
if (D[i][j] > D[i][k] + D[k][j]):
D[i][j] = D[i][k] + D[k][j]
P[i][j] = k
return D, P
def path(P, u, v):
if (P[u][v] != -1):
path(P, u, P[u][v]) # u에서 k까지
print("{P[u][v]} -> ")
path(P, P[u][v], v) # k에서 v까지
'크래프톤정글 > TIL & WIL' 카테고리의 다른 글
크래프톤정글 4주차; TIL 3 - 컴퓨터 구조와 운영체제의 큰 그림 (0) | 2022.11.28 |
---|---|
크래프톤정글 4주차; TIL 2 - DP 동전 문제, LCS, 파이썬에서 2차원 배열 주의사항, 배낭 문제 (1) | 2022.11.28 |
크래프톤정글 3주차; TIL 4 - 파이썬 type error, BFS, topological sort, 문자열 검색 알고리즘 (0) | 2022.11.28 |
크래프톤정글 3주차; TIL 3 - 최소 신장 트리 문제, DFS와 BFS, 연결 요소의 개수 문제 (0) | 2022.11.14 |
크래프톤정글 3주차; TIL 2 - 파이썬 BST, 최소 신장 트리 (0) | 2022.11.14 |