[TIL] 동적 계획법(DP: Dynamic Programming)에 대해
오늘도 백준에서 문제를 풀던 도중, “동적 계획법” 개념이 필요한 문제에 자주 막히게 되었습니다
이 개념에 대해 잘 모르는 채로 풀고 있었기 때문이라 생각되어 DP에 대해 한 번 알아보고 문제를 풀어보겠습니다
1. 동적 계획법
동적 계획법 : Dynamic Programming (DP)
‘동적 계획법’은 프로그래밍 문제에 자주 출현되는 개념 중 하나입니다
동적 계획법의 큰 특징은 큰 문제를 작게 나누어 풀이를 한다는 점이라 볼 수 있겠습니다
가장 대표적인 예시로는 ‘피보나치(Fibonacci)’ 수열이 있습니다
피보나치 수열은 1, 1, 2, 3, 5, 8, 13, … 값을 가지며 n > 3인 n번째 항 F_n에 대해
앞의 두 항의 합 F_n = F_(n-2) + F_(n-1)으로 나타낼 수 있습니다
1-1. DP / Top-Down
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int[] arr;
public static int fibonacci(int n) {
if (n <= 2)
return 1;
if (arr[n] == 0)
arr[n] = fibonacci(n-2) + fibonacci(n-1);
return arr[n];
}
public static void main(String[] args) {
int n = 10; // n을 입력받은 경우
int result;
arr = new int[n+1]; // 피보나치 수열의 index를 0이 아닌 1부터 진행하므로 1 추가
result = fibonacci(arr);
System.out.println("피보나치 수열의 " + n + "번째 값 : " + result);
// 출력 : "피보나치 수열의 10번째 값 : 55"
}
평범하게 재귀 함수로 푸는 문제처럼 보이지만 여기서는 static int[] arr;
을 사용하고 있습니다
그리고 fibonacci
가 실행되면 배열에 이미 계산된 값을 저장하면서 굳이 했던 계산을 더 할 필요가 없어졌구요
위 예시의 재귀 함수처럼 큰 문제를 풀기 위해 아래로 내려가며 풀어 나아가는 방법을 ‘Top-Down’이라 합니다
배열을 사용하지 않고 재귀 함수로 푼다면 같은 함수를 여러 번 실행시키기에 중복 연산이 많아지면서
시간 복잡도(Time Complexity)마저 매우 커지기 때문에 비효율적인 알고리즘이 됩니다
1-2. Bottom-Up
Top-Down의 반대인 Bottom-Up은 작은 문제부터 구하고자 하는 문제까지 올라가며 푸는 방법으로
재귀 함수를 사용하던 Top-Down과는 다르게 Bottom-Up은 반복문으로 구현하는 경우가 일반적입니다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static int fibonacci(int n) {
int[] arr = new int[n+1];
// 피보나치 수열의 1, 2번째 항 초기화
arr[1] = 1;
arr[2] = 1;
for (int i=3; i<=n; i++)
arr[i] = arr[i-1] + arr[i-2];
return arr[n];
}
public static void main(String[] args) {
int n = 10; // n을 입력받은 경우
int result = fibonacci(10);
System.out.println("피보나치 수열의 " + n + "번째 값 : " + result);
// 출력 : "피보나치 수열의 10번째 값 : 55"
}
Bottom-Up의 경우, 수열을 사용하기 위해 필요한 항만 미리 대입한 뒤
반복문으로 차근차근 올라가면서 이전에 저장해둔 값으로 계산을 하다가 큰 문제를 풀게 됩니다
2. DP를 이용해 문제 하나 더 풀어보기
2-1. 백준 11053 - 가장 긴 증가하는 부분 수열
알고리즘 분류가 ‘다이나믹 프로그래밍(DP)’인 백준 11053번 - 가장 긴 증가하는 부분 수열을 풀어봅시다
- 문제
- 수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.
- 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이고, 길이는 4이다.
- 입력
- 첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000)이 주어진다.
- 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ Ai ≤ 1,000)
2-2. 문제 풀이
1
int[] dp = new int[N];
조건(증가하는 부분)에 만족하는 수열의 개수를 dp[N]
에 저장해봅시다
1
2
3
4
for (int i=0; i<N; i++) {
// 부분 수열의 개수는 반드시 1개 포함이므로 1 대입
dp[i] = 1;
}
이제 i = [0, N) 범위로 반복문을 실행시키면서, [0, i] 범위 내에 오름차순이 얼마나 있는지 확인해볼 것입니다
예를 들어, {10, 20, 10, 30}이면 다음처럼 확인해야 합니다
- 10
- 10 -> 20
- 10
- 10 -> 20 -> 30
1
2
3
4
5
6
7
8
9
10
for (int i=0; i<N; i++) {
dp[i] = 1;
// A[j]가 A[i]보다 작고, dp[i]가 dp[j+1]보다 작으면
// dp[i]에 dp[j] + 1 대입
for (int j=0; j<i; j++) {
if (A[j] < A[i] && dp[i] < dp[j] + 1)
dp[i] = dp[j] + 1;
}
}
다시 dp에 값을 넣으면서 확인해보자면
- dp[0] : 1
- dp[1] : 2 // A[0] < A[1]이고 dp[1] < dp[0]+1 이므로 dp[0] + 1 대입
- dp[2] : 1
- dp[3] : 3 // A[1] < A[3]이고 dp[3] < dp[1]+1 이므로 dp[1] + 1 대입
- j = 2일 때, (dp[3] < dp[2]+1) -> (3 < 1+1)은 거짓이므로 실행X
{10, 20, 10, 30}에서 30을 탐색하는 타이밍에 {10, 20}이 제일 많은 수열이었으므로 2+1 -> 3 대입
즉, 순회하면서 이전의 요소들 중 dp가 가장 높은 값보다 더 높으면 거기에 +1을 적용하게 되는 것입니다
2-3. 답안
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.*;
import java.util.Arrays;
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringTokenizer st;
// 숫자 입력
int N = Integer.parseInt(br.readLine());
int[] A = new int[N];
st = new StringTokenizer(br.readLine());
for (int i = 0; i < N; i++)
A[i] = Integer.parseInt(st.nextToken());
// Dynamic Programming을 위한 배열 추가 (Bottom-Up 방식)
int[] dp = new int[N];
for (int i=0; i<N; i++) {
dp[i] = 1;
// 0번째부터 i번째 까지 수열 탐색
for (int j=0; j<i; j++) {
if (A[j] < A[i] && dp[i] < dp[j] + 1)
dp[i] = dp[j] + 1;
}
}
// dp중 최댓값 출력
bw.write(Arrays.stream(dp).max().getAsInt() + "\n");
bw.flush();
br.close();
bw.close();
}
}
최댓값을 출력하는 부분은 알맞게 사용하면 되겠습니다
저는 간단하게 스트림의 max()
를 사용하여 최댓값을 추출하였습니다