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)); } }