다이나믹 프로그래밍 기초

정기홍·2024년 12월 11일
0

코딩테스트_파이썬

목록 보기
36/49

다이나믹 프로그래밍

  • 다이나믹 프로그래밍은 메모리를 적절히 사용하여 수행 시간 효율성을 비약적으로 향상시키는 방법
  • 이미 계산된 결과(작은 문제)는 별도의 메모리 영역에 저장하여 다시 계산하지 않도록 함.
  • 다이나믹 프로그래밍 구현은 일반적으로 탑다운(하향식)과 보텀업(상향식)으로 구성되어 있음.
  • 다이나믹 프로그래밍은 동적 계획법이라고 부름
    • 프로그래밍 분야에서의 동적이란?
      • 자료구조에서 동적 할당(Dynamic Allocation)은 '프로그램이 실행된느 도중에 실행에 필요한 메모리를 할당하는 기법'을 의미함.
      • 방면에 다이나믹 프로그래밍에서 '다이나믹'은 별 다른 의미 없이 사용된 단어임. 엥..?

사용 조건

  1. 최적 부분 구조(Optimal Substructure)
    • 큰 문제를 작은 문제로 나눌 수 있으며 작은 문제의 답을 모아서 큰 문제를 해결할 수 있음.
  2. 중복되는 부분 문제(Overlapping Subproblem)
    • 동일한 작은 문제를 반복적으로 해결해야 함.

다이나믹 프로그래밍으로 해결 할 수 있는 문제

피보나치 수열

  • 피보나치 수열
    • 1,1,2,3,5,8,13,21,34,55,89
  • 점화식이란 인접한 항들 사이의 관계식을 의미함.
  • 피보나치 수열을 점화식으로 표현하면 다음과 같음.

피보나치 수열 코드

# 피보나치 수열을 재귀함수로 표현
def fibo(x):
   if x == 1 or x == 2:
      return 1
   return fibo(x-1) + fibo (x-2)
print(fibo(4))

실행결과
1

피보나치 수열의 시간 복잡도 분석

  • 단순 재귀 함수로 피보나치 수열을 해결하면 지수 시간 복잡도를 가지게 됨.
  • 왜냐하면 동일한 함수가 반복 호출되니까.

메모이제이션(Memoization)

  • 메모이제이션은 다이나믹 프로그래밍을 구현하는 방법 중 하나입니다.
  • 한 번 계산한 결과를 메모리 공간에 메모하는 기법
    • 같은 문제를 다시 호출하면 메모했던 결과를 그대로 가져옵니다.
    • 값을 기록해 놓는다는 점에서 캐싱(cashing)이라고도 함.

탑다운 vs 보텀업

  • 탑다운(메모이제이션) 방식은 하향식이라고도 하며 보텀업 방식은 상향식이라고 함.
  • 다이나믹 프로그래밍의 전형적인 형태는 보텀업 방식입니다.
    • 결과 저장용 리스트는 DP 테이블이라고도 합니다.
  • 엄밀히 말하면 메모이제이션은 이전에 계산된 결과를 일시적으로 기록해 놓은 넓은 개념을 의미합니다.
    • 따라서 메모이제이션은 다이나믹 프로그래밍에 국한된 개념이 아닙니다.
    • 한 번 계산된 결과를 담아놓기만 하고 다이나믹 프로그래밍을 위해 활용하지 않을 수도 있습니다.

피보나치 수열 : 탑 다운 다이나믹 프로그래밍 소스코드 (하향식)

d= [0] * 100

# 피보나치 수열을 재귀함수로 표현(탑다운 다이나믹 프로그래밍)
def fibo(x):
   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(4))

피보나치 수열 : 보텀업 다이나믹 프로그래밍 소스코드 (상향식)

d= [0] * 100

d[1] = 1
d[2] = 2
n = 99
 
 # 피보나치 함수 반복문으로 구현(보텀업 다이나믹 프로그래밍)
for i in range(3, n+1):
  d[i] = d[i-1] + d[i-2]
  
print(d[n]) 
  • 메모이제이션을 이용하는 경우 피보나치 수열 함수의 시간 복잡도는 O(N)입니다. -> 연산이 되는 부분에 중복 연산이 사라지기 때문에.

다이나믹 프로그래밍 vs 분할 정복

  • 다이나믹 프로그래밍과 분할 정복은 모두 최적 부분 구조를 가질 때 사용할 수 있습니다.
    • 큰 문제를 작은 문제로 나눌 수 있으며 작은 문제의 답을 모아서 큰 문제를 해결할 수 있는 상황
  • 다이나믹 프로그래밍과 분할 정복의 큰 차이점은 부분 문제의 중복입니다.
    • 다이나믹 프로그래밍 문제에서는 각 부분 문제들이 서로 영향을 끼치며 부분 문제가 중복됩니다.
    • 분할 정복 문제에서는 동일한 부분 문제가 반복적으로 계산되지 않습니다.
      • 분할 정복 문제의 예시 : 퀵정렬

다이나믹 프로그래밍 문제에 접근하는 방법

  • 주어진 문제가 다이나믹 프로그래밍 유형임을 파악하는 것이 중요합니다.
  • 가장 먼저 그리디, 구현, 완전 탐색 등의 아이디어로 문제를 해결할 수 있는 지 검토합니다.
    • 다른 알고리즘으로 풀이 방법이 떠오르지 않으면 다이나믹 프로그래밍을 고려합니다.
  • 일단 재귀 함수로 비효율적인 완전 탐색 프로그램을 작성한 뒤에 (탑다운) 작은 문제에서 구한 답이 큰 문제에서 그대로 사용될 수 있으면, 코드를 개션하는 방법을 사용할 수 있습니다.
  • 일반적인 코딩테스트 수준에서는 기본 유형의 다이나믹 프로그래밍 문제가 출제되는 경우가 많습니다.

문제

1. 1로 만들기

문제.

x = int(input())

d = [0] * 30001

# 다이나믹 프로그래밍(Dynamic programming) 진행(보텀업)
for i in range(2, x+1):
   d[i] = d[i-1] + 1
   if i % 2 == 0:
      d[i] = max(d[i], d[i//2]+1)
   if i % 3 == 0:
      d[i] = max(d[i], d[i//3]+1)
   if i % 5 == 0:
      d[i] = max(d[i], d[i//5]+1)

print(d[x])

2. 효율적인 화폐 구성: 문제 조건

문제.

n, m = map(int,input().split())
array = []

for i in range(n):
   array.append(int(input()))

d = [10001] * (m+1)

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

3. 금광: 문제 설명

문제

n x m 크기의 금광이 있습니다. 금광은 1 x 1 크기의 칸으로 나누어져 있으며, 각 칸은 특정한 크기의 금이 들어 있습니다. 채굴자는 첫 번째 열부터 출발하여 금을 캐기 시작합니다. 맨 처음에는 첫 번째 열의 어느 행에서든 출발할 수 있습니다. 이후에 m번에 걸쳐서 매번 오른쪽 위, 오른쪽, 오른쪽 아래 3가지 중 하나의 위치로 이동해야 합니다. 결과적으로 채굴자가 얻을 수 있는 금의 최대 크기를 출력하는 프로그램을 작성하세요.

만약 다음과 같이 3 x 4 크기의 금광이 존재한다고 가정합시다.

가장 왼쪽 위의 위치를 (1, 1), 가장 오른쪽 아래의 위치를 (n, m)이라고 할 때, 위 예시에서는 (2, 1) → (3, 2) → (3, 3) → (3, 4)의 위치로 이동하면 총 19만큼의 금을 채굴할 수 있으며, 이때의 값이 최댓값입니다.

T = int(input())
for i in range(T):
    graph = []
    n, m = map(int,input().split())
    graph =list(map(int,input().split()))
    #graph = [[1, 3, 3, 2, 2, 1, 4, 1, 0, 6, 4, 7]]

    dp = [[0]*m for _ in range(n)]

    #2차원 배열 형태로 바꾸기(dp 초기화)
    for i in range(n):
        for j in range(m):
            dp[i][j] = graph[i*m+j]
    #dp = [[1, 3, 3, 2], [2, 1, 4, 1], [0, 6, 4, 7]]

    for j in range(1,m):
        for i in range(n):
            #왼쪽 위에서 오는 경우
            if i == 0:
                left_up = 0
            else:
                left_up = dp[i-1][j-1]
            #왼쪽 아래에서 오는 경우
            if i == n-1:
                left_down = 0
            else:
                left_down = dp[i+1][j-1]
            #왼쪽에서 오는 경우
            left = dp[i][j-1]
            dp[i][j] = dp[i][j] + max(left_up, left_down, left)

    result = 0
    for i in range(n):
        result = max(result,dp[i][m-1])
    print(result)]

4. 병사 배치하기: 문제 설명

이 문제의 기본 아이디어는 가장 긴 증가하는 부분 수열(Longest Increasing Subsequence, LIS)로, 전형적인 다이나믹 프로그래밍 문제의 아이디어와 동일함.

문제

N명의 병사가 무작위로 나열되어 있다. 각 병사는 특정한 값의 전투력을 보유하고 있으며, 병사를 배치할 때는 전투력이 높은 병사가 앞쪽에 오도록 내림차순으로 배치를 하고자 한다. 다시 말해 앞쪽에 있는 병사의 전투력이 항상 뒤쪽에 있는 병사보다 높아야 한다.

또한 배치 과정에서는 특정한 위치에 있는 병사를 열외시키는 방법을 이용한다. 그러면서도 남아있는 병사의 수가 최대가 되도록 하고 싶다.

예를 들어, N=7일 때 나열된 병사들의 전투력이 다음과 같다고 가정하자.

이 때 3번 병사와 6번 병사를 열외시키면, 다음과 같이 남아있는 병사의 수가 내림차순의 형태가 되며 5명이 된다. 이는 남아있는 병사의 수가 최대가 되도록 하는 방법이다.

병사에 대한 정보가 주어졌을 때, 남아있는 병사의 수가 최대가 되도록 하기 위해서 열외해야 하는 병사의 수를 출력하는 프로그램을 작성하시오.

입력

  • 첫째 줄에 N이 주어진다. (1 ≤ N ≤ 2,000) 둘째 줄에 각 병사의 전투력이 공백을 기준으로 구분되어 차례대로 주어진다. 각 병사의 전투력은 10,000,000보다 작거나 같은 자연수이다.

출력

  • 첫째 줄에 남아있는 병사의 수가 최대가 되도록 하기 위해서 열외해야 하는 병사의 수를 출력한다.

정답 코드

n = int(input())

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

array.reverse()

dp = [1] * n

# 가장 긴 증가하는 부분 수열(LTS) 알고리즘 수행
for i in range(1,n):
    for j in range(0,i):
        if array[j] < array[i]:
            dp[i] = max(dp[i], dp[j] + 1)

# 열외해야 하는 병사의 최소 수
print(n - max(dp))
profile
하나를 알고 그걸로 모든걸 관통한다.

0개의 댓글