신입사원 어피치는 카카오톡으로 전송되는 메시지를 압축하여 전송 효율을 높이는 업무를 맡게 되었다. 메시지를 압축하더라도 전달되는 정보가 바뀌어서는 안 되므로, 압축 전의 정보를 완벽하게 복원 가능한 무손실 압축 알고리즘을 구현하기로 했다.
어피치는 여러 압축 알고리즘 중에서 성능이 좋고 구현이 간단한 LZW(Lempel–Ziv–Welch) 압축을 구현하기로 했다. LZW 압축은 1983년 발표된 알고리즘으로, 이미지 파일 포맷인 GIF 등 다양한 응용에서 사용되었다.
LZW 압축은 다음 과정을 거친다.
길이가 1인 모든 단어를 포함하도록 사전을 초기화한다.
사전에서 현재 입력과 일치하는 가장 긴 문자열 w를 찾는다.
w에 해당하는 사전의 색인 번호를 출력하고, 입력에서 w를 제거한다.
입력에서 처리되지 않은 다음 글자가 남아있다면(c), w+c에 해당하는 단어를 사전에 등록한다.
단계 2로 돌아간다.
압축 알고리즘이 영문 대문자만 처리한다고 할 때, 사전은 다음과 같이 초기화된다. 사전의 색인 번호는 정수값으로 주어지며, 1부터 시작한다고 하자.
| 색인 번호 | 1 | 2 | 3 | ... | 24 | 25 | 26 |
|---|---|---|---|---|---|---|---|
| 단어 | A | B | C | ... | X | Y | Z |
예를 들어 입력으로 KAKAO가 들어온다고 하자.
현재 사전에는 KAKAO의 첫 글자 K는 등록되어 있으나, 두 번째 글자까지인 KA는 없으므로, 첫 글자 K에 해당하는 색인 번호 11을 출력하고, 다음 글자인 A를 포함한 KA를 사전에 27 번째로 등록한다.
두 번째 글자 A는 사전에 있으나, 세 번째 글자까지인 AK는 사전에 없으므로, A의 색인 번호 1을 출력하고, AK를 사전에 28 번째로 등록한다.
세 번째 글자에서 시작하는 KA가 사전에 있으므로, KA에 해당하는 색인 번호 27을 출력하고, 다음 글자 O를 포함한 KAO를 29 번째로 등록한다.
마지막으로 처리되지 않은 글자 O에 해당하는 색인 번호 15를 출력한다.
| 현재 입력(w) | 다음 글자(c) | 출력 | 사전 추가(w+c) |
|---|---|---|---|
| K | A | 11 | 27: KA |
| A | K | 1 | 28: AK |
| KA | O | 27 | 29: KAO |
| O | 15 |
이 과정을 거쳐 다섯 글자의 문장 KAKAO가 4개의 색인 번호 [11, 1, 27, 15]로 압축된다.
입력으로 TOBEORNOTTOBEORTOBEORNOT가 들어오면 다음과 같이 압축이 진행된다.
| 현재 입력(w) | 다음 글자(c) | 출력 | 사전 추가(w+c) |
|---|---|---|---|
| T | O | 20 | 27: TO |
| O | B | 15 | 28: OB |
| B | E | 2 | 29: BE |
| E | O | 5 | 30: EO |
| O | R | 15 | 31: OR |
| R | N | 18 | 32: RN |
| N | O | 14 | 33: NO |
| O | T | 15 | 34: OT |
| T | T | 20 | 35: TT |
| TO | B | 27 | 36: TOB |
| BE | O | 29 | 37: BEO |
| OR | T | 31 | 38: ORT |
| TOB | E | 36 | 39: TOBE |
| EO | R | 30 | 40: EOR |
| RN | O | 32 | 41: RNO |
| OT | 34 |
입력 형식
입력으로 영문 대문자로만 이뤄진 문자열 msg가 주어진다. msg의 길이는 1 글자 이상, 1000 글자 이하이다.
출력 형식
주어진 문자열을 압축한 후의 사전 색인 번호를 배열로 출력하라.
입출력 예제
| msg | answer |
|---|---|
| KAKAO | [11, 1, 27, 15] |
| TOBEORNOTTOBEORTOBEORNOT | [20, 15, 2, 5, 15, 18, 14, 15, 20, 27, 29, 31, 36, 30, 32, 34] |
| ABABABABABABABAB | [1, 2, 27, 29, 28, 31, 30] |
import java.util.*;
class Solution {
public int[] solution(String msg) {
// 사전을 저장할 배열
ArrayList<String> dictionary = new ArrayList<>();
// 출력 결과를 저장할 배열
ArrayList<Integer> result = new ArrayList<>();
// A~Z를 사전에 저장
for(int i = 0; i < 26; i++) {
dictionary.add(String.valueOf((char)('A' + i)));
}
// msg의 길이만큼 반복
for(int i = 0; i < msg.length(); i++) {
// 사전의 길이만큼 반복
for(int j = dictionary.size()-1; j >= 0; j--) {
// i부터 시작하는 문자열이 사전에 있는 것으로 시작한다면
if(msg.substring(i).startsWith(dictionary.get(j))) {
// i의 값을 바꿔줌
i += dictionary.get(j).length() - 1;
// 인덱스는 0부터 이므로 1 더한 값을 넣어줌
result.add(j+1);
// i+1의 값이 주어진 문자열의 길이를 넘지 않는다면
if(i+1 < msg.length()) {
// 사전에 값을 추가
dictionary.add(dictionary.get(j) + msg.charAt(i+1));
}
break;
}
}
}
// 결과값을 int[] 배열에 저장
int[] answer = new int[result.size()];
for(int i = 0; i < result.size(); i++) {
answer[i] = result.get(i);
}
return answer;
}
}
자바의 함수들을 사용해서 문제를 풀었다. 우선 문제의 초깃값으로 A~Z가 배열에 담겨져 있으므로 탐색할 수 있는 사전 배열을 하나 만들고 그 배열에 값을 넣어준다.
이후 주어진 문자열의 길이만큼 반복을 진행하는데, 이때 2중 반복을 통해 사전의 길이만큼 탐색을 진행한다. 첫번째 조건문에서 문자열의 함수 중 substring을 사용했다. substring(시작 인덱스, 종료 인덱스)을 사용하는데 시작 인덱스만 주어졌을 경우, 해당 위치부터 마지막까지 문자열을 잘라준다. 만약 시작과 종료 인덱스를 모두 작성했을 경우에는 시작 인덱스부터 (종료 - 1) 인덱스까지 문자열을 잘라준다.
예를 들어, programmers 라는 문자열 str이 있다고 하면
str.substring(2)는 ogrammers가 반환이 된다.
str.substring(2, 5)는 ogr가 반환이 된다.
또한 startsWith 함수를 사용하는데, startsWith 함수는 startsWith("예시")라고 했을 때, 해당 문자열이 예시로 시작한다면 true를 아니라면 false를 반환한다.
예를 들어, programmers 라는 문자열 str이 있다고 하면
str.startsWith("pro")는 true를 반환한다.
str.startsWith("program")도 true를 반환한다.
str.startsWith("pros")는 false를 반환한다.
str.startsWith(" pro")도 false를 반환한다.
위의 함수는 띄어쓰기 역시 다르다고 생각하므로 주의해서 사용해야한다..
이 두 함수를 사용하여 위치별로 문자열을 잘라주고 사전에서 가장 긴 접두사를 찾아주는 방식으로 문제를 해결하였다.
이때 i의 값 역시 변경을 해주어야하는데, 변경을 해주지 않는다면 KAKAO의 예시에서 K, A, KA, O로 탐색이 되는 것이 아니라 K, A, KA, AK, O로 탐색이 된다. 즉, 탐색을 해서 같은 접두사의 위치까지 i의 위치를 옮겨주는 것이다.
이처럼 해당 탐색이 다 끝났다면 결과값을 저장한 배열에 있는 값들을 answer 배열로 옮겨서 반환해준다.
문제를 봤을 때 굉장히 길어서 대충 읽고 넘겼었다. 그런데 다시 문제를 풀면서 읽어보니 풀이 방법을 거의 다 설명을 해주어서 문제를 제대로 읽어가면서 풀었다. 푸는 과정이 단순하진 않았으나, 매우 친절한 문제여서 재밌게 풀었다!