https://leetcode.com/problems/longest-palindromic-subsequence/description/
코드설명
동적계획법(Dynamic Programming) + 깊이우선탐색(DFS) + 문자열(String) 을 활용합니다.
처음에 Palindromic Subsequence이기에 가운데에서 팰린드롬을 판단하면서 처리하려고 했습니다. 하지만, 이 문제의 경우 Subsequence로써 문자열을 언제든지 자를 수 있는 것을 의미하므로 굳이 가운데에서부터 처리할 필요가 없었습니다.
양 옆에서 다가오면서 모든 경우를 탐색하고, 중복 연산은 동적계획법으로 Cache 처리합니다.
구현에 있어서 어려웠던 점은, DFS를 통해 기저사례에서 left와 right가 다른경우를 처리하게 되었는데, 이 과정에서 for문을 사용하게 되고, 구현이 제대로 작동되지 않았습니다.
그 대신에 아래와 같이 현재 left와 right가 일치할경우에만 개수를 증가시킨다는 생각으로, 일치한다면 개수를 1 증가시키고, 재귀함수를 계속해서 호출해나가서 완전탐색으로 처리합니다.
//비교를 여기서 처리합니다. 기저사례에서 처리하는 것보다는 재귀실행 전 처리해야 로직이 간단해집니다.
if(S.charAt(left) == S.charAt(right)){
first = DFS(left - 1, right + 1) + 1;
}
항상 다음 위치를 기반으로 처리한뒤, 재귀함수를 실행하는 구조에 익숙해져서 위와 같이 현재 위치를 처리하고, 개수를 세는 방식을 놓쳤던 것 같습니다. 이러한 구조는 frist = DFS(left - 1, right + 1) + 1; 처럼 현재 함수가 실행시 +1 을 현재 재귀레벨에서 처리하기에, 이것도 결국에는 다음 재귀의 기저사례에서 처리하는 것과 똑같습니다.
코드
정답코드2입니다. 양쪽끝에서 출발하여 처리한 코드입니다.
class Solution {
String S;
int[][] cache = new int[1001][1001];
public int longestPalindromeSubseq(String s) {
S = s;
int answer = 0;
for(int[] v : cache) Arrays.fill(v, -1);
return DFS(0, S.length() - 1);
}
int DFS(int left, int right){
if(left > right) return 0;
if(left == right) return 1;
if(cache[left][right] != -1) return cache[left][right];
int ret = 0;
if(S.charAt(left) == S.charAt(right)){
ret = DFS(left + 1, right - 1) + 2;
}
else if(S.charAt(left) != S.charAt(right)){
int ret1 = DFS(left + 1, right);
int ret2 = DFS(left, right - 1);
ret = Math.max(ret1, ret2);
}
return cache[left][right] = ret;
}
}
DFS함수 호출시 아래의 조건문을 활용하여 먼저 체크하고 함수를 호출함으로써 현재위치를 판단하고 넘어갑니다.
if(S.charAt(left) == S.charAt(right)){
정답코드1입니다. 하지만, DFS(i, i), DFS(i, i+1)처럼 모든 경우를 완탐하면서 DP를 적용하므로 통과는 하지만, 속도가 느립니다.
class Solution {
String S;
int[][] cache = new int[1001][1001];
public int longestPalindromeSubseq(String s) {
S = s;
int answer = 0;
for(int[] v : cache) Arrays.fill(v, -1);
for(int i=0; i<S.length(); i++){
int first = DFS(i, i);
int second = DFS(i, i + 1);
answer = Math.max(answer, Math.max(DFS(i, i), DFS(i, i + 1)));
}
return answer;
}
int DFS(int left, int right){
if(left < 0 || right >= S.length()) return 0;
if(cache[left][right] != -1) return cache[left][right];
int first = 0, second = 0, third = 0;
if(left == right){
//비교를 여기서 처리합니다. 기저사례에서 처리하는 것보다는 재귀실행 전 처리해야 로직이 간단해집니다.
if(S.charAt(left) == S.charAt(right)){
first = DFS(left - 1, right + 1) + 1;
}
}else if(left < right){
if(S.charAt(left) == S.charAt(right)){
first = DFS(left - 1, right + 1) + 2;
}
if(S.charAt(left) != S.charAt(right)){
first = DFS(left - 1, right);
second = DFS(left, right + 1);
}
}
return cache[left][right] = Math.max(first, second);
}
}
처음에 틀린 방식입니다. DFS만으로 해결하다가, for문이 섞이게 되면서 접근이 틀렸습니다.
class Solution {
String S;
public int longestPalindromeSubseq(String s) {
S = s;
int answer = 0;
for(int i=0; i<s.length(); i++){
System.out.println("===================== i:"+i+"i+1:"+(i+1));
System.out.println(DFS(i, i));
System.out.println(DFS(i, i + 1));
answer = Math.max(answer, Math.max(DFS(i, i), DFS(i, i + 1)));
}
return answer;
}
int DFS(int left, int right){
if(left < 0 || right >= S.length()) return 0;
// if(S.charAt(left) != S.charAt(right)) return 0;
int ret = 0, leftJump = 0, rightJump = 0, noJump = 0;
if(left == right) {
noJump = DFS(left - 1,right + 1) + 1;
for(int i=left - 1; i>=0; i--){
if(S.charAt(i) == S.charAt(right))
leftJump = Math.max(leftJump, DFS(i, right) + 1);
}
for(int i=right + 1; i<S.length(); i++){
if(S.charAt(left) == S.charAt(i))
rightJump = Math.max(rightJump, DFS(left, i) + 1);
}
}
else {
for(int i=left - 1; i>=0; i--){
if(S.charAt(i) == S.charAt(right))
leftJump = Math.max(leftJump, DFS(i, right) + 1);
}
for(int i=right + 1; i<S.length(); i++){
if(S.charAt(left) == S.charAt(i))
rightJump = Math.max(rightJump, DFS(left, i) + 1);
}
leftJump = DFS(left - 1, right) + 0;
rightJump = DFS(left, right + 1) + 0;
noJump = DFS(left - 1, right + 1) + 2;
//3. 왼족오른쪽 삭제는 1번과 2번 실행시 자동으로 커버링됨.
}
return ret + Math.max(leftJump, Math.max(rightJump, noJump));
}
}