3722. Lexicographically Smallest String After Reverse
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
kcharacters ofs(the prefix of lengthk), or - Reverse the last
kcharacters ofs(the suffix of lengthk).
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 = 1reverses 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 = 3characters gives"acdb". - Reversing the first
k = 2characters gives"cdab". - Reversing the last
k = 2characters gives"dcba".
Among all possible choices of k and both directions (prefix or suffix), you need to find the smallest resulting string.
How We Pick the Algorithm
Why Simulation / Basic DSA?
This problem maps to Simulation / Basic DSA through a short path in the full flowchart.
With only 2n possible outcomes, enumerating every prefix/suffix reversal and comparing strings lexicographically is simple and sufficient.
Open in FlowchartIntuition
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
2ncandidates, the total work isO(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 firstkcharacters.[::-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 firstn - kcharacters unchanged.s[-k:]extracts the lastkcharacters, and[::-1]reverses them.
Concatenating them gives the string after reversing the last k characters.
Note: when
k = n, the slices[:-k]evaluates to the empty string, sot2is simply the full reversal ofs— 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 runsntimes, and each iteration performs slicing, reversal, concatenation, and string comparison — each of these isO(n). - Space complexity:
O(n). At any moment we only hold a constant number of temporary strings (t1,t2,ans), each of lengthn.
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 — notes[:-4]is the empty string, handled cleanly by slicing)ans = min("acdb", "bacd", "bacd")="acdb"— no improvement
Summary of all candidates:
k | Prefix reversal (t1) | Suffix reversal (t2) |
|---|---|---|
| 1 | dcab | dcab |
| 2 | cdab | dcba |
| 3 | acdb | dbac |
| 4 | bacd | bacd |
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
191class 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}
321class 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};
271/**
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}
42Time and Space Complexity
-
Time Complexity:
O(n^2), wherenis the length of the strings.- The
forloop iteratesntimes (forkfrom1ton). - In each iteration, the slicing operations
s[:k],s[k:],s[:-k],s[-k:], the reversals[::-1], and the string concatenations each takeO(n)time to construct the candidate stringst1andt2. - The
min(ans, t1, t2)comparison also takesO(n)time in the worst case, since lexicographic comparison may scan the entire strings. - Therefore, the total time is
niterations ×O(n)work per iteration =O(n^2).
- The
-
Space Complexity:
O(n), wherenis the length of the strings.- In each iteration, the temporary strings
t1,t2, andanseach requireO(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).
- In each iteration, the temporary strings
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:
- 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. - 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
ansto something other thans(e.g., an empty string or a sentinel like"~"), then assuming the loop will regenerates. The loop does regeneratesviak = 1, but initializing withans = ""makes the empty string compare as smallest and returns a wrong answer immediately. - Excluding
k = 1from 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 (sinceansstarts ass), 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 RoadmapWhich algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Two Pointers Technique Explained If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe width 560 height 315 src https www youtube nocookie com embed rQhNcycbf8w si lE7qtd1h_JSQwGpW title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!