지난 포스팅에 이어 다이나믹 프로그래밍 문제들을 많이 풀었다. 푼 문제가 너무 많은데 비슷한 유형도 많아서, 풀면서 어려움이 있었던, 혼자 해결하지 못했던 문제들만 집중적으로 복습 후 포스팅하려고 한다.
점화식 유도가 엄청 쉬운 문제들만 풀다가 처음 접한 조금 까다로운 문제였다. 처음엔 접근 방법을 생각해내지 못했다.
지금까지의 최대합이 자신보다 작으면 이어갈 필요가 없다. 위 과정을 통해 D 배열에는 각 원소(arr[i])가 마지막인 연속합 중의 최댓값이 들어가게 되고, 그 중 최댓값을 찾아 반환하면 된다.
package dynamic.review;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;
public class BOJ1912 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int[] arr = new int[n + 1];
int[] d = new int[n + 1];
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 1; i < n+1; i++) {
arr[i] = Integer.parseInt(st.nextToken());
}
d[1] = arr[1];
long max = arr[1];
for (int i = 2; i < n+1; i++) {
if (d[i-1] + arr[i] > arr[i]) {
d[i] = d[i - 1] + arr[i];
} else d[i] = arr[i];
max = Math.max(d[i], max);
}
System.out.println(max);
}
}
이전 문제의 응용된 형태였다. 이전 문제와 달라진 점은 수열이 연속될 필요는 없고, 증가하는 형태여야한다.
N이 최대 1000이기 때문에 가능했다. d[i] 값을 저장할 때, 자신 이전의 최댓값들을 모두 검사하며 이전의 원소보다 증가하는 지 확인한 뒤 최댓값을 갱신해주면 된다.
package dynamic.review;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;
public class BOJ11055 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int[] arr = new int[n + 1];
int[] d = new int[n + 1];
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 1; i < n + 1; i++) {
arr[i] = Integer.parseInt(st.nextToken());
}
d[1] = arr[1];
for (int i = 2; i < n + 1; i++) {
d[i] = arr[i];
for (int j = 1; j < i; j++) {
if (arr[i] > arr[j] && d[i] < d[j] + arr[i]) d[i] = d[j] + arr[i];
}
}
System.out.println(Arrays.toString(d));
}
}
이전 문제와 달라진 점은 최대합이 아닌, 최대 원소의 길이를 구해야한다는 것이다. d 배열에 합이 아닌 부분수열의 길이를 저장하면 될 것이다.
package dynamic.review;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;
public class BOJ11053 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
if (n == 1) {
System.out.println(1);
return;
}
int[] arr = new int[n + 1];
int[] d = new int[n + 1];
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 1; i < n+1; i++) {
arr[i] = Integer.parseInt(st.nextToken());
}
d[1] = 1;
int max = -1;
for (int i = 2; i < n + 1; i++) {
d[i] = 1;
for (int j = 1; j < i; j++) {
//최댓값이 아닌 원소 개수이므로 1을 더해준당.
if (d[j] + 1 > d[i] && arr[j] < arr[i]) d[i] = d[j] + 1;
}
if (d[i] > max) max = d[i];
}
System.out.println(max);
}
}
퇴사 문제에서 N의 범위가 늘어나서 O(N^2) 풀이로는 풀 수 없었다. O(n)의 점화식을 도저히 생각해낼 수 없어 다른 사람의 풀이를 참고했다.
퇴사 1 풀이
O(n)의 접근법은 이렇다. 자기 이전의 D들로부터 얻어오는 것이 아니라, 모든 날짜에서 T[i]만큼 지난 날짜에 D[i] + P[i]를 때려넣는다. (최대일 때만)
퇴사2 풀이
package dynamic;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;
public class BOJ15486 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int[] T = new int[n + 1];
int[] P = new int[n + 1];
for (int i = 1; i < n + 1; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
T[i] = Integer.parseInt(st.nextToken());
P[i] = Integer.parseInt(st.nextToken());
}
int[] d = new int[n + 2];
int max = -1; //현재 검사한 날짜 중 최대 수익. 중간에 일을 하지 않는 날이 있으면 0인 원소가 생김 -> update
for (int i = 1; i < n + 1; i++) {
if (d[i] < max) d[i] = max;
else max = d[i];
if (i + T[i] > n + 1) continue;
d[i + T[i]] = Math.max(d[i + T[i]], d[i] + P[i]);
}
max = Math.max(max, d[n + 1]);
System.out.println(max);
}
}
주의할 점은 max 변수다. 단순히 최댓값을 저장하는 것 뿐 아니라, 이후의 d에 접근할 때마다 d가 max보다 작으면 갱신시켜줘야한다. d[i+T[i]]를 갱신하는 과정은 모든 날에 상담을 하는 경우만을 cover한다.
예를 들어, 5일에 상담이 끝난 뒤에, i=5의 상담을 바로 진행하는 경우 말고, 5일은 건너뛰고 6일의 상담을 진행하는 경우가 있을 수 있다. 하지만 상담은 5일에 끝났기 때문에 d[5]만이 갱신되고, 6일에 끝나는 상담이 존재하지 않기 때문에 d[6] = 0으로 남아있다. 6일에 이미 얻은 수익의 최댓값은 d[5]보다 크거나 같은 것이 당연한데도 말이다.
마지막으로 n일에 끝나는 일은 d[n+1]에 저장되므로, max와 비교해서 반환해준다.
풀이를 전혀 구상해내지 못했는데, 다른 풀이를 참고해 LCS 알고리즘을 확인하고 나니 처음 봤을 땐 못 푸는게 당연한 문제구나 싶었다.
LCS는 두 수열의 가장 긴 공동의 부분 수열을 의미한다.
배열의 정의는, D[i][j] = arr1의 0~i 배열과, arr2의 0~j 배열의 LCS이다. 직접 2차원 배열을 채워나가며 점화식을 유도해보니 이해가 됐다. 예제를 그려보고 점화식을 직접 유도하는 것이 dp를 푸는 데 정말 효과적인 방법이라고 느꼈다. 점화식은 다음과 같다.
1번의 예를 들어보자면, ACA와 CAPCA에서 마지막 원소가 A로 같은 것을 확인했다. 그렇다면 ACA와 APCA의 LCS는, AC와 CAPC의 LCS에 하나의 공통원소가 추가된 것이다. 따라서 D[3][5] = D[2][4] + 1이 된다.
2번 조건 같은 경우는, ACA와 CAPC를 비교하고 나서 마지막 원소가 다른 것을 확인했다. 그렇다면 이전까지 구한 값 중에 최댓값을 취한다. AC와 CAPC, 또는 ACA와 CAP의 LCS가 ACA와 CAPC의 LCS가 될 것이다.
package dynamic;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.util.Arrays;
public class BOJ9251 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
char[] a = br.readLine().toCharArray();
char[] b = br.readLine().toCharArray();
int[][] d = new int[a.length + 1][b.length + 1];
for (int i = 1; i < a.length+1; i++) {
for (int j = 1; j < b.length+1; j++) {
if (a[i-1] == b[j-1]) {
d[i][j] = d[i - 1][j - 1] + 1;
} else {
//여긴 d[i-1][j]랑 d[i][j-1]중에 큰걸루 넣어야함.
//자기 이전의 수열들 사이의 경우의 수는 모두 포함하기 때무넹
d[i][j] = Math.max(d[i - 1][j], d[i][j - 1]);
}
}
}
for (int i = 0; i < d.length; i++) {
System.out.println(Arrays.toString(d[i]));
}
}
}
처음에 접근한 풀이는, 배열 D[i] = i원을 만드는 동전 조합의 수로 설정하고, 모든 동전에 대해서 D[i] += D[i-coin]했다. 하지만 점화식을 이렇게 세우면, 조합이 아니라 순열을 얻게 된다는 것을 깨달았다.
예를 들어, 동전의 종류가 2, 3인 상태에서 D[5]를 구한다고 할 때, D[5] += D[2] + D[3]이 되는데, 이것은 2+3과, 3+2 두 개의 과정을 모두 포함한다. -> 조합이 아니다.
이것의 해법은, D[i]의 i가 아닌, 동전을 중심으로 쌓아올리는 방식으로 푸는 것이다.
이렇게 동전을 중심으로 D에 경우의 수를 쌓아나가면 해당 동전의 개수가 의도된대로 더해진다.
package dynamic;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.StringTokenizer;
public class BOJ9084_2 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int t = Integer.parseInt(br.readLine());
for (int i = 0; i < t; i++) {
int n = Integer.parseInt(br.readLine());
int[] coins = new int[n];
StringTokenizer st = new StringTokenizer(br.readLine());
for (int j = 0; j < n; j++) {
coins[j] = Integer.parseInt(st.nextToken());
}
int money = Integer.parseInt(br.readLine());
int[] d = new int[money + 1]; // d[i] = i원을 만드는 동전의 조합 수
d[0] = 1;
// for (int coin : coins) {
// d[coin]++;
// }
//이 방법은 coin부터 더하니까 coin 이전의 숫자들에 coin을 더하는 계산이 누락됨.
// for (int coin : coins) {
// if (coin > money) continue;
// d[coin]++;
// for (int j = coin; j + coin < money+1; j++) {
// d[j + coin] += d[j]; //j를 만드는 조합에다가 coin을 추가 -> 이러면 저번 풀이랑 똑같잖아..
// }
// System.out.println(Arrays.toString(d));
// }
for (int coin : coins) {
for (int j = coin; j < money + 1; j++) {
d[j] += d[j - coin];
}
}
System.out.println(d[money]);
// System.out.println(Arrays.toString(d));
}
}
}
d[0]이 1이 되어야하는 이유는 d[j-coin]을 참조하기 때문에 초기값을 잡아줄 필요가 있기 때문이다.
DP는 다른 알고리즘들에 비해서 많이 어려웠던 것 같다. 문제를 풀면서 느낀 가장 중요한 점은,
연습문제 출처 : Encrypted-def github