분할 정복과 같이 접근하는 방식을 배웠다. 점화식을 세우기까지가 헬이고 세우면 코딩은 별 거 아님… 유용하게 쓰일 방식일 듯
Input
프로그램은 표준 입력에서 입력 데이터를 받는다. 프로그램의 입력은 T개의 테스트 데이터로 이루어져 있는데, T는 입력의 맨 첫 줄에 주어진다.
각 테스트 데이터는 두 개의 행으로 주어지는데, 첫 행에는 소설을 구성하는 장의 수를 나타내는 양의 정수 K (3 ≤ K ≤ 500)가 주어진다.
두 번째 행에는 1장부터 K장까지 수록한 파일의 크기를 나타내는 양의 정수 K개가 주어진다. 파일의 크기는 10,000을 초과하지 않는다.
1 | import sys |
DP
어떻게 접근해야 하는지 막막했던 문제다. 일단 일반화를 어떻게 해야하지.. 싶었는데, 일단 정리해본 조건은
- 순서가 뒤바뀌면 안 됨
- 큰 수는 최소한으로 더하기 = 작은 수를 여러번 반복해서 더하게끔
이를 토대로 작성해본 조건식은 다음과 같다.
1 | if s[i-1] > s[i] and s[i] < s[i+1]: |
결과적으로 정답과 크게 벗어나진 않는다. 다만 이 조건을 DP에 어떻게 적용하는가에서 헤맸다는 점?
일단 내가 놓친 부분은
- DP 배열이 2차원 배열이라는 것
- 반복문의 조건
이었다. Ci부터 Cj까지 더했을 때 최소값을 갱신하는 것이므로 min
을 사용하는 정도만 유추했지만, 이걸 어떻게 적용해야 할지 모르겠는 느낌. 갱신이므로 중첩 반복문이 들어갈 것이고, i번째 ~ j번째 의 최소값을 저장해야 하므로 2차원 배열을 사용한다.
1 | dp = [[0]*k for _ in range(k)] |
반복문을 다음과 같이 설정하면 된다.
작은 단위부터 시작해 점차 gap을 키우면서 start부터 end까지 dp 값을 갱신한다.
1
for gap in range(1, k):
dp[start][end]
는 부분 수열을 포함해 전체 수열까지 가능하다.1
2
3
4for start in range(k-1):
end = start + gap
if end == k:
breakdp를 0으로 세팅하고 갱신하는데, min을 사용하므로 현재 dp 값을 inf로 설정한다.
1
dp[start][end] = sys.maxsize
그리고 gap만큼 start부터 증가시키면서 dp값을 갱신한다. 조건은
- Ci~Cj까지 최소 축적합 =
dp[i][j]
- 점화식:
dp[i][j] = min(dp[i][j], dp[i][k]+dp[k+1][j]) + sum(s[i:j+1])
1
2for i in range(gap):
dp[start][end] = min(dp[start][end], dp[start][start+i] + dp[start+(i+1)][end] + sum(s[start:end+1]))근데 여기서 재밌는 건,
sum
을 쓰면 시간 초과가 뜬다는 점. 아래와 같이 딕셔너리 를 활용해 (sum의) 반복문을 줄여야 한다.- Ci~Cj까지 최소 축적합 =
1 | cumsum = {-1: 0} |
1 | for i in range(gap): |
크누스 최적화
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j]) + sum(s[i:j+1])
와 같은 점화식이 사용되는 DP 문제의 경우, 크누스 최적화 방식을 사용하여 시간복잡도를 **O(n^3)에서 O(n^2)**로 줄일 수 있다. 조건은
A[i][j−1] ≤ A[i][j] ≤ A[i+1][j]
dp[i][k⋆] + dp[k⋆][j]
를 최소화하는 가장 작은 i<k⋆<j 의A[i][j]
C[a][c] + C[b][d] ≤ C[a][d] + C[b][c]
(a≤b≤c≤d)C[b][c] ≤ C[a][d]
(a≤b≤c≤d)
.
.
.
Output
프로그램은 표준 출력에 출력한다. 각 테스트 데이터마다 정확히 한 행에 출력하는데, 모든 장을 합치는데 필요한 최소비용을 출력한다.
1 | sys.stdout.write(str(dp[0][k-1])+'\n') |