1. LCS란
- 최장 공통 부분수열(L.C.Subsequence) 혹은 최장 공통 문자열 (L.C.Substring)을 말한다.
- 각각의 차이는 아래 그림과 같다.
- 최장 공통 부분수열은 BCDF, BCDE가 될 수 있다. 부분 수열은 분자 사이를 건너뛰어 공통이 되면 가장 긴 문자열을 찾으면 된다.
- 최장 공통 문자열은 BCD로 부분 문자열이 아니기 때문에 한번에 이어져 있는 문자열만 가능하다.
1. 최장 공통 문자열(Longest Common Substring)
class Solution_0045 {
public static int solution(String a, String b) {
int[][] cal = new int[a.length() + 1][b.length() + 1];
int ans = 0;
for (int i = 0; i < a.length() + 1; i++) {
for (int j = 0; j < b.length() + 1; j++) {
if (i == 0 || j == 0) {
cal[i][j] = 0;
} else if (a.charAt(i - 1) == b.charAt(j - 1)) {
cal[i][j] = cal[i - 1][j - 1] + 1;
} else {
cal[i][j] = 0;
}
ans = Math.max(ans, cal[i][j]);
}
}
System.out.println(Arrays.deepToString(cal));
return ans;
}
public static void main(String[] args) {
System.out.println(solution("ABCDEF", "GBCDFE"));
}
}
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 2, 0, 0, 0],
[0, 0, 0, 0, 3, 0, 0],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 0]]
3
- 문자열을 1번 부터 세어야 하며 0인경우는 세지 않는다.
- ABCDEF와 BCDFE의 LCS 는 BCD이다.
- 코드 작동 과정
- 문자열A와 문자열B를 한글자씩 비교한다
- 두 문자가 다르다면 [i][j]에 0을 표시한다.
- 두 문자가 같다면 [i-1][j-1] 값에 +1을 한다.
- 위 작동 과정이 성립하는 이유는 공통 문자열은 연속되어야 하기 때문이다.
- 문자 앞의 글자까지가 공통 문자열이라면 계속 공통 문자열 일 것이다.
2. 최장 공통 부분수열(Longest Common Subsequence)
class Solution_0046 {
public static int solution(String a, String b) {
int[][] cal = new int[a.length() + 1][b.length() + 1];
int ans = 0;
for (int i = 0; i < a.length() + 1; i++) {
for (int j = 0; j < b.length() + 1; j++) {
if (i == 0 || j == 0) {
cal[i][j] = 0;
} else if (a.charAt(i - 1) == b.charAt(j - 1)) {
cal[i][j] = cal[i - 1][j - 1] + 1;
} else {
cal[i][j] = Math.max(cal[i - 1][j], cal[i][j - 1]);
}
ans = Math.max(ans, cal[i][j]);
}
}
System.out.println(Arrays.deepToString(cal));
return ans;
}
public static void main(String[] args) {
System.out.println(solution("ABCDEF", "GBCDFE"));
}
}
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1],
[0, 0, 1, 2, 2, 2, 2],
[0, 0, 1, 2, 3, 3, 3],
[0, 0, 1, 2, 3, 3, 4],
[0, 0, 1, 2, 3, 4, 4]]
4
- 문자열을 1번 부터 세어야 하며 0인경우는 세지 않는다.
- ABCDEF와 BCDFE의 LCS 는 BCDE 혹은 BCDF이다.
- 코드 작동 과정( i-1, j-1은 문자열의 인덱스를 맞추기 위해 -1을 해주는 것으로 헷갈리면 안된다.
- i나 j 중 0 이 있으면 0으로 입력
- 현재 비교중인 A의 i번째 문자와 B의 j번째 문자가 같다면 [i-1][j-1]에서+1을 해준다.
- 두 문자가 다르다면 [i-1][j] 와 [i][j-1] 중 가장 높은 숫자를 얻어 온다.
- 즉 해당 위치까지 오기전 최대한 연속된 공통 부분수열의 수를 얻어오는 것이다.
3. 최장 공통 부분수열 응용 문제
문자열 A와 문자열 B가 있을 때
문자열 A의 한글자만 바꿔서 최소공통 부분수열을 최대로 만들었을 때 LCS의 길이는 몇인가
예를들어 A = "ABABC" 이고
B = "CADB"이다
현재의 최소 공통 부분수열은 "AB"이다. A의 한글자를 바꿔서 "CBABC"나 "ADABC"형태로 바꾸면
LCS는 "CAB" 나 "ADB"로 늘어날 수 있다.
- 하나 바꾸면 되기에 +1을 해주면 된다고 생각할 수 도 있지만 "ABCDEF"와 "AFK"의 경우 A의 어떤걸 어떻게 바꿔도 최대 LCS가 2가 한계이다.바꾸던 안바꾸던 LCS는 2이다.
- 이렇게 증가시킬 수 없는 경우가 어떤게 있을까?
- A의 길이와 B의 길이중 짧은 길이만큼 LCS가 있다면 굳이 바꾸지 않아도 이미 가능한 최대이다
- "ABCDEF" 와 "BCD처럼
- LCS의 앞글자와 뒷글자가 A의 처음과 끝문자이거나 B의 처음과 끝문자 인경우
- 위 예시처럼 "ABCDEF와 "AFK"의 경우 "AF"가 LSC이며 이는 A의 첫글자와 끝 글자이다.
- A의 길이와 B의 길이중 짧은 길이만큼 LCS가 있다면 굳이 바꾸지 않아도 이미 가능한 최대이다
- 위 두가지 경우를 어떻게 판별할 것인가?
1불가)a= "ABCDEF"이고 b는 "ABFZ"일 때 배열은
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 1]
[0, 1, 2, 2, 2]
[0, 1, 2, 2, 2]
[0, 1, 2, 2, 2]
[0, 1, 2, 2, 2]
[0, 1, 2, 3, 3]
2가능)a= "ABCDEF"이고 b는 "ABZF"일 때 배열은
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 1]
[0, 1, 2, 2, 2]
[0, 1, 2, 2, 2]
[0, 1, 2, 2, 2]
[0, 1, 2, 2, 2]
[0, 1, 2, 2, 3]
3불가)a= "ABCDEF"이고 b는 "AFZ"일 때 배열은
[0, 0, 0, 0]
[0, 1, 1, 1]
[0, 1, 1, 1]
[0, 1, 1, 1]
[0, 1, 1, 1]
[0, 1, 1, 1]
[0, 1, 2, 2]
4가능)a= "ABCDEF"이고 b는 "CFF"일 때 배열은
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 0, 0, 0]
[0, 1, 1, 1]
[0, 1, 1, 1]
[0, 1, 1, 1]
[0, 1, 2, 2]
5가능)a= "ABCDFZ"이고 b는 "ABCDEF"일 때 배열은
[0, 0, 0, 0, 0, 0, 0]
[0, 1, 1, 1, 1, 1, 1]
[0, 1, 2, 2, 2, 2, 2]
[0, 1, 2, 3, 3, 3, 3]
[0, 1, 2, 3, 4, 4, 4]
[0, 1, 2, 3, 4, 4, 5]
[0, 1, 2, 3, 4, 4, 5]
6)불가능)a= "ABCDEF"이고 b는 "ABC"일 때 배열은
[0, 0, 0, 0]
[0, 1, 1, 1]
[0, 1, 2, 2]
[0, 1, 2, 3]
[0, 1, 2, 3]
[0, 1, 2, 3]
[0, 1, 2, 3]
7)가능)a= "ABCDEFZ"이고 b는 "CFFM"일 때 배열은
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 1]
[0, 1, 1, 1, 1]
[0, 1, 1, 1, 1]
[0, 1, 2, 2, 2]
[0, 1, 2, 2, 2]
8)불가)a= "ABCDEF"이고 b는 "CFMK"일 때 배열은
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 1]
[0, 1, 1, 1, 1]
[0, 1, 1, 1, 1]
[0, 1, 2, 2, 2]
9)가능)a= "ABCDEF"이고 b는 "CFMF"일 때 배열은
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 1, 1, 1, 1]
[0, 1, 1, 1, 1]
[0, 1, 1, 1, 1]
[0, 1, 2, 2, 2]
- 아래 모든 경우는 LCS가 A나 B의 최소 길이보다 짧은 경우를 전제로 한다.
- cal의 마지막 원소를 [M][N]이라고 했을 때 0행의 원소들> 1행의 원소들 ... 순으로 탐색한다고 했을 때
- [M][N]원소가 아님에도 LCS의 길이와 같은 값을 가지고 있다면 확인을 해봐야한다.
- 단 7)경우처럼 [x][N],이나 [M][x]의 원소가 아닌 원소에서 같은 값이 나온 경우는 +1이 무조건 가능하다.
- M이나 K를 바꿔봤자 LCS는 그대로 2 이다.
- 8)과 9)경우를 보면 b의 마지막 문자가 a의 마지막 문자와 같을 때 +1이 가능하다.
- 단 7)경우처럼 [x][N],이나 [M][x]의 원소가 아닌 원소에서 같은 값이 나온 경우는 +1이 무조건 가능하다.
1 : 조건 결론
- LCS의 길이와 두 문자열 중 짧은 문자열 길이를 비교한다.
- 같은 경우 > return ans
- 아닐경우
- [M][N]까지 LCS의 길이와 같은 원소가 있는지 찾는다.
- 못찾는다 > return ans +1 [앞서 LCS와 짧은 문자열 길이를 이미 비교했기 때문에 가능 2)경우 생각]
- 찾는다
- [x][N] 나 [M][x] 형태의 원소가 아닌 경우 > return ans +1 [ 7)경우라고 생각하면 된다]
- [x][N] 나 [M][x] 형태인 경우
- [x][N]인 경우엔 a의 x-1 번째 문자열 지난후 x-1와 같은 문자열이 나오는지 확인.
- 나온다면 return ans + 1 [7)경우]
- 안나온다면 return ans [8)경우혹은 3)경우]
- [M][y]인 경우엔 b의 y-1 번째 문자열 지난후 y-1와 같은 문자열이 나오는지 확인 .
- 나온다면 return ans + 1 [7)경우]
- 안나온다면 return ans [8)경우혹은 3)경우]
- [x][N]인 경우엔 a의 x-1 번째 문자열 지난후 x-1와 같은 문자열이 나오는지 확인.
- [M][N]까지 LCS의 길이와 같은 원소가 있는지 찾는다.
2. 최종 코드
class Solution_0047 {
public static int solution(String a, String b) {
int[][] cal = new int[a.length() + 1][b.length() + 1];
int ans = 0;
for (int i = 0; i < a.length() + 1; i++) {
for (int j = 0; j < b.length() + 1; j++) {
if (i == 0 || j == 0) {
cal[i][j] = 0;
} else if (a.charAt(i - 1) == b.charAt(j - 1)) {
cal[i][j] = cal[i - 1][j - 1] + 1;
} else {
cal[i][j] = Math.max(cal[i - 1][j], cal[i][j - 1]);
}
ans = Math.max(ans, cal[i][j]);
}
}
for (int i = 0; i < cal.length; i++) {
System.out.println(Arrays.toString(cal[i]));
}
if (ans == Math.min(a.length(), b.length())) {
return ans;
} else {
int x = a.length();
int y = b.length();
all:
for (int i = 0; i < a.length() + 1; i++) {
for (int j = 0; j < b.length() + 1; j++) {
if (cal[i][j] == ans) {
x = i;
y = j;
break all;
}
}
}
if (x == a.length() && y == b.length()) {
return ans + 1;
} else {
if (y == b.length()) {
for (int i = x; i < a.length(); i++) {
if (a.charAt(i) == a.charAt(x - 1)) {
return ans + 1;
}
}
return ans;
} else if (x == a.length()) {
for (int i = y; i < b.length(); i++) {
if (b.charAt(i) == b.charAt(y - 1)) {
return ans + 1;
}
}
return ans;
} else {
return ans + 1;
}
}
}
}
public static void main(String[] args) {
System.out.println(solution("ABCDEF", "ABFZ"));
}
}
3. 참조 : lcs : https://velog.io/@emplam27/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B7%B8%EB%A6%BC%EC%9C%BC%EB%A1%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EB%8A%94-LCS-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Longest-Common-Substring%EC%99%80-Longest-Common-Subsequence