지난번 동적 계획법 1에서도 언급했듯이 동적 계획법은 특정 문제를 풀기위한 알고리즘이 아닌 문제를 효율적으로 풀기 위한 방법론이기 때문에 그 난이도가 문제마다 천차만별이며 적용하는 방법 또한 굉장히 다양하다. 그렇기에 문제에 따라서는 동적 계획법 문제임을 알고도 해법을 찾지 못하는 경우도 많다. 이번에는 동적 계획법을 사용하여 해결 가능한 조금 더 어려운 문제들을 풀어본다.
1. 파일 합치기(https://www.acmicpc.net/problem/11066)
소설을 구성하는 파일의 크기가 챕터 순으로 주어지고 두 파일을 합쳐 하나의 파일로 만드는 작업의 비용이 두 파일의 크기의 합이라고 할 때, 챕터 순서를 유지하면서 파일을 모두 합치기 위해 필요한 최소비용을 구하는 문제이다. 단순히 가장 작은 두 페이지를 합쳐나가는 방식이라면 heap을 사용하여 간단하게 구현할 수 있지만, 이 문제의 경우 챕터의 순서를 유지해야하나는 조건이 있기 때문에 인접한 두 파일만을 합칠 수 있다. 이 문제는 동적 계획법으로 접근해야한다.
일반적으로 문제해결에 동적 계획법을 적용할 때, 가장 중요한 것은 점화식을 찾는 것이다. i번부터 j번까지의 파일을 하나로 합친 값을 dp[i][n] 이라고 하자. 파일은 두 개씩만 합칠 수 있기 때문에, dp[i][j]은 i번부터 k번까지의 파일, k+1번부터 j번까지의 파일을 각각 합친 두 파일을 합친 경우 중 최솟값에 두 파일의 크기를 합친 값, 즉 i번부터 j번 까지의 파일 크기의 합을 더한 것과 같다고 생각할 수 있다.
예를들어 1번부터 5번 파일까지의 크기가 주어졌을 때, dp[1][5] 를 구하는 경우의 수는 다음 그림과 같다.
위 네 가지 경우 중 최솟값이 1번부터 5번까지의 파일을 합치는 데 드는 최소비용이 된다. 이 점화식을 Python 코드로 표현하면 다음과 같다.
dp[i][j] = min([dp[i][k] + dp[k+1][j] for k in range(i, j)]) + sum(filesize[i:j+1])
점화식을 찾아냈으면 나머지는 간단하다. dp 배열의 초기값을 설정한 뒤 반복문으로 나머지를 채워나가면 답을 찾을 수 있다. 이를 구현한 코드는 다음과 같다.
import sys
input = sys.stdin.readline
def sol11066():
# 각 케이스의 답변을 저장할 리스트
answer = []
for T in range(int(input())):
# 파일 갯수
K = int(input())
# 파일 크기 리스트
filesize = list(map(int, input().split()))
# dp[i][j] 는 i부터 j 까지의 파일을 합치는데 드는 최소비용
# dp[i][i] 는 파일 갯수가 한 개이기 떄문에 합치는 비용 0 - 초기화 별도로 필요없음
dp = [[0] * K for _ in range(K)]
# acc[i] 는 인덱스 i 까지의 파일 크기의 합
# i부터 j 까지의 파일 크기의 합을 구할 경우 acc[j] - acc[i-1] 로 구할 수 있음
# i가 0일 경우를 대비하여 크기를 K+1로 함
acc = [0] * (K + 1)
acc[0] = filesize[0]
for i in range(1, K):
acc[i] = acc[i - 1] + filesize[i]
# i와 j의 차
for margin in range(1, K):
# 시작점 i
# i + margin 이 K를 넘지 않아야 하기 때문에 K-margin-1 까지
for i in range(K - margin):
# 끝점 j
j = i + margin
# i부터 j 까지의 파일을 두 개의 파일까지 합치기 위한 최소비용은 dp[i][m] + dp[m+1][j] 의 최솟값
# 두 파일을 하나의 파일로 합치기 위해 필요한 비용은 i부터 j 까지의 파일의 크기의 합
dp[i][j] = min([dp[i][m] + dp[m + 1][j] for m in range(i, j)]) + acc[j] - acc[i - 1]
# answer 리스트에 정답 저장
answer.append(dp[0][-1])
# 출력 형식에 맞춰 정답 리스트 반환
return '\n'.join(map(str, answer))
위 코드는 O(K^3)의 시간복잡도로 문제를 해결할 수 있다. 이를 추가적으로 최적화하는 특수한 방법도 있지만 아직은 다루지 않는다.
2. 행렬 곱셈 순서(https://www.acmicpc.net/problem/11049)
크기가 NxM 인 행렬 A와 MxK 인 행렬 B를 곱하기 위해 필요한 곱셈 연산의 수는 총 NxMxK 번일 때, 행렬 N개의 크기가 곱할 순서대로 주어졌을 때, 곱셈연산 수의 최솟값을 구하는 문제.
import sys
input = sys.stdin.readline
def sol11049():
# 곱할 행렬의 갯수
n = int(input())
# 행렬의 크기 리스트
mats = [tuple(map(int, input().split())) for _ in range(n)]
# dp[i][j] 는 i 행렬부터 j 행렬 까지 곱하기 위한 곱셈 연산 수의 최솟값
dp = [[0]*n for _ in range(n)]
# i와 j의 차
for margin in range(1, n):
# 시작점 i
for i in range(n-margin):
# 끝점 j
j = i + margin
# 행렬 i 부터 j 까지 곱하는데 필요한 곱셈 연산 수 의 최솟값
dp[i][j] = min([dp[i][k] + dp[k+1][j] + mats[i][0] * mats[k][1] * mats[j][1] for k in range(i, j)])
# 모든 행렬을 곱하는데 필요한 곱셈 연산 수의 최솟값 반환
return dp[0][-1]
1번의 파일 합치기와 거의 동일한 방식으로 접근하면 간단하게 해결 가능한 문제이다.
'코딩테스트 > 알고리즘' 카테고리의 다른 글
#27 동적 계획법(Dynamic Programming) 2 - 심화(3) (0) | 2021.09.02 |
---|---|
#26 동적 계획법(Dynamic Programming) 2 - 심화(2) (0) | 2021.09.01 |
#24 비트 마스크(Bit Mask) (0) | 2021.08.30 |
#23 기하 - 원의 교차범위 면적 구하기 (0) | 2021.08.28 |
#22 기하 - CCW(Counter-ClockWise) 2 (0) | 2021.08.27 |