CS/알고리즘
[알고리즘] 다이나믹 프로그래밍
삶_
2022. 7. 5. 09:29
다이나믹 프로그래밍(동적계획법)
- 다음 두가지 조건을 만족할 때 사용하기
- 최적부분구조
- 큰 문제를 작은문제로 나눌수있음
- 중복되는 부분 문제
- 동일한 작은 문제를 반복적으로 해결해야함
피보나치 수열
- 1,1,2,3,5,8,13,21,34,55,89 ...
- 점화식 : 인접한 항들 사이의 관계식
- 1,1을 더해 2. /// 2,3을 더해 5 ...
- 문제점 : 재귀함수를 통해 해결하면, 같은 것이 중복적으로 호출되는 문제가 있다
탑다운 VS 보텀업
- 메모이제이션(탑다운, 하향식)
- 이전에 한 번 계산된 결과를 일시적으로 메모리 공간에 메모하는 기법
- 값을 기록해 놓는다는 점에서 캐싱이라고도 함
#메모이제이션 하기 위한 리스트 초기화
d = [0] * 100
#피보나치 함수를 재귀함수로 구현
def fibo(x):
#종료 조건(1,2면 1을 반환. 첫시작은 1,1 이니까! x=3이면 아마 이럴거같다.)
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))
- 보텀업(상향식)
- 다이나믹 프로그래밍의 전형적인 형태
- DP 테이블 : 결과저장용 리스트
#DP 테이블 초기화
d = [0] * 100
#첫번째, 두번째 피보나치수 초기화
d[1] = 1
d[2] = 1
n = 99 #99까지를 구하는 것이기 때문에
#피보나치 함수를 반복문으로 구현. (3부터 진행 (d[1]+d[2])값을 아니까)
for i in range(3, n+1):
d[i] = d[i-1] + d[i-2]
print(d[n])
문제풀이
- 개미전사 -> 메뚜기 식량창고 공격
여러개의 식량창고가 일직선으로 이어짐
서로 인접한 식량창고가 공격받으면 바로 알아챌수있음
최소한 한칸이상 떨어진 식량창고 약탈 - 첫째줄에 식량창고의 개수 n
둘째줄에 공백 기준으로 각 식량창고에 저장된 식량의 개수 k
첫째줄에 개미전사가 얻을수있는 식량의 최댓값 출력하기
import sys
n = int(sys.stdin.readline())
k = list(map(int, sys.stdin.readline()))
#DP테이블
d = [0]*n
#인덱스 1+3과 2를 비교 반복
#순차적으로 비교해서 맨 마지막에 나오는 값을 출력함
#첫, 두번째값을 먼저 비교해준다. 첫,두번째를 대입해야 for문에서 i-1,i-2를 할수있어서
d[0] = k[0] #첫번째값
d[1] = max(k[0], k[1]) #두번째엔 첫번째,두번째를 비교한 값
for i in range(2, n):
#i번째까지의 최대값 구하기
d[i] = max(d[i-1], d[i-2]+k[i])
#마지막값을 출력 = 결과
print(d[n-1])
- 보텀업 문제
- 정수 x가 주어질때 x에 사용가능한 연산은 4가지
1. x가 5로 나누어 떨어지면, 5로 나눔
2. x가 3으로 나누어 떨어지면, 3으로 나눔
3. x가 2로 나누어 떨어지면, 2로 나눔
4. x에서 1을 뺌 - 1 <= x <= 30000
- 연산 4개를 사용해 값을 1로 만들려고 함
연산을 사용하는 최소 횟수값을 출력하기. - 피보나치 수열 문제와 관련있음
#단, 이 식은 n-1빼고 해당수로 나누어 떨어질때만 적용이 가능
import sys
n = int(sys.stdin.readline())
count = 0
while n != 1:
#n이 1이면 n-1이 0이되니까 최소 1이될수있게 +1함
#4가지 과정중 제일 작은수
n = min(n-1, n//2, n//3, n//5)+1
count += 1
print(count)
#따라서 아래처럼 풀어야함!
import sys
x = int(sys.stdin.readline())
#DP 테이블 초기화
#0은 안치고 인덱스 1부터 접근할거기 때문에 30001로 설정
#d[n] = n을 1로 만드는 최소 연산의 수
d = [0]*30001
#d[7] = 1 + d[6] 처럼 작은 문제들로 큰문제를 해결할수 있는 구조
for i in range(2, x+1):
#현재의 수에서 1을 빼는 경우 (d[2]=1, d[1]=0)
d[i] = d[i-1]+1
#2,3,5의 공배수는 안되므로 if문으로 적어야함
#1을 뺀 경우 vs 2로 나눈경우
if i%2 == 0:
d[i] = min(d[i], d[i//2]+1)
#2로 나눈경우 vs 3으로 나눈 경우
if i%3 == 0:
d[i] = min(d[i], d[i//3]+1)
#3으로 나눈 경우 vs 5로 나눈 경우
if i%5 == 0:
d[i] = min(d[i], d[i//5]+1)
print(d[x])
- 보텀업 문제
- n가지종류 화폐
화폐 갯수를 최소한으로 이용해서
가치의 합이 m원이 되도록함 - 첫째줄에 n가지 화폐, 가치의 합이 주어짐
불가능할땐 -1출력 - 1<=n<=100
1<=m<=10000

import sys
# DP 테이블 초기화 (INF(무한)값으로 설정) (숫자는 크게 상관없음)
# m이 10000까지이기 때문에 10001 라는 값을 통해 화폐구성이 가능하지 않은 인덱스를 알게해줌
# d[m] = 총 화폐금액이 m 일때, 최소 화폐갯수
array = []
d = [10001]*(m+1)
n, m = map(int, sys.stdin.readline().split())
for _ in range(n):
array.append(int(sys.stdin.readline()))
# 보텀업
d[0] = 0
for i in range(n): # 화폐종류갯수
for j in range(array[i], m+1): # 화폐 단위 ~ 총 화폐금액
if d[j-array[i]] != 10001: # 현재 금액 - 화폐단위금액
# 10001을 다른값으로 변경
# 점화식을 이용해 푼다
d[j] = min(d[j], d[j - array[i]]+1)
if d[m] == 10001:
print(-1)
else:
print(d[m])
- n x m 크기의 금광
- m-1번에 걸쳐서 오른쪽위,오른쪽,오른쪽아래 중 하나의 위치로 이동해야함
채굴자가 얻을 수 있는 금의 최대 크기 - 첫째줄에 테스트케이스 갯수가 입력됨
1<= T <=1000 - 그다음 테스트 케이스갯수만큼 아래가 입력됨
n과(세로) m이(가로) 공백으로 구분되어 입력됨
1 <= n,m <= 20 - nxm 개의 위치에 매장된 금의 개수가 공백으로 구분되어 입력됨
1 <= 금의 개수 <=100 - 테스트 케이스마다 채굴자가 얻을수있는 금의 최대 크기 출력
# 테스트 케이스 만들기
for tc in range(int(input())):
# 금광 정보 입력하기
n, m = map(int, input().split()) # 세로, 가로
array = list(map(int, input().split())) # 일단 한줄로 받는다
# 2차원 테이블로 초기화 하기
# dp [i][j] 가는만큼
dp = []
index = 0
# 한줄로 받은 dp 자르기
for i in range(n):
dp.append(array[index:index + m])
index += m
# 다이나믹 프로그래밍 실행
for j in range(1, m): #1부터 시작. 맨왼쪽, 0이기 때문에
for i in range(n):
# 왼쪽 위에서 오는 경우
if i==0: left_up = 0 # 위로 움직일수가 없으니 0
else: left_up = dp[i-1][j-1] # 원래 위치에서 왼쪽 위
# 왼쪽 아래에서 오는 경우
if i == n-1: left_down = 0 #아래로 움직일수 없으니 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)
- n명의 병사 무작위로 나열됨
각 병사는 특정한 값의 전투력을 보유함.
병사를 배치할 때, 전투력이 높은 병사가 앞쪽에 오도록 내림차순으로 배치를 하고자 합니다
앞쪽에 있는 병사가 뒤쪽에있는 병사보다 높아야함 - 특정한 위치에 있는 병사를 열외시킴.
그러면서도 남아있는 병사의 수를 최대로 하고싶음. - 첫째줄에 병사의 수 n (1<= n <= 2000)
둘째줄에 병사의 전투력, 공백으로 구분되어 주어짐 - 첫째줄에 남아있는 병사의 수가 최대가 되도록 하기 위해서 열외시켜야 하는 병사의 수를 출력
- 이 문제의 아이디어
- 가장 긴 증가하는 부분 수열
전형적인 다이나믹 프로그래밍 문제의 아이디어 - 어떤 특정한 배열이 있다고 칠때, 인덱스마다 값이 증가하는 폭이 점점 커지는 값들이 모인 아이디어를 말함
- 본 문제는 가장 긴 감소하는 부분 수열을 찾는 문제 (내림차순이므로), 배열 순서를 뒤집거나 부등호를 반대로 해야함.
- 가장 긴 증가하는 부분 수열

n = int(input())
array = list(map(int, input().split()))
#순서를 뒤집어서 '최장 증가 부분 수열' 문제로 변환
array.reverse()
# 다이나믹 프로그래밍을 위한 1차원 DP 테이블 초기화
dp = [1]*n
#내림차순으로 구하기.(가장 긴 증가하는 부분 수열)
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))