-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
191 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
--- | ||
title: '1509. 팰린드롬 분할' | ||
metaTitle: '만렙 개발자 키우기' | ||
metaDescription: '알고리즘 문제를 풀고 정리한 곳입니다.' | ||
tags: ['DP'] | ||
date: '2021-06-24' | ||
--- | ||
|
||
# 문제 | ||
- [1509. 팰린드롬 분할](https://www.acmicpc.net/problem/1509) | ||
|
||
## 코드 | ||
|
||
<details><summary> 코드 보기 </summary> | ||
|
||
``` java | ||
import java.io.BufferedReader; | ||
import java.io.IOException; | ||
import java.io.InputStreamReader; | ||
import java.util.Arrays; | ||
|
||
public class Q1509 { | ||
static String line; | ||
static int size, dp[]; | ||
static boolean palin[][]; | ||
public static void main(String[] args) throws IOException { | ||
init(); | ||
int ans = solution(0); | ||
System.out.println(ans); | ||
} | ||
|
||
private static void init() throws IOException { | ||
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); | ||
line = br. readLine(); | ||
size = line.length(); | ||
dp = new int[size]; | ||
palin = new boolean[size][size]; | ||
Arrays.fill(dp, -1); | ||
} | ||
|
||
private static boolean makePalindrom(int l, int r) { | ||
if(l >= r || palin[l][r]) return palin[l][r] = true; | ||
if(line.charAt(l) != line.charAt(r)) | ||
return palin[l][r] = false; | ||
return palin[l][r] = makePalindrom(l+1, r-1); | ||
} | ||
|
||
private static int solution(int start) { | ||
if(start == size) return 0; | ||
if(dp[start] != -1) return dp[start]; | ||
int ret = dp[start] = 987654321; | ||
for (int i = start; i < size; i++) { | ||
if (makePalindrom(start, i)) { | ||
ret = Math.min(ret, 1 + solution(i+1)); | ||
} | ||
} | ||
return dp[start] = ret; | ||
} | ||
} | ||
|
||
``` | ||
</details> | ||
|
||
## ⭐️느낀점⭐️ | ||
> 문자열 알고리즘을 사용하는 문제인가 해서 쫄았다가 DP 라는 유형을 보고 나서 접근하였다. | ||
> | ||
> DP 로 풀어야 한다는 힌트를 얻고도 쉽지 않은 문제였다. 다시 풀어볼 문제에 추가해야겠다. | ||
|
||
## 풀이 📣 | ||
<hr/> | ||
|
||
1️⃣ 입력받은 문자열의 부분 문자열들이 팰린드롬인지 상태정보를 저장하기 위한 DP를 사용한다. | ||
|
||
- boolean palin[부분 문자열 시작 인덱스][부분 문자열 종료 인덱스] | ||
|
||
- 재귀적으로 [시작인덱스+1][종료인덱스-1] 의 상태 정보를 채워나간다. | ||
|
||
- 이미 구한 부분 문자열의 상태는 재계산하지 않는다. | ||
|
||
|
||
2️⃣ 시작 인덱스를 옮겨가며 부분 문자열을 만든다. | ||
|
||
- 시작은 0부터 한다. | ||
|
||
- 재귀 중 특정 깊이에서, 시작 인덱스부터 오른쪽으로 한 칸씩 옮겨가는 포인터(i)를 하나 설정해준다. | ||
|
||
- (start, i) 의 부분 문자열이 팰린드롬이라면, 즉 makePalindrom(start, i) 가 true이면 개수의 + 1을 해준다. | ||
|
||
- 그리고 i + 1 부터는 새로운 재귀 함수를 시행해준다. -> ret = Math.min(ret, 1 + solution(i+1)); | ||
|
||
- 이렇게 시작 인덱스로만 모든 부분 문자열을 구할 수 있는 이유는 i를 start부터 시작해서 한 칸씩 옆으로 옮기기 때문이다. | ||
|
||
- 그러면 i를 통해 start 혹은 이후의 모든 부분 문자열을 한 번 미리 계산할 수 있다. | ||
|
||
- 따라서 start 자체를 한 칸씩 옮겼을 때 만들어지는 부분 문자열에서, 이전에 저장해둔 최소 분할 개수를 재사용할 수 있도록 해준다. | ||
|
||
|
||
- 글로 설명하면 장황하고 이해하기 어려우니 예시를 들어보겠다. | ||
|
||
- ex) abcdee 가 입력된다면, 재귀의 깊이를 따라 진행하였을 때, 처음엔 분할의 개수가 {a, b, c, d, e, e} 로 총 6개가 계산된다. | ||
|
||
이후 start가 4일 때, i 가 5로 이동하면 부분 문자열은 ee 가 된다. 그럼 isPalindrom(start, i) 는 true가 되서, 1 + solution(i+1) = 1 + solution(6) = 1 + 0 = 1 이 된다. | ||
|
||
이렇게 모든 경우를 커버하면서, 팰린드롬을 만났을 때 최소값으로 갱신되는 구조를 갖는다. | ||
|
||
|
||
3️⃣ 최소 분할 개수를 출력하고 종료한다. | ||
|
||
- solution(0) 을 출력한다. | ||
|
||
|
||
## 실수 😅 | ||
<hr/> | ||
|
||
- 처음에 팰린드롬을 판별하기 위해 부분 문자열을 만들고 그때마다 양쪽 끝에서부터 확인을 하였다. | ||
|
||
- 이 부분의 불필요한 중복 계산을 막기 위해 재귀를 통한 메모이제이션을 구현하였다. | ||
|
||
|
||
- 이후 분할 개수를 찾기 위해서 추가적인 인덱스 포인터(i)를 설정해서 옮겨가며 부분 문자열이 팰린드롬이 되는지 찾도록 구현하였다. | ||
|
||
|
||
- 만약 팰린드롬이 되면 1 + solution(i+1, right) 를 최소 값으로 갱신하면서 새로운 재귀를 수행하였다. | ||
|
||
|
||
- 팰린드롬이 아니라면 solution(left, i) + solution(i+1, right) 를 최소 값으로 갱신하면서 새로운 재귀를 수행하였다. | ||
|
||
|
||
- 하지만 이 부분에서 solution(left + i) 를 앞쪽에서 구하였을 때, 상태값이 저장은 되지만 해당 상태값이 이후에 결코 재사용될 수 없었다.. => 정답은 나오지만, `시간 초과` | ||
|
||
|
||
- 따라서 문자열에서 왼쪽의 부분 문자열의 상태가 아닌, 오른쪽의 부분 문자열의 상태를 메모이제이션 하도록 구현하여 통과할 수 있었다. => `시작 인덱스` 만 사용 | ||
|
||
<br/> | ||
|
||
<details> <summary> 실패한 코드 </summary> | ||
|
||
```java | ||
import java.io.BufferedReader; | ||
import java.io.IOException; | ||
import java.io.InputStreamReader; | ||
import java.util.Arrays; | ||
|
||
public class Main { | ||
static String line; | ||
static int size, dp[][]; | ||
static boolean palin[][]; | ||
public static void main(String[] args) throws IOException { | ||
init(); | ||
int ans = solution(0, size-1); | ||
System.out.println(ans); | ||
} | ||
|
||
private static void init() throws IOException { | ||
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); | ||
line = br. readLine(); | ||
size = line.length(); | ||
dp = new int[size][size]; | ||
palin = new boolean[size][size]; | ||
for (int i = 0; i < size; i++) { | ||
Arrays.fill(dp[i], -1); | ||
} | ||
} | ||
|
||
private static boolean makePalindrom(int l, int r) { | ||
if(l >= r || palin[l][r]) return palin[l][r] = true; | ||
if(line.charAt(l) != line.charAt(r)) | ||
return palin[l][r] = false; | ||
return palin[l][r] = makePalindrom(l+1, r-1); | ||
} | ||
|
||
private static int solution(int left, int right) { | ||
if(left == right) return 1; | ||
if(left > right) return 0; | ||
if(dp[left][right] != -1) return dp[left][right]; | ||
int ret = dp[left][right] = 987654321; | ||
for (int i = left; i <= right; i++) { | ||
if (makePalindrom(left, i)) { | ||
ret = Math.min(ret, 1 + solution(i+1, right)); | ||
} else { // 이 부분에서 메모이제이션이 제대로 이루어지지 않음. | ||
ret = Math.min(ret, solution(left, i) + solution(i+1, right)); | ||
} | ||
} | ||
return dp[left][right] = ret; | ||
} | ||
} | ||
|
||
``` | ||
|
||
</details> |