세리프 따라잡기

WEEK04 - TIL 다이나믹 프로그래밍 본문

SW사관학교 정글

WEEK04 - TIL 다이나믹 프로그래밍

맑은 고딕 2022. 4. 22. 14:02

다이나믹 프로그래밍(Dynamic Programming)이란?

- 메모리를 적절히 사용해 수행 시간 효율성을 비약적으로 향상시키는 방법

- 이미 계산된 결과(작은 문제)는 별도의 메모리 영역에 저장해두었다가 나중에 해당 결과가 필요할 때 메모리 영역에 기록되어 있던 정보를 그대로 사용하도록 한다. 즉, 한 번 계산해서 해결한 문제는 다시 해결하지 않도록 함

└> 굉장히 비효율적인 시간 복잡도를 가진 문제여도, DP를 이용해서 시간 복잡도를 줄일 수 있는 경우가 많다!

- DP의 구현은 일반적으로 탑다운(하향식), 바텀업(상향식)의 두 가지 방식으로 구성된다.

- DP는 동적 계획법이라고도 부른다.

└> 여기서의 동적(Dynamic)의 의미는, 별다른 의미가 없다..!

더보기

ex. 자료구조에서 동적 할당(Dynamic Allocation)은 '프로그램이 실행되는 도중에 실행에 필요한 메모리를 할당하는 기법'을 의미한다.

 

다이나믹 프로그래밍의 조건

- DP문제는 다음의 조건을 만족할 때 사용할 수 있다.

    1. 최적 부분 구조 (Optimal Substructure)

        = 큰 문제를 작은 문제로 나눌 수 있으며, 작은 문제의 답을 모아서 큰 문제를 해결할 수 있는 걸 의미

    2. 중복되는 부분 문제 (Overlapping Subproblem)

        = 동일한 작은 문제가 반복적으로 호출되어, 동일한 문제를 반복적으로 해결해야 할 때

            == 특정 문제가 중복되는 문제를 가진다.

 

더보기

- DP를 이용해서 풀 수 있는 문제 대표: 피보나치 수열

    1. 각 피보나치 수는 앞에 있는 두 수의 합으로 구성된다.

    2. 인접한 항들 사이의 관계식을 의미 == 점화식

 

- 피보나치 수열을 점화식으로 표현

    ex. a₁=1, a₂=1 일때 a₃은 1+1=2 라는 것을 알 수 있다.

- 점화식은 실제 프로그래밍상에서 재귀함수를 이용해 점화식 형태 그대로 소스 코드로 옮겨서 구현할 수 있다는 특징이 있다.

 

- 피보나치 수열 계산되는 과정을 그림으로 표현

    = 프로그래밍에서는 이런 수열을 배열이나 리스트를 이용해 표현한다.

    = 그래서 DP는 각 계산된 결과를 담기 위해 별도의 테이블(배열,리스트)과 같은 공간에 값을 기록한다고 표현한다.

 

- 피보나치 수열 트리로 표현

 

- 피보나치 수열 재귀적으로 표현

def fibo(x):
    if x == 1 or x == 2: # 첫번째나 두번째 피보나치 수를 호출할 때는
        return 1 # 종료조건 명시
    return fibo(x-1)+fibo(x-2)

print(fibo(4)) #3

 

- 피보나치 수열의 시간 복잡도는 지수 시간 복잡도 이다.

    ∴ 피보나치 수열은 DP의 사용 조건을 만족하기에 DP를 사용하여 효율적으로 해결할 수 있다.

        1. 최적 부분 구조: f(4)에서 쪼갤 수 있음

        2. 중복되는 부분 문제: f(2)가 여러번 호출됨

 

메모이제이션(Memoization) - 탑다운(하향식) 방법

- DP를 구현하는 방법 중 하나 (재귀함수를 사용한다) ↔ 바텀업 방식에선 반복문을 사용

- 한 번 계산한 결과를 메모리 공간에 메모하는 기법

    1. 같은 문제를 다시 호출하면 메모했던 결과를 그대로 가져온다.

    2. 별도의 테이플(배열)에 값을 기록해 놓기에 '캐싱(caching)'이라고도 한다.

- 메모이제이션은 원래 '이전에 계산된 결과를 일시적으로 기록해 놓는 넓은 개념'을 의미한다.

    - 때문에 DP에만 국한된 개념은 X

    - 한 번 계산된 결과를 담기만 하고 DP에서 활용하지 않을 수 있기 때문에

 

☞ 즉, DP을 구현하는 방법 중에 하향식 방법으로 접근할 때 이미 계산된 결과를 기록하는 방법으로 메모이제이션을 사용할 수 있는 것이다~ [ DP != 메모이제이션 ]

 

- 피보나치 수열을 통해 보여주는 DP 메모이제이션(탑다운) 이용한 구현

# 한 번 계산된 결과를 메모이제이션하기 위한 리스트 초기화
# 0~99 인덱스를 가지게 된다. == 여기서는 16줄처럼 99개가 필요해서 이렇게 해줌
d = [0]*100

def fibo(x):
    # 종료 조건 명시 (1, 2 일때 1을 반환)
    if x == 1 or x == 2:
        return 1
    # 이미 계산한 적 있는 문제라면 그대로 반환 [0이 아니라면 이미 계산된 것]
    if d[x] != 0:
        return d[x]
    # 아직 계산하지 않은 문제면, 점화식에 따라 피보나치 결과 반환
    d[x] = fibo(x-1)+fibo(x-2)
    return d[x]

print(fibo(99)) # 218922995834555169026

 

DP의 전형적인 형태는 바텀업 방식, 이때 사용되는 결과 저장용 리스트는 DP 테이블이라고 한다.

- 피보나치 수열을 통해 보여주는 DP 바텀업 방식 구현

# 앞서 계산된 결과를 저장하기 위한 dp 테이블 초기화
# 0~99 인덱스의 원소의 값이 0이도록
d = [0]*100

# 첫번째 피보나치 수와 두번째 피보나치 수는 1
d[1] = 1
d[2] = 1
n = 99

# 피보나치 함수 → 반복문으로 구현
for i in range(3, n+1):
    d[i] = d[i-1]+d[i-2]

print(d[n]) # 218922995834555169026

즉, 작은 문제부터 먼저 해결하고 해결한 작은 문제들을 조합해서 앞으로의 큰 문제들을 차례대로 구해나가는 것

 

메모이제이션 동작 분석: 피보나치 수열을 이용

- 이미 계산된 결과를 메모리에 저장하면 다음과 같이 색칠된 노드만 처리할 것을 기대할 수 있다.

 

더보기

- DP와 분할 정복은 모두 최적 부분 구조를 가질 때 사용할 수 있다.

    - 큰 문제를 작은 문제로 나눌 수 있고 작은 문제의 답을 모아서 큰 문제를 해결할 수 있는 상황

- DP와 분할 정복의 차이점은 부분 문제의 중복!

    - DP문제에서는 각 부분 문제들이 서로 영향을 미치며 부분 문제가 중복

    - 분할 정복 문제에서는 동일한 부분 문제가 반복적으로 계산되지 않는다.

 

ex. 분할 정복의 대표적 예인 퀵 정렬을 보자면,

- 한 번 기준 원소가 자리를 변경해 자리를 잡으면, 그 원소의 위치는 이제 바뀌지 않음

- 분할 이후에 해당 피벗을 다시 처리하는 부분 문제는 호출 X == 부분 문제가 중복되지 않는다

 

끗~ 🤸‍♀️

Comments