알고리즘/정리

동적 계획법 (Dynamic Programming)

sssbin 2022. 3. 21. 21:38

 

중복되는 연산을 줄이자

- 어떤 문제는 메모리 공간을 약간 더 사용하면 연산 속도를 비약적으로 증가시킬 수 있는 방법이 있다. 

- 다이나믹 프로그래밍으로 해결할 수 있는 대표적인 예시로는 피보나치 수열이 있다.

- 수학적 점화식을 프로그래밍으로 표현하려면 재귀 함수를 사용하면 간단하다.

# 피보나치 함수를 재귀 함수로 구현

def fibo(x):
    if x == 1 or x == 2:
        return 1
    return fibo(x-1) + fibo(x-2)

print(fibo(4))

 

- 그런데 피보나치 수열의 소스코드를 이렇게 작성하면 심각한 문제가 생길 수 있다.

- f(n) 함수에서 n이 커지면 커질수록 수행 시간이 기하급수적으로 늘어나기 때문! -> O(2^N)

- 피보나치 수열의 호출 과정을 그림을 그려 확인해보면 동일한 함수가 반복적으로 호출되는 것을 알 수 있다.

- 이미 한 번 계산했지만, 계속 호출할 때마다 계산하는 것이다.

- 이와 같은 문제를 다이나믹 프로그래밍을 사용하면 효율적으로 해결할 수 있다.

    조건1) 큰 문제를 작은 문제로 나눌 수 있다.

    조건2) 작은 문제에서 구한 정답은 그것을 포함하는 큰 문제에서도 동일하다.

- 메모이제이션 기법(다이나믹 프로그래밍을 구현하는 방법 중 한 종류) = 캐싱

  : 한 번 구한 결과를 메모리 공간에 메모해두고 같은 식을 다시 호출하면 메모한 결과를 그대로 가져오는 기법

# 한 번 계산된 결과를 메모이제이션하기 위한 리스트 초기화
d = [0] * 100

# 피보나치 함수를 재귀함수로 구현 (탑다운 다이나믹 프로그래밍)
def fibo(x):
    # 종료 조건 (1 혹은 2일 때 1을 반환)
    if x == 1 or x == 2:
        return 1
    # 이미 계산한 적 있는 문제라면 그대로 반환
    if d[x] != 0:
        return d[x]
    # 아직 계산하지 않은 문제라면 점화식에 따라서 피보나치 결과 반환
    d[x] = fibo(x-1) + fibo(x-2)
    return d[x]

print(fibo(99))

- 정리하자면 다이나믹 프로그래밍이란 큰 문제를 작게 나누고,

   같은 문제라면 한 번씩만 풀어 문제를 효율적으로 해결하는 알고리즘 기법이다.

- 분할 정복 알고리즘(퀵정렬)과 다른점은 다이나믹 프로그래밍은 문제들이 서로 영향을 미치고 있다는 것.

- 다이나믹 프로그래밍을 적용했을 때의 피보나치 수열 알고리즘의 시간 복잡도는 O(N)

    ㄴ f(1) 구한 값이 f(2) 푸는 데 사용되고,

        f(2)의 값이 f(3) 푸는 데 사용되고 ···

    ㄴ 한 번 구한 결과는 다시 구해지지 않는다.

f(6)을 호출했을 때 실제로 호출되는 함수에 대해서 확인

# 피보나치에서 호출되는 함수 확인

d = [0] * 100

def pibo(x):
    print('f(' + str(x) + ')', end=' ')
    if x == 1 or x == 2:
        return 1
    if d[x] != 0:
        return d[x]
    d[x] = pibo(x-1) + pibo(x-2)
    return d[x]

pibo(6)

 

- 재귀함수를 이용하여 다이나믹 프로그래밍 소스코드를 작성하는 방법을,

   큰 문제를 해결하기 위해 작은 문제를 호출한다고 하여 탑다운 방식이라고 말한다. -> 하향식

- 단순히 반복문을 이용하여 소스코드를 작성하는 경우

   작은 문제부터 차근차근 답을 도출한다고 하여 보텀업 방식이라고 말한다. -> 상향식

# 피보나치 수열 (반복적)

# 앞서 계산된 결과를 저장하기 위한 DP 테이블 초기화
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])

 

실전문제1. 1로 만들기

x = int(input())

# 앞서 계산된 결과를 저장하기 위한 DP 테이블 초기화
d = [0] * 3001

# 다이나믹 프로그래밍 진행 (보텀업)
for i in range(2, x+1):
    # 현재의 수에서 1을 빼는 경우
    d[i] = d[i-1] + 1

    # 현재의 수가 2로 나누어 떨어지는 경우
    if i % 2 == 0:
        d[i] = min(d[i], d[i//2] + 1)

    # 현재의 수가 3으로 나누어 떨어지는 경우
    if i % 3 == 0:
        d[i] = min(d[i], d[i//3] + 1)

    # 현재의 수가 5로 나누어 떨어지는 경우
    if i % 5 == 0:
        d[i] = min(d[i], d[i//5] + 1)

print(d[x])

 

실전문제2. 개미 전사

n = int(input())
array = list(map(int, input().split()))

# 앞서 계산된 결과를 저장하기 위한 DP 테이블
d = [0] * 100

# 다이나믹 프로그래밍 진행 (보텀업)
d[0] = array[0]
d[1] = max(array[0], array[1])
for i in range(2, n):
    d[i] = max(d[i-1], d[i-2] + array[i])

print(d[n-1])

 

실전문제3. 바닥 공사 

n = int(input())

# 앞서 계산된 결과를 저장하기 위한 DP 테이블 초기화
d = [0] * 1001

# 다이나믹 프로그래밍 진행(보텀업)
d[1] = 1
d[2] = 3
for i in range(3, n+1):
    d[i] = (d[i-1] + 2 * d[i-2]) % 796796

print(d[n])

 

실전 문제4. 효율적인 화폐 구성

n, m = map(int, input().split())
values = []
for i in range(n):
    values.append(int(input()))

# 한 번 계산된 결과를 저장하기 위한 DP 테이블 초기화
dp = [10001] * (m+1)

# 다이나믹 프로그래밍 진행 (보텀업)
dp[0] = 0
for i in range(n):
    for j in range(values[i], m+1):
        if dp[j - values[i]] != 10001: # (i - k)원을 만드는 방법이 존재하는 경우
            dp[j] = min(dp[j], dp[j - values[i]] + 1)

if dp[m] == 10001: # 최종적으로 M원을 만드는 방법이 없는 경우
    print(-1)
else:
    print(dp[m])