Facebook Pixel

2734. Lexicographically Smallest String After Substring Operation

Problem Description

You are given a string s consisting of lowercase English letters. You need to perform exactly one operation on this string to make it lexicographically smallest.

The operation allows you to:

  • Select any non-empty substring from s
  • Replace every letter in that substring with the preceding letter in the English alphabet
    • For example: 'b' becomes 'a', 'c' becomes 'b', and so on
    • Special case: 'a' wraps around to become 'z'

Your goal is to return the lexicographically smallest string possible after performing this operation exactly once.

For example:

  • If s = "cbabc", you could select the substring "cba" and transform it to "baz", resulting in "bazbc"
  • If s = "acbbc", you could select the substring "cbbc" and transform it to "baab", resulting in "abaab"
  • If s = "aaa", since all characters are already 'a', the best strategy is to change the last 'a' to 'z', resulting in "aaz"

The key insight is that to get the lexicographically smallest result, you want to:

  1. Skip any leading 'a' characters (since changing 'a' to 'z' makes it larger)
  2. Find the first non-'a' character and start the operation there
  3. Continue the operation until you hit another 'a' or reach the end of the string
  4. If the entire string is all 'a's, change only the last character to 'z'
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

To make the string lexicographically smallest, we need to think about which substring to select and transform. Since we want the smallest possible result, we should focus on making the leftmost characters as small as possible.

Let's think about what happens when we apply the operation:

  • Characters like 'b', 'c', 'd' become 'a', 'b', 'c' respectively - they get smaller
  • The character 'a' becomes 'z' - it gets larger

This tells us that we should avoid transforming 'a' characters whenever possible, as turning 'a' into 'z' makes the string larger, not smaller.

The greedy strategy emerges from this observation:

  1. We want to start our transformation as early as possible in the string to affect the most significant positions
  2. But we should skip any leading 'a' characters since transforming them would make the string larger
  3. Once we find the first non-'a' character, we should start our transformation there
  4. We should continue the transformation as long as we encounter non-'a' characters, because each transformation makes those characters smaller
  5. We should stop when we hit another 'a', because including it in our transformation would turn it into 'z', making the string larger

The special case is when the entire string consists only of 'a' characters. In this situation, we must perform the operation (as required by the problem), so the best we can do is transform just the last 'a' to 'z'. This minimizes the impact on the lexicographical order since the last position is the least significant.

For example, with s = "acbbc":

  • Skip the leading 'a'
  • Start transforming from 'c' at position 1
  • Continue through all non-'a' characters: "cbbc" β†’ "baab"
  • Result: "abaab"

This greedy approach guarantees the lexicographically smallest result because we're making the earliest possible non-'a' characters smaller while avoiding making any 'a' characters larger (except when unavoidable).

Learn more about Greedy patterns.

Solution Approach

The implementation follows a greedy algorithm with two pointers to identify the optimal substring to transform:

Step 1: Find the starting position

i = 0
while i < n and s[i] == "a":
    i += 1

We traverse from the beginning of the string to find the first non-'a' character. This position i marks where our transformation should begin, as we want to skip all leading 'a' characters.

Step 2: Handle the special case

if i == n:
    return s[:-1] + "z"

If i reaches the end of the string (i == n), it means the entire string consists only of 'a' characters. In this case, we must still perform the operation, so we transform only the last character from 'a' to 'z' to minimize the lexicographical impact.

Step 3: Find the ending position

j = i
while j < n and s[j] != "a":
    j += 1

Starting from position i, we find the first 'a' character or reach the end of the string. This position j marks where our transformation should end, as we want to transform all consecutive non-'a' characters.

Step 4: Perform the transformation

return s[:i] + "".join(chr(ord(c) - 1) for c in s[i:j]) + s[j:]

We construct the result by:

  • Keeping the prefix s[:i] unchanged (all the leading 'a' characters)
  • Transforming the substring s[i:j] by decrementing each character using chr(ord(c) - 1)
  • Keeping the suffix s[j:] unchanged (everything after our selected substring)

The transformation uses ASCII values: ord(c) gets the ASCII value of character c, we subtract 1 to get the previous letter, and chr() converts it back to a character.

Time Complexity: O(n) where n is the length of the string, as we traverse the string once and perform the transformation in linear time.

Space Complexity: O(n) for creating the result string.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with the example s = "acbbc":

Step 1: Find the starting position

  • Start at index 0: s[0] = 'a'
  • Since it's an 'a', increment i to 1
  • Check s[1] = 'c' (not 'a'), so stop here
  • Starting position i = 1

Step 2: Check for special case

  • Since i = 1 (not equal to string length 5), this is not an all-'a' string
  • Continue to step 3

Step 3: Find the ending position

  • Start from j = i = 1
  • s[1] = 'c' (not 'a'), increment j to 2
  • s[2] = 'b' (not 'a'), increment j to 3
  • s[3] = 'b' (not 'a'), increment j to 4
  • s[4] = 'c' (not 'a'), increment j to 5
  • Reached end of string, so j = 5

Step 4: Perform the transformation

  • Prefix (unchanged): s[0:1] = "a"
  • Substring to transform: s[1:5] = "cbbc"
    • 'c' β†’ 'b' (ASCII 99 β†’ 98)
    • 'b' β†’ 'a' (ASCII 98 β†’ 97)
    • 'b' β†’ 'a' (ASCII 98 β†’ 97)
    • 'c' β†’ 'b' (ASCII 99 β†’ 98)
    • Transformed substring: "baab"
  • Suffix (unchanged): s[5:] = "" (empty)

Final result: "a" + "baab" + "" = "abaab"

This is lexicographically smaller than the original "acbbc" because we've made all the non-'a' characters smaller while avoiding turning any 'a' into 'z'.

Solution Implementation

1class Solution:
2    def smallestString(self, s: str) -> str:
3        """
4        Find the lexicographically smallest string by applying exactly one operation:
5        - Choose a non-empty substring where no character is 'a'
6        - Decrease each character in the substring by one ('b' -> 'a', 'c' -> 'b', etc.)
7      
8        Args:
9            s: Input string consisting of lowercase English letters
10          
11        Returns:
12            The lexicographically smallest string after applying exactly one operation
13        """
14        n = len(s)
15      
16        # Find the first non-'a' character position
17        start_index = 0
18        while start_index < n and s[start_index] == 'a':
19            start_index += 1
20      
21        # Special case: if all characters are 'a', change the last 'a' to 'z'
22        # This ensures we perform exactly one operation
23        if start_index == n:
24            return s[:-1] + 'z'
25      
26        # Find the end of the continuous non-'a' substring
27        end_index = start_index
28        while end_index < n and s[end_index] != 'a':
29            end_index += 1
30      
31        # Build the result string:
32        # - Keep prefix of 'a's unchanged
33        # - Decrease each character in the selected substring by 1
34        # - Keep the remaining suffix unchanged
35        modified_substring = ''.join(chr(ord(char) - 1) for char in s[start_index:end_index])
36        result = s[:start_index] + modified_substring + s[end_index:]
37      
38        return result
39
1class Solution {
2    public String smallestString(String s) {
3        int length = s.length();
4        int startIndex = 0;
5      
6        // Find the first non-'a' character position
7        while (startIndex < length && s.charAt(startIndex) == 'a') {
8            startIndex++;
9        }
10      
11        // Special case: if all characters are 'a', change the last character to 'z'
12        if (startIndex == length) {
13            return s.substring(0, length - 1) + "z";
14        }
15      
16        // Convert string to character array for modification
17        char[] charArray = s.toCharArray();
18        int currentIndex = startIndex;
19      
20        // Decrement all consecutive non-'a' characters starting from startIndex
21        // Stop when we encounter an 'a' or reach the end of string
22        while (currentIndex < length && charArray[currentIndex] != 'a') {
23            charArray[currentIndex] = (char) (charArray[currentIndex] - 1);
24            currentIndex++;
25        }
26      
27        // Convert character array back to string and return
28        return String.valueOf(charArray);
29    }
30}
31
1class Solution {
2public:
3    string smallestString(string s) {
4        int stringLength = s.size();
5        int startIndex = 0;
6      
7        // Skip all leading 'a' characters
8        while (startIndex < stringLength && s[startIndex] == 'a') {
9            ++startIndex;
10        }
11      
12        // Special case: if the entire string consists of 'a's
13        // Change the last character to 'z' (one operation required)
14        if (startIndex == stringLength) {
15            s[stringLength - 1] = 'z';
16            return s;
17        }
18      
19        // Find the first contiguous non-'a' substring and decrement each character
20        int currentIndex = startIndex;
21        while (currentIndex < stringLength && s[currentIndex] != 'a') {
22            // Decrement the character by 1 (e.g., 'b' -> 'a', 'c' -> 'b')
23            s[currentIndex] = s[currentIndex] - 1;
24            ++currentIndex;
25        }
26      
27        return s;
28    }
29};
30
1/**
2 * Finds the lexicographically smallest string by performing one operation:
3 * - Choose a substring where no character is 'a' and decrease each character by one
4 * - Special case: if all characters are 'a', change the last character to 'z'
5 * 
6 * @param s - The input string to process
7 * @returns The lexicographically smallest string after one operation
8 */
9function smallestString(s: string): string {
10    // Convert string to character array for easier manipulation
11    const characters: string[] = s.split('');
12    const length: number = characters.length;
13  
14    // Find the first non-'a' character
15    let firstNonAIndex: number = 0;
16    while (firstNonAIndex < length && characters[firstNonAIndex] === 'a') {
17        firstNonAIndex++;
18    }
19
20    // Special case: if all characters are 'a', change the last character to 'z'
21    if (firstNonAIndex === length) {
22        characters[length - 1] = 'z';
23        return characters.join('');
24    }
25
26    // Process consecutive non-'a' characters starting from the first non-'a'
27    let currentIndex: number = firstNonAIndex;
28    while (currentIndex < length && characters[currentIndex] !== 'a') {
29        // Decrease the character by one (e.g., 'b' -> 'a', 'c' -> 'b')
30        const charCode: number = characters[currentIndex].charCodeAt(0);
31        characters[currentIndex] = String.fromCharCode(charCode - 1);
32        currentIndex++;
33    }
34
35    // Join the character array back into a string
36    return characters.join('');
37}
38

Time and Space Complexity

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

The algorithm performs the following operations:

  • First while loop: iterates through the string to skip leading 'a' characters - O(n) in worst case
  • Second while loop: iterates through the string to find the next 'a' character - O(n) in worst case
  • String slicing operations s[:i], s[i:j], and s[j:] - each takes O(n) in worst case
  • The join operation with list comprehension iterates through at most n characters - O(n)
  • Character operations chr(ord(c) - 1) are O(1) for each character

Since all operations are sequential and each is at most O(n), the overall time complexity is O(n).

Space Complexity: O(n), where n is the length of the string s.

The space usage includes:

  • The list comprehension [chr(ord(c) - 1) for c in s[i:j]] creates a new list that can contain up to n characters - O(n)
  • String slicing creates new string objects for s[:i], s[i:j], and s[j:] - O(n) in total
  • The final concatenated string result - O(n)
  • Variables i, j, and n use constant space - O(1)

The dominant space usage comes from creating new strings and the intermediate list, resulting in O(n) space complexity.

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

Common Pitfalls

1. Forgetting the "Exactly Once" Requirement

Pitfall: A common mistake is focusing only on making the string lexicographically smallest without ensuring the operation is performed exactly once. This leads to incorrect handling of strings consisting entirely of 'a' characters.

Incorrect approach:

# Wrong: Returns the original string without performing any operation
def smallestString(self, s: str) -> str:
    n = len(s)
    i = 0
    while i < n and s[i] == 'a':
        i += 1
  
    if i == n:
        return s  # ERROR: No operation performed!
  
    # ... rest of the code

Why it's wrong: For input like s = "aaa", this would return "aaa" without performing any operation, violating the constraint that we must perform exactly one operation.

Correct solution: Always perform the operation, even if it makes the string lexicographically larger:

if i == n:
    return s[:-1] + 'z'  # Transform the last 'a' to 'z'

2. Incorrectly Handling Character Wrap-Around

Pitfall: Forgetting that 'a' should wrap around to 'z' when decreased, not become an invalid character.

Incorrect approach:

# Wrong: Blindly decrements all characters without handling 'a' specially
def smallestString(self, s: str) -> str:
    # Find any substring and decrease all characters
    result = ""
    for i in range(len(s)):
        result += chr(ord(s[i]) - 1)  # ERROR: 'a' becomes '`' (invalid)
    return result

Why it's wrong: The ASCII value of 'a' is 97, and subtracting 1 gives 96, which is the backtick character '`', not 'z'.

Correct solution: Handle the wrap-around case explicitly:

def decrease_char(c):
    if c == 'a':
        return 'z'
    else:
        return chr(ord(c) - 1)

However, the optimal algorithm avoids this issue by strategically selecting substrings that don't contain 'a' (except for the special all-'a' case).

3. Selecting the Wrong Substring

Pitfall: Choosing a substring that doesn't lead to the lexicographically smallest result, such as transforming characters that are already 'a' or stopping the transformation too early.

Incorrect approach:

# Wrong: Transforms only the first non-'a' character
def smallestString(self, s: str) -> str:
    for i in range(len(s)):
        if s[i] != 'a':
            return s[:i] + chr(ord(s[i]) - 1) + s[i+1:]
    return s[:-1] + 'z'

Why it's wrong: For input s = "bcb", this would transform only the first 'b' to get "acb", but the optimal solution is to transform "bcb" entirely to get "aba".

Correct solution: Transform all consecutive non-'a' characters starting from the first non-'a':

j = i
while j < n and s[j] != 'a':
    j += 1
# Transform the entire substring s[i:j]

4. Off-by-One Errors in Substring Selection

Pitfall: Incorrectly handling the indices when slicing the string, leading to missing characters or including wrong characters in the transformation.

Incorrect approach:

# Wrong: Incorrect slicing
result = s[:i-1] + modified + s[j+1:]  # ERROR: Wrong indices

Correct solution: Use proper Python slicing conventions:

result = s[:i] + modified_substring + s[j:]

Remember that s[:i] includes characters from index 0 to i-1, and s[j:] includes characters from index j to the end.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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

Load More