Skip to main content

Command Palette

Search for a command to run...

[LeetCode] 2434. Using a Robot to Print the Lexicographically Smallest String

Published
4 min read

Edit this2434. Using a Robot to Print the Lexicographically Smallest String text

문제 설명

You are given a string s and a robot that currently holds an empty string t. Apply one of the following operations until s and t are both empty:

  • Remove the first character of a string s and give it to the robot. The robot will append this character to the string t.

  • Remove the last character of a string t and give it to the robot. The robot will write this character on paper.

Return the lexicographically smallest string that can be written on the paper.

Example 1:

Input: s = "zza"
Output: "azz"
Explanation: Let p denote the written string.
Initially p="", s="zza", t="".
Perform first operation three times p="", s="", t="zza".
Perform second operation three times p="azz", s="", t="".

Example 2:

Input: s = "bac"
Output: "abc"
Explanation: Let p denote the written string.
Perform first operation twice p="", s="c", t="ba". 
Perform second operation twice p="ab", s="c", t="". 
Perform first operation p="ab", s="", t="c". 
Perform second operation p="abc", s="", t="".

Example 3:

Input: s = "bdda"
Output: "addb"
Explanation: Let p denote the written string.
Initially p="", s="bdda", t="".
Perform first operation four times p="", s="", t="bdda".
Perform second operation four times p="addb", s="", t="".

Constraints:

  • 1 <= s.length <= 10<sup>5</sup>

  • s consists of only English lowercase letters.

문제 요약

  • 로봇과 내가 각각 문자열 t, s를 들고 있습니다.

  • t, s가 모두 빈 문자열이 될때까지 다음의 동작을 반복합니다

  • s의 맨 앞 문자을 때서 t에 뒤로 붙입니다

  • t의 맵 뒤 문자를 때서 로봇이 그걸 종이에 출력합니다.

  • 이때 출력되는 문자가 사전적으로 가장 순서가 빠른 문자를 출력하도록 문자열을 구성하시오

분석

결국 해당 문제는 정해진 규칙 내에서 사전적으로 가장 순서가 빠른 문자를 출력할 수 있도록 하는 문제입니다.

가능 빠른 문자를 출력하기 위해서는 가장 작은 문자가 먼저 출력되도록 처리하는 것이 좋습니다.

그래서 문제를 탐욕적으로 해결 할 수 있을 것으로 보입니다.

첫 번째 접근: 직관적 해결법

가장 먼저 떠오르는 방법은 "현재 상황에서 최선의 선택하기"입니다.

kotlinclass Solution {
    fun robotWithString(s: String): String {
        var t = ""
        var u = s
        var answer = ""

        while (u.isNotEmpty()) {
            val a = u.min()  // 남은 문자 중 최소값
            val b = t.lastOrNull()  // 스택 top

            if (b == null || a < b) {
                // 더 작은 문자가 남아있으니 계속 스택에 쌓기
                val idx = u.indexOfFirst { it == a }
                for (i in 0..idx) {
                    t += u[i]
                }
                u = u.substring(idx + 1)
            } else {
                // 스택 top을 꺼내는 것이 유리
                answer += b
                t = t.substring(0, t.lastIndex)
            }
        }

        // 남은 스택 모두 pop
        for (i in t.lastIndex downTo 0) {
            answer += t[i]
        }

        return answer
    }
}

성능 문제점

이 접근법은 논리적으로는 맞지만 심각한 성능 문제가 있습니다:

  1. 매번 u.min() 계산: O(n) × O(n) = O(n²)

  2. 문자열 연결 연산: t += u[i]가 O(n²) 시간 소요

  3. substring 연산: 매번 새로운 문자열 객체 생성

  4. 전체 시간복잡도: O(n³)

최적화 아이디어: minSuffix 전처리

핵심 통찰: "매번 최소값을 찾지 말고, 미리 계산해두자!"

kotlin// 각 위치에서 그 위치부터 끝까지의 최소값을 미리 계산
val minSuffix = CharArray(n)
minSuffix[n-1] = s[n-1]
for (i in n-2 downTo 0) {
    minSuffix[i] = minOf(s[i], minSuffix[i+1])
}

예시로 이해하기

s = "bac"

minSuffix 계산:
minSuffix[2] = 'c'           // 위치 2부터 끝: "c" → 최소값 'c'
minSuffix[1] = min('a','c') = 'a'  // 위치 1부터 끝: "ac" → 최소값 'a'  
minSuffix[0] = min('b','a') = 'a'  // 위치 0부터 끝: "bac" → 최소값 'a'

결과: minSuffix = ['a', 'a', 'c']

해답

import java.util.*

class Solution {
    fun robotWithString(s: String): String {
        val answer = StringBuilder()
        val t = ArrayDeque<Char>()
        val minSuffix = CharArray(s.length)

        minSuffix[s.lastIndex] = s[s.lastIndex]

        for (i in s.length - 2 downTo 0) {
            minSuffix[i] = minOf(minSuffix[i + 1], s[i])
        }

        var i = 0
        val n = s.length

        while (i < n || t.isNotEmpty()) {

            while (t.isNotEmpty() && (i >= n || t.last() <= minSuffix[i])) {
                answer.append(t.removeLast())
            }

            if (i < n) {
                t.addLast(s[i])
                i++
            }
        }

        return answer.toString()
    }
}

More from this blog

JVM은 컨테이너의 CPU와 메모리 한계를 어떻게 알아낼까

8코어 노드에 컨테이너를 띄웠는데 ForkJoinPool이 스레드를 한두 개만 만들어요. 메모리는 넉넉히 줬는데 컨테이너가 자꾸 OOMKilled로 죽고요. 분명히 같은 JAR인데 로컬에서는 멀쩡하다가 쿠버네티스에만 올리면 이상해져요. 이 글은 "왜 컨테이너 속 JVM은 다르게 행동하는가"를 cgroup이라는 진짜 경계선과, JVM이 그 경계를 읽어내는 내

May 21, 202615 min read

ThreadPoolExecutor는 언제 스레드를 새로 만들까 — execute()의 3단계

Executors.newFixedThreadPool(10) 한 줄을 쓰면서도, 11번째 작업이 오면 스레드가 11개로 늘어날 거라고 막연히 기대해 본 적 없으신가요. 실제로는 큐가 먼저 무한히 쌓이고 스레드는 영원히 10개에 머물러요. 이 글은 ThreadPoolExecutor가 작업을 받았을 때 "스레드를 새로 만들지, 큐에 넣을지, 거부할지"를 결정하는

May 21, 202617 min read

자바 synchronized는 어떻게 동작할까 — 모니터, 락 인플레이션, 그리고 사라진 biased locking

synchronized 키워드 하나로 스레드 안전을 얻는 동안, JVM 안에서는 객체 헤더의 비트를 뒤집고, 스택에 락 레코드를 쌓고, 경합이 생기면 네이티브 모니터로 승격하는 일이 벌어져요. 이 글은 그 한 번의 잠금이 객체 헤더부터 ObjectMonitor까지 어떤 경로를 거치는지, 그리고 한때 있었다가 JDK 18에서 사라진 biased locking

May 19, 202616 min read

JVM 객체 할당의 비밀 — TLAB, Bump-the-Pointer, 그리고 할당이 거의 공짜인 이유

Java에서 new를 호출하면 무슨 일이 벌어질까요? "힙에 메모리를 잡는다"는 한 문장 뒤에는 스레드마다 자기만의 분양 구역을 나눠 갖는 정교한 설계가 숨어 있어요. 이 글은 HotSpot JVM이 객체 할당을 어떻게 "거의 공짜"로 만드는지 그 내부를 따라가 보려는 글이에요. JVM 메모리 동작 원리에 관심 있는 분께 권해요. 자바를 쓰다 보면 객체를

May 15, 202614 min read

Java Zero-Copy — FileChannel.transferTo, sendfile, 그리고 Kafka가 디스크를 네트워크로 흘려보내는 방법

"파일을 읽어서 소켓으로 보낸다." 한 줄짜리 요구사항이에요. 그런데 이 한 줄 뒤에서 데이터는 메모리를 네 번이나 복사하고, CPU는 커널과 유저 공간을 네 번이나 들락거려요. Kafka처럼 초당 수십만 건을 흘려보내야 하는 시스템에서 이 비용은 그냥 넘길 수가 없어요. 이 글은 그 복사를 한 겹씩 벗겨내는 zero-copy의 동작 원리를 따라가요. 전통

May 15, 202617 min read

끄적끄적 테크 블로그

165 posts

물류 회사에 다니고 있는 개발자 블로그입니다. 개발을 너무 좋아해서 정신없이 작업하다가 중간에 끄적거리며 내용들을 몇개 적어봅니다 ㅎㅎ