안녕하세요
인포돈 입니다.
백준 알고리즘 9251번 풀이입니다.
* 참고사항
- 개발환경은 eclipse을 기준으로 작성되었습니다.
- java언어를 이용하여 문제를 풀이합니다.
- 알고리즘 문제는 풀이를 보고 해답을 찾는 것도 중요하지만 무엇보다 스스로 풀이를 시도해봐야 합니다!!
(해당 풀이를 보기 전 충분히 문제에 대해 고민해봐야 합니다!)
- 해당 풀이 말고도 수많은 풀이 방법이 존재합니다.
백준 9251 (LCS)
문제
LCS(Longest Common Subsequence, 최장 공통부분 수열) 문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다.
예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다.
입력
첫째 줄과 둘째 줄에 두 문자열이 주어진다. 문자열은 알파벳 대문자로만 이루어져 있으며, 최대 1000글자로 이루어져 있다.
출력
첫째 줄에 입력으로 주어진 두 문자열의 LCS의 길이를 출력한다.
입력 예제
ACAYKP
CAPCAK
출력 예제
4
성공코드
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.StringTokenizer;
public class Main {
static char[] str1;
static char[] str2;
static Integer[][] dp;
static int max = Integer.MIN_VALUE;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
str1 = br.readLine().toCharArray();
str2 = br.readLine().toCharArray();
dp = new Integer[str1.length][str2.length];
System.out.println(lcs(str1.length-1, str2.length-1));
}
public static int lcs(int a, int b) {
if(a<0||b<0)
return 0;
if(dp[a][b]==null) {
dp[a][b]=0;
if(str1[a] == str2[b])
dp[a][b] = lcs(a-1, b-1)+1;
else
dp[a][b] = Math.max(lcs(a-1,b), lcs(a,b-1));
}
return dp[a][b];
}
}
LCS문제는 DP문제에 있어서 가끔 나오는 문제 중 하나입니다. 바로 가장 긴 공통된 부분 수열 문제로 이러한 문제를 풀 때 규칙을 잘 발견해야 합니다. 기본적으로 첫 시도는 표로 만들어 보는 것이 가장 규칙이 잘 보이는 방법입니다. 해당 문제는 규칙만 찾는다면 쉽게 풀이할 수 있음으로 STEP을 따라가며 이해해 봅시다.
알고리즘 흐름도
입력 받기 -> 데이터 변환하기 -> 재귀 하기 (규칙 찾기) -> 출력하기
STEP1 규칙 찾아보기
이것만 기억하시면 쉽게 찾아볼 수 있습니다. LCS는 규칙을 찾을 때 표로 확인해 보자. 기본적으로 공통된 부분 수열을 찾기 위해서는 2개의 비교대상이 필요합니다. 그렇다면 행/열로 각 아이템들을 나열해서 확인해 볼 수 있죠.
A | C | A | Y | K | P | |
C | ||||||
A | ||||||
P | ||||||
C | ||||||
A | ||||||
K |
자 첫 시작부터 하나씩 따라가 보도록 하죠. 2열의 A부터 가보도록 하죠
A | C | A | Y | K | P | |
C | 0 | |||||
A | 1 | |||||
P | 1 | |||||
C | 1 | |||||
A | 1 | |||||
K | 1 |
A하나로 중복되는 부분 수열을 수한다면 이와 같이 표현할 수 있죠.
C
CA
CAP
CAPC
CAPCA
CAPCAK
이렇게 부분 수열이 있다면, A 하나가 들어있는 부분 수열의 길이의 최대는 1개가 되죠.
(왜 A가 2개인데 그렇지 않나요? 여기서 저희가 구하는 것은 개수가 아니라 길이의 최대입니다.
즉, 첫 번째 A이든 두 번째 A이든 길이가 1 임으로 변동이 없는 것입니다.)
A | C | A | Y | K | P | |
C | 0 | 1 | ||||
A | 1 | 1 | ||||
P | 1 | 1 | ||||
C | 1 | 2 | ||||
A | 1 | 2 | ||||
K | 1 | 2 |
두 번 째는 단지 C가 아니라 {A, C}라고 생각해야 합니다. 그렇다면 이 수열의 부분 수열은 생각해 보면 A, C, {A, C}가 되죠 그러면 이제 차차 확인해봅시다.
C : C가 들어있다. (최대 길이 1)
CA : C나 A가 들어있다. (최대 길이 1)
CAP : C나 A가 들어있다. (최대 길이 1)
CAPC : C, A, {A, C}가 들어있다. (최대 길이 2)
CAPCA : C, A, {A, C}가 들어있다. (최대 길이 2)
CAPCAK : C, A, {A, C}가 들어있다. (최대 길이 2)
이런 식으로 사고해야 한다. 이때 {A, C}의 경우 순서도 고려해 줘야 합니다. 애초에 문제가 순서를 바꾸면 안 되기 때문이죠. 따라서 {C, A}는 해당 수열의 부분 수열이 아니게 되죠
A | C | A | Y | K | P | |
C | 0 | 1 | 1 | 1 | 1 | 1 |
A | 1 | 1 | 2 | 2 | 2 | 2 |
P | 1 | 1 | 2 | 2 | 2 | 3 |
C | 1 | 2 | 2 | 2 | 2 | 3 |
A | 1 | 2 | 3 | 3 | 3 | 3 |
K | 1 | 2 | 3 | 3 | 4 | 4 |
이런 식으로 나머지 부분 수열을 하나씩 입력해 보면, 다음과 같이 채워질 수 있습니다. 여기서 규칙을 파악해야 합니다.
처음에 느낌이 오지 않으실 수도 있습니다. 그렇다면 힌트를 하나 주자면 공통된 문자가 있다면 변화가 있고 그렇지 않다면 그 숫자는 어디서 온 숫자일까요?
결과는 바로 자신과 같은 문자가 있을 경우 +1 그렇지 않을 경우 열-1의 값과 행 -1 값 중 높은 값을 가져오죠
그런데!! 여기서 한 가지 또 함정이 있습니다. 같은 문자가 있을 경우 어떤 수를 +1 해야 하냐는 말입니다. 이를 위해서는 간략한 생각을 하나 해야 합니다.
(0, 1)을 본다면 C와 {A, C} 공통분모인 C를 의미하여 1의 값이 들어옵니다.
(1, 2)를 본다면 {C, A}와 {A, C, A} 공통 수열인 {C, A}가 되어 2의 값이 들어옵니다.
즉, 모든 수열에 A가 추가된 상황이 된 것이죠. 그 의미는 즉 대각선에 있는 값에 +1을 해줘야 한다는 의미입니다. 행과 또는 열에서만 +1을 할 경우 입력 예제와는 다른 값들이 들어오게 된다면, 오류가 나기 때문이죠
이런 규칙을 기억하시고 다음 STEP으로 넘어가 보도록 하죠
STEP2 재귀 구현하기
규칙을 이해하셨다면 굉장히 쉽습니다.
public static int lcs(int a, int b) {
if(a<0||b<0)
return 0;
if(dp[a][b]==null) {
dp[a][b]=0;
if(str1[a] == str2[b])
dp[a][b] = lcs(a-1, b-1)+1;
else
dp[a][b] = Math.max(lcs(a-1,b), lcs(a,b-1));
}
return dp[a][b];
}
if(a <0||b <0)
해당 조건문은 A, B과 음의 값이라면 0을 반환해 줍니다. 이런 조건을 넣어준 이유는 바로 제가 구현한 방식이 TOP-DOWN 방식이기 때문입니다. 해당 조건이 없다면 끝없이 A, B가 내려가기 때문에 인덱스 오류가 나게 되죠.
if(dp [a][b]==null)
해당 조건문은 누구나 쉽게 알 수 있는 메모 이제이 션 기법을 사용하기 위한 조건문입니다. NULL값이라면 재귀를 실행하라는 의미입니다.
dp [a][b]=0;
기본적으로 0으로 초기화를 해줍니다. (밖에서 초기화하지 않은 이유는 메모 이제이 션 NULL을 이용하기 위해서)
맨 처음 C와 A와 같은 상황을 조정해주기 위해서죠
if(str1 [a] == str2 [b])
dp [a][b] = lcs(a-1, b-1)+1;
else
dp [a][b] = Math.max(lcs(a-1, b), lcs(a, b-1));
이문제의 핵심이죠 간단해요 각 문자열이 같다면 왼쪽 대각선 위에서 1을 더합니다. 아니라면 행-1 / 열 -1 중에서 높은 값을 넣어줍니다. 이런 식으로 최상위에서 최하위로 가면서 재귀를 돌며 값을 도출합니다.
그 후
System.out.println(lcs(str1.length-1, str2.length-1));
이런 식으로 호출해 준다면 끝~!
* 여담으로 toCharArray()는 string을 char []로 변환해주는 메서드입니다.
'알고리즘 > 백준알고리즘' 카테고리의 다른 글
[백준알고리즘-JAVA]12865번 풀이(평범한 배낭) - 초보도 이해하는 풀이 (1) | 2021.08.31 |
---|---|
[백준알고리즘-JAVA]1912번 풀이(연속합) - 초보도 이해하는 풀이 (2) | 2021.08.30 |
[백준알고리즘-JAVA]2565번 풀이(전깃줄) - 초보도 이해하는 풀이 (1) | 2021.08.28 |
[백준알고리즘-JAVA]11054번 풀이(가장 긴 바이토닉 부분 수열) - 초보도 이해하는 풀이 (0) | 2021.08.27 |
[백준알고리즘-JAVA]11053번 풀이(가장 긴 증가하는 부분 수열) - 초보도 이해하는 풀이 (0) | 2021.08.26 |