Facebook Pixel

3722. Lexicographically Smallest String After Reverse

MediumTwo PointersBinary SearchEnumeration
LeetCode ↗

Problem Description

You are given a string s of length n made up of lowercase English letters.

You must perform exactly one operation. To do this, pick any integer k where 1 <= k <= n, and then do one of the following:

  • Reverse the first k characters of s (the prefix of length k), or
  • Reverse the last k characters of s (the suffix of length k).

After performing this single operation, return the lexicographically smallest string you can obtain.

A few important points to keep in mind:

  • The operation is mandatory — you cannot skip it. However, choosing k = 1 reverses a single character, which leaves the string unchanged, so the original string is always achievable.
  • You only reverse a prefix or a suffix, never a substring in the middle.
  • "Lexicographically smallest" means the string that would come first in dictionary order. For example, "abc" is smaller than "acb" because at the first position where they differ, 'b' < 'c'.

For instance, if s = "dcab":

  • Reversing the first k = 3 characters gives "acdb".
  • Reversing the first k = 2 characters gives "cdab".
  • Reversing the last k = 2 characters gives "dcba".

Among all possible choices of k and both directions (prefix or suffix), you need to find the smallest resulting string.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Simulation / Basic DSA?

This problem maps to Simulation / Basic DSA through a short path in the full flowchart.

Tree orgraph?noSimulation/ directcalculation?yesSimulation /Basic DSA

With only 2n possible outcomes, enumerating every prefix/suffix reversal and comparing strings lexicographically is simple and sufficient.

Open in Flowchart

Intuition

The first thing to notice is how limited the operation space actually is. For a string of length n, there are only n possible values of k, and for each k there are just two choices: reverse the prefix of length k or reverse the suffix of length k. That gives us at most 2n candidate strings in total.

When the number of possible outcomes is this small, the most natural idea is: why not just try them all? Instead of trying to be clever about predicting which reversal produces the smallest string, we can simply generate every candidate and compare them directly.

This brute-force mindset is justified here because:

  • Each candidate string can be built in O(n) time using slicing and reversal.
  • Comparing two strings lexicographically also takes O(n) time.
  • With 2n candidates, the total work is O(n^2), which is perfectly acceptable for typical constraints on this problem.

One subtle point makes this approach airtight: the problem requires exactly one operation, but reversing a prefix or suffix of length k = 1 changes nothing. So the original string s itself is always among the reachable results — that's why we can safely initialize the answer as ans = s and only ever improve it.

Trying to reason analytically about which k is optimal (e.g., finding where the smallest character sits and reversing it to the front) is tempting, but it quickly gets messy with ties and edge cases. Exhaustive enumeration sidesteps all of that complexity: it is simple, obviously correct, and fast enough. When correctness is easy to guarantee and the input size permits it, enumeration is the right tool.

Pattern Learn more about Two Pointers and Binary Search patterns.

Solution Approach

The solution uses enumeration (brute force) over all possible operations. We try every valid k from 1 to n, build both possible resulting strings for each k, and keep the smallest one seen so far.

Step-by-Step Walkthrough

Step 1: Initialize the answer.

ans = s

We start with ans = s because choosing k = 1 (reversing a single character) leaves the string unchanged, so s itself is always a valid outcome. This gives us a safe baseline to compare against.

Step 2: Enumerate every value of k.

for k in range(1, len(s) + 1):

We loop over all n possible choices of k. For each one, there are exactly two operations to consider.

Step 3: Build the candidate from reversing the prefix.

t1 = s[:k][::-1] + s[k:]
  • s[:k] extracts the first k characters.
  • [::-1] reverses that prefix.
  • s[k:] keeps the rest of the string untouched.

Concatenating them gives the string after reversing the first k characters.

Step 4: Build the candidate from reversing the suffix.

t2 = s[:-k] + s[-k:][::-1]
  • s[:-k] keeps the first n - k characters unchanged.
  • s[-k:] extracts the last k characters, and [::-1] reverses them.

Concatenating them gives the string after reversing the last k characters.

Note: when k = n, the slice s[:-k] evaluates to the empty string, so t2 is simply the full reversal of s — Python's slicing handles this edge case cleanly without special treatment.

Step 5: Update the answer.

ans = min(ans, t1, t2)

Python's min compares strings lexicographically, which is exactly the ordering the problem asks for. After the loop ends, ans holds the smallest string among all 2n candidates (plus the original).

Full Code

class Solution:
    def lexSmallest(self, s: str) -> str:
        ans = s
        for k in range(1, len(s) + 1):
            t1 = s[:k][::-1] + s[k:]
            t2 = s[:-k] + s[-k:][::-1]
            ans = min(ans, t1, t2)
        return ans

Complexity Analysis

Let n be the length of s.

  • Time complexity: O(n^2). The loop runs n times, and each iteration performs slicing, reversal, concatenation, and string comparison — each of these is O(n).
  • Space complexity: O(n). At any moment we only hold a constant number of temporary strings (t1, t2, ans), each of length n.

Why This Works

The operation space is exhaustively covered: every legal move is "reverse a prefix of length k" or "reverse a suffix of length k" for some k in [1, n]. Since we generate all of these outcomes and take the minimum, the result is guaranteed to be optimal — there is no reachable string we could have missed.

Example Walkthrough

Let's trace the algorithm on s = "dcab" (so n = 4).

Initialization: ans = "dcab" — this corresponds to the always-available "do nothing" outcome (reversing a single character with k = 1).

Now we enumerate every k from 1 to 4, building both candidates each time:

Iteration k = 1:

  • t1 = s[:1][::-1] + s[1:]"d" + "cab" = "dcab" (reversing one character changes nothing)
  • t2 = s[:-1] + s[-1:][::-1]"dca" + "b" = "dcab"
  • ans = min("dcab", "dcab", "dcab") = "dcab"

Iteration k = 2:

  • t1 = "dc"[::-1] + "ab""cd" + "ab" = "cdab"
  • t2 = "dc" + "ab"[::-1]"dc" + "ba" = "dcba"
  • ans = min("dcab", "cdab", "dcba") = "cdab" ✅ improved! ('c' < 'd' at position 0)

Iteration k = 3:

  • t1 = "dca"[::-1] + "b""acd" + "b" = "acdb"
  • t2 = "d" + "cab"[::-1]"d" + "bac" = "dbac"
  • ans = min("cdab", "acdb", "dbac") = "acdb" ✅ improved! ('a' < 'c' at position 0)

Iteration k = 4:

  • t1 = "dcab"[::-1] + """bacd" (full reversal via prefix)
  • t2 = "" + "dcab"[::-1]"bacd" (full reversal via suffix — note s[:-4] is the empty string, handled cleanly by slicing)
  • ans = min("acdb", "bacd", "bacd") = "acdb" — no improvement

Summary of all candidates:

kPrefix reversal (t1)Suffix reversal (t2)
1dcabdcab
2cdabdcba
3acdbdbac
4bacdbacd

The loop ends, and we return ans = "acdb".

This matches our intuition: reversing the prefix of length 3 brings the smallest character 'a' to the front, and since the enumeration covered every legal operation, we are certain no better result exists.

Solution Implementation

1class Solution:
2    def lexSmallest(self, s: str) -> str:
3        # Initialize the answer with the original string
4        answer = s
5        string_length = len(s)
6
7        # Try every possible prefix/suffix length k from 1 to len(s)
8        for k in range(1, string_length + 1):
9            # Candidate 1: reverse the prefix of length k, keep the rest unchanged
10            prefix_reversed = s[:k][::-1] + s[k:]
11
12            # Candidate 2: keep the front unchanged, reverse the suffix of length k
13            suffix_reversed = s[:-k] + s[-k:][::-1]
14
15            # Update the answer with the lexicographically smallest candidate
16            answer = min(answer, prefix_reversed, suffix_reversed)
17
18        return answer
19
1class Solution {
2    public String lexSmallest(String s) {
3        // Initialize the answer as the original string
4        String smallest = s;
5        int n = s.length();
6
7        // Try every possible reversal length k from 1 to n
8        for (int k = 1; k <= n; ++k) {
9            // Candidate 1: reverse the prefix of length k, keep the suffix unchanged
10            String prefixReversed = new StringBuilder(s.substring(0, k)).reverse().toString()
11                + s.substring(k);
12
13            // Candidate 2: keep the prefix unchanged, reverse the suffix of length k
14            String suffixReversed = s.substring(0, n - k)
15                + new StringBuilder(s.substring(n - k)).reverse().toString();
16
17            // Update the answer if the prefix-reversed candidate is lexicographically smaller
18            if (prefixReversed.compareTo(smallest) < 0) {
19                smallest = prefixReversed;
20            }
21
22            // Update the answer if the suffix-reversed candidate is lexicographically smaller
23            if (suffixReversed.compareTo(smallest) < 0) {
24                smallest = suffixReversed;
25            }
26        }
27
28        // Return the lexicographically smallest string obtained
29        return smallest;
30    }
31}
32
1class Solution {
2public:
3    string lexSmallest(string s) {
4        // Initialize the answer with the original string as the baseline candidate
5        string smallest = s;
6        int length = s.size();
7
8        // Try every possible split length k from 1 to length
9        for (int k = 1; k <= length; ++k) {
10            // Candidate 1: reverse the prefix of length k, keep the suffix unchanged
11            string prefixReversed = s.substr(0, k);
12            reverse(prefixReversed.begin(), prefixReversed.end());
13            string candidatePrefix = prefixReversed + s.substr(k);
14
15            // Candidate 2: keep the prefix unchanged, reverse the suffix of length k
16            string suffixReversed = s.substr(length - k);
17            reverse(suffixReversed.begin(), suffixReversed.end());
18            string candidateSuffix = s.substr(0, length - k) + suffixReversed;
19
20            // Update the answer with the lexicographically smallest candidate so far
21            smallest = min({smallest, candidatePrefix, candidateSuffix});
22        }
23
24        return smallest;
25    }
26};
27
1/**
2 * Returns the lexicographically smallest string obtainable by reversing
3 * either a prefix or a suffix (of any length) of the input string.
4 *
5 * For every possible length k (1 <= k <= n), two candidates are generated:
6 *   1. Reverse the prefix of length k, keep the rest unchanged.
7 *   2. Reverse the suffix of length k, keep the rest unchanged.
8 * The answer is the smallest string among the original and all candidates.
9 *
10 * @param s - The input string
11 * @returns The lexicographically smallest string after one reversal operation
12 */
13function lexSmallest(s: string): string {
14    // Initialize the answer with the original string
15    let ans: string = s;
16    const n: number = s.length;
17
18    // Try every possible reversal length k
19    for (let k = 1; k <= n; ++k) {
20        // Candidate 1: reverse the prefix of length k
21        const prefixReversed: string = reverse(s.slice(0, k)) + s.slice(k);
22
23        // Candidate 2: reverse the suffix of length k
24        const suffixReversed: string = s.slice(0, n - k) + reverse(s.slice(n - k));
25
26        // Keep the lexicographically smallest among current answer and candidates
27        ans = [ans, prefixReversed, suffixReversed].sort()[0];
28    }
29
30    return ans;
31}
32
33/**
34 * Reverses the given string.
35 *
36 * @param s - The string to reverse
37 * @returns The reversed string
38 */
39function reverse(s: string): string {
40    return s.split('').reverse().join('');
41}
42

Time and Space Complexity

  • Time Complexity: O(n^2), where n is the length of the string s.

    • The for loop iterates n times (for k from 1 to n).
    • In each iteration, the slicing operations s[:k], s[k:], s[:-k], s[-k:], the reversals [::-1], and the string concatenations each take O(n) time to construct the candidate strings t1 and t2.
    • The min(ans, t1, t2) comparison also takes O(n) time in the worst case, since lexicographic comparison may scan the entire strings.
    • Therefore, the total time is n iterations × O(n) work per iteration = O(n^2).
  • Space Complexity: O(n), where n is the length of the string s.

    • In each iteration, the temporary strings t1, t2, and ans each require O(n) space.
    • These temporaries are created and discarded per iteration (only one set exists at a time), so the peak extra space remains O(n).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Mishandling negative-slice edge cases when building the suffix candidate

The expression s[:-k] looks harmless, but it hides a subtle trap that bites when the code is restructured. In the given loop, k always ranges from 1 to n, so s[:-k] behaves correctly — including the k = n case, where s[:-n] is the empty string. The danger appears when someone "optimizes" or refactors the loop to start from k = 0 (e.g., thinking of k = 0 as "do nothing" instead of using k = 1):

# WRONG: starting the loop at k = 0
for k in range(0, len(s) + 1):
    t2 = s[:-k] + s[-k:][::-1]   # s[:-0] is '' and s[-0:] is the WHOLE string!

With k = 0, s[:-0] evaluates to s[:0], which is the empty string, and s[-0:] evaluates to s[0:], which is the entire string. So t2 becomes the full reversal of s — a candidate that should only correspond to k = n, not k = 0. The code still produces a valid candidate by coincidence here, but in problem variants (e.g., where the operation is optional only for certain k, or where you must record which k achieved the optimum), this silently corrupts the answer.

Solution: Keep k strictly in [1, n], and rely on k = 1 as the "no-op" case as the problem intends. If you ever need index-based slicing that must work for k = 0, write the suffix split with explicit non-negative indices instead of negative ones:

t2 = s[:string_length - k] + s[string_length - k:][::-1]

This form is unambiguous for every k in [0, n] and never falls into the -0 trap.


Pitfall 2: Trying to be "clever" with a greedy shortcut that misses candidates

A tempting optimization is to skip the brute force and reason greedily, e.g., "find the smallest character in s and reverse the prefix that brings it to the front." This fails because:

  1. Ties matter. If the smallest character appears multiple times, different occurrences produce different strings after reversal, and the best choice depends on the characters dragged along with it. For s = "bab", reversing the prefix of length 2 gives "abb", while reversing the suffix of length 2 gives "bba" — picking the wrong occurrence of 'a' loses.
  2. Suffix reversals can win even when they don't improve the first character. The optimal move sometimes changes only middle/late positions, which a "fix the front" greedy never considers.

Solution: For n up to a few thousand, the O(n²) exhaustive enumeration is both simple and fast enough. Only reach for a smarter algorithm if the constraints actually demand it — and if they do, verify any greedy rule against the brute force on random small inputs first:

import random, string

def brute(s):
    ans = s
    for k in range(1, len(s) + 1):
        ans = min(ans, s[:k][::-1] + s[k:], s[:len(s)-k] + s[len(s)-k:][::-1])
    return ans

for _ in range(10000):
    s = ''.join(random.choices('ab', k=random.randint(1, 8)))
    assert my_clever_solution(s) == brute(s), s

Pitfall 3: Forgetting that the operation is mandatory (or assuming it always helps)

Two opposite mistakes occur here:

  • Initializing ans to something other than s (e.g., an empty string or a sentinel like "~"), then assuming the loop will regenerate s. The loop does regenerate s via k = 1, but initializing with ans = "" makes the empty string compare as smallest and returns a wrong answer immediately.
  • Excluding k = 1 from the loop under the belief that "reversing one character is pointless." It is pointless as a transformation, but it is precisely what makes the original string a legal outcome. Skipping it changes nothing in this problem (since ans starts as s), but combined with a bad initialization it produces incorrect results.

Solution: Initialize ans = s and keep the loop range as range(1, n + 1). The two together make the code robust: even if one of them is later modified, the other still guarantees the original string remains a candidate.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More