Skip to main content

3403 - Find the Lexicographically Largest String From the Box I

info

풀이 키워드​

스포주의

문자열


풀이 코드​

info
  • 메모리: 52700 KB
  • 시간: 10 ms
class Solution {
public String answerString(String word, int numFriends) {
if (numFriends == 1) return word; // corner case

// lex order first, desc length order then
final int n = word.length();
final int mxLen = word.length() - (numFriends-1);

Set<String> candSet = new TreeSet<>(Collections.reverseOrder());
char mxChar = 'a';
char[] wordArr = word.toCharArray();
for (char ch : wordArr) mxChar = mxChar < ch ? ch : mxChar;

for (int i = 0; i < n; ++i) {
if (wordArr[i] != mxChar) continue;
int j = Math.min(i+mxLen, n);
candSet.add(word.substring(i, j));
}

return candSet.iterator().next();
}
}

풀이 해설​

문자열을 친구 수만큼 빈 문자열이 되지 않도록 쪼개고,

그 문자열들 중에서 lexicographically largest한 문자열을 반환해야 하는 문제이다.

...길어서 lex라고 하겠다. lex largest string을 구하려면

  1. 사전순으로 뒤에 있는 것 우선
  2. 비교 가능한 범위에서 문자 구성이 모두 동일하면 길이가 긴 문자열 우선

이 2가지 기준으로 문자열을 필터링해야 한다.

이는 결국 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 보다 "z" 가 더 lex largest 하다는 뜻이라서,

mxChar을 사전에 결정하고 해당 문자로 시작하는 인덱스에서만 후보군을 탐색하도록 그리디한 접근 방식을 떠올렸다.


자바의 TreeSet 사용해보기​

TreeSet은 RBT로 구현된, '자동정렬 unique 우선순위 큐' 같은 것이라고 생각하면 된다.

그냥 set이 필요한데 정렬도 필요하다 싶으면 TreeSet 쓰면 된다.

Set<String> candSet = new TreeSet<>(Collections.reverseOrder());

root에 lex largest한 문자열이 놓이도록 내림차순으로 생성했고, iterator().next()로 root에 접근했다.

TreeSet을 선택한 이유는 다음과 같다.

  • 후보 문자열의 중복을 제거하여 후보군 탐색시 복잡도를 최적화할 수 있기 때문
  • 자동 정렬 조아용 🤗💕

caution

corner case 조심 ❗​

public String answerString(String word, int numFriends) {
if (numFriends == 1) return word; // corner case
틀리기 쉬운 반례
"gh"
1
답: "gh"

상단의 corner case가 별도로 처리되지 않는다면 어떻게 로직이 돌아갈지 분석해보자.

final int mxLen = word.length() - (numFriends-1);

char mxChar = 'a';
char[] wordArr = word.toCharArray();
for (char ch : wordArr) mxChar = mxChar < ch ? ch : mxChar;

별다른 처리가 없을 시 mxLen은 정상적으로 2로 설정되지만

그리디하게 가장 사전순으로 높은 알파벳을 추리는 과정에서 mxChar이 h로 설정되고

여기서 candSet엔 뭐가 들어갈까요?
for (int i = 0; i < n; ++i) {
if (wordArr[i] != mxChar) continue;
int j = Math.min(i+mxLen, n);
candSet.add(word.substring(i, j));
}

결국 for문 순회에서 i = 0일때 g로 시작하는 문자열은 스킵되며

i = 1일때 h로 시작하는 문자열 중에서 j가 min(1+2, 2) = 2로 설정되어 "h"가 candSet에 들어가게 된다.

하지만 numFriends가 1이었기에 사실 "gh"는 쪼개질 수가 없었다 (!)

완전히 예외 케이스인 것이다.


✨ 다른 풀이 방법​

TreeSet을 사용하지 않고 compareTo 메소드를 사용하여 더 나은 문자열 발견시 그때그때 바꿔주면

수행시간이 5ms로 줄어든다. (대다수가 이렇게 풀이한듯?)

class Solution {
public String answerString(String word, int numFriends) {
if (numFriends == 1) return word; // corner case

// lex order first, desc length order then
final int n = word.length();
final int mxLen = word.length() - (numFriends-1);

char mxChar = 'a';
char[] wordArr = word.toCharArray();
for (char ch : wordArr) mxChar = mxChar < ch ? ch : mxChar;

String ans = "";
for (int i = 0; i < n; ++i) {
if (wordArr[i] != mxChar) continue;
int j = Math.min(i+mxLen, n);
String cand = word.substring(i, j);
if (ans.compareTo(cand) < 0) ans = cand;
}

return ans;
}
}

메모​

  • 코테 연습용으로도 괜춘한듯
  • corner case 말고는 전반적으로 무난한 난이도
    • 기본적으로 입력의 최소값은 좀 테스트해보십쇼 🙂...