원래 알고리즘 블로그 포스팅은 블로그 원정대의 일정에 맞춰 주 1회씩만 작성하려고 했었는데, 드디어 DP에 대한 감을 잡을 수 있었던 기념비적인 문제라 야심한 시각에 포스팅을 작성하게 되었다.
ACAYKP
CAPCAK
4
접근법은 다음과 같다.
- 두 문자열을 받아온다.
- 두 문자열의 크기 + 1을 가지는 DP라는 이름의 2차원 배열을 선언한다.
- 이때, dp에는 구하고자 하는 LCS의 길이를 저장한다.
- i와 j라는 임시 변수를 통해 2차원 loop으로 두 문자열의 각각의 문자를 탐색한다.
- 이때, 두 문자가 같을 경우는 직전 행 및 열의 값(= 직전 행 && 직전 열의 값)에 + 1을 함으로써 LCS를 1 증가시킨다.
- 두 문자가 같지 않을 경우는 직전 행 또는 열의 값(= 직전 행 || 직전 열의 값)과 각각 비교해서 큰 값을 가져온다.(= 더 큰 LCS 값으로 업데이트)
- 2차원 dp 배열의 마지막 값을 출력한다.
풀이 흐름은 다음과 같다.
- 왜 2차원 dp 배열을 선언해야 할까?
- dp 안의 데이터는 무엇을 담고 있을까?
- 왜 dp 배열의 크기를 +1만큼 선언해야 할까? (=
int[][] dp = new int[strSequence1.length() + 1][strSequence2.length() + 1];
)- 왜 문자가 같을 경우는
dp[i][j] = dp[i - 1][j - 1] + 1;
으로 선언이 될까?- 또, 문자가 다를 경우는 왜
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
와 같이 선언이 될까?
적고보니 사실상 DP
의 핵심 개념들을 잘 몰랐던 것 같다.
우선 각각의 질문들에 대해 정리를 해보자면 다음과 같다.
LCS
가 담겨져 있을 것이다. 왜냐하면 DP 배열을 통해 값을 도출해야 하니까?int[][] dp = new int[strSequence1.length()][strSequence2.length()];
과 같이 선언하고 진행해도 된다.하지만, 이럴 경우 아주아주 귀찮아진다.
우선 2차원 DP 배열을 선언할 경우 모양은 다음과 같은 n x n 크기의 정사각형 배열이 될 것이다.
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
이때 위와 같이 +1을 하지 않은 크기를 선언했을 경우, 이 상태에서 i - 1과 j - 1(-1을 하는 이유는 조건에 따라 이전 LCS의 값을 가져오기 위함이다.)을 가져올 때 첫 번째 행과 열에서 범위를 벗어나게 되므로 인덱스의 크기가 부족하다는 예외가 터진다.
따라서 이례적으로 첫 번째 열과 행에 1이라는 값들을 세팅해줘야 하는데,
0 1 1 1 1 1
1 0 0 0 0 0
1 0 0 0 0 0
1 0 0 0 0 0
1 0 0 0 0 0
이것이 귀찮기 때문에 그냥 +1만큼의 크기를 선언함으로써 바로 (2, 2) (= (1,1))이 원점이라는 가정 하에)부터 값을 채워나갈 수 있는 것이다.
4.와 5.는 이 문제의 핵심이다.
참고 그림은 다음과 같다.
위 그림을 보면 업데이트 된 LCS값을 주황색으로 마킹해두었다.
대표적으로 현재 LCS의 길이
라고 명시된 2를 보자. (= (2, 2))
해당 값은 "CAPC"와 "AC"를 비교했을 때, 누적된 LCS의 값인 2가 들어있다.
그리고 이 값은 왼쪽 대각선 상에 위치한 (1, 1)로부터 각 문자열의 문자를 하나씩 더 탐색한 위치이다. 또한, 각 문자가 동일한 순간이다.
즉, 문자가 동일한 순간
이므로 dp[i][j] = dp[i - 1][j - 1] + 1;
으로 표기가 되는 것이다.
반대로, 문자가 같지 않을 경우는 현재 값(= dp[i][j])을 기준으로 직전 열 또는 직전 행의 값 중 큰 값을 가져오면 되기 때문에 dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
으로 표기가 되는 것이다.
package silver.class4;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
public class LCS {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 입력부터 다 처리해놓고 고민할래~
String strSequence1 = br.readLine();
String strSequence2 = br.readLine();
int[][] dp = new int[strSequence1.length() + 1][strSequence2.length() + 1];
for (int i = 1; i <= strSequence1.length(); i ++) {
for (int j = 1; j <= strSequence2.length(); j ++) {
if (strSequence1.charAt(i - 1) == strSequence2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
System.out.println(dp[strSequence1.length()][strSequence2.length()]);
br.close();
}
}
DP는 정말정말 어려운 것 같다.
특히, 개념 상으로도 잘 와닿지 않는 개념이었다.
바텀업
또는 탑다운
은 뭔지 직접 좀 풀어보면서 느낄 수 있었다.재귀
에 익숙치 않은 탓인 것 같다. 그래도 위 문제를 스터디원 분과 함께 궁금한 부분은 직접 디버깅해보고, 이야기를 나눠보면서 직접 해결함으로써 조금 DP와 친해진 것 같다. (한 1/7 정도?)
당분간은 DP
와 백트래킹
을 집중적으로 알아볼 예정이다.
그러면 한 달 후에는 4/7 정도 알게 되지 않을까?