Facebook Pixel

3106. Lexicographically Smallest String After Operations With Constraint

Problem Description

You are given a string s consisting of lowercase English letters and an integer k.

The problem defines a distance function between two strings of the same length. For two strings s1 and s2 of length n, the distance is calculated as the sum of minimum distances between corresponding characters at each position i (from 0 to n-1).

The key point is that the alphabet is treated as cyclic - meaning after 'z' comes 'a' again. So for any two characters, you can calculate the distance in two ways:

  • Going forward in the alphabet
  • Going backward (wrapping around)

The minimum of these two distances is used.

For example:

  • distance("ab", "cd") = 4 because 'a' to 'c' has distance 2, and 'b' to 'd' has distance 2, totaling 4
  • distance("a", "z") = 1 because you can go backward from 'a' to 'z' in just 1 step

Your task is to transform the string s by changing any of its characters to any other lowercase letter (you can make multiple changes). The goal is to find the lexicographically smallest possible string t such that the distance between the original string s and the new string t is at most k.

A lexicographically smaller string means it would appear earlier in dictionary order - for instance, "abc" is lexicographically smaller than "abd".

You need to return this lexicographically smallest string that can be obtained with a total distance cost not exceeding k.

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

Intuition

To get the lexicographically smallest string, we should try to make characters as small as possible, starting from the leftmost position. This is because the leftmost characters have the highest priority in lexicographical ordering - changing the first character from 'c' to 'a' makes the string smaller than changing any later character.

The greedy approach works here: for each position from left to right, we want to change the current character to the smallest possible character we can afford with our remaining budget k.

For each character in the original string, we check all characters that are smaller than it (from 'a' up to but not including the current character). Why only smaller characters? Because we want the lexicographically smallest result, and changing to a character that's equal or larger wouldn't help achieve that goal.

When considering changing character c1 to a smaller character c2, we need to calculate the cost. Due to the cyclic nature of the alphabet, there are two possible paths:

  • Direct path: ord(c1) - ord(c2) (going backward in the alphabet)
  • Wrap-around path: 26 - ord(c1) + ord(c2) (going forward and wrapping around)

We take the minimum of these two as our actual cost d.

If we can afford this change (i.e., d <= k), we make it immediately, deduct the cost from our budget, and move to the next position. We don't need to consider other characters for this position because we've already found the smallest character we can change to within our budget.

This greedy strategy ensures we get the lexicographically smallest result because we're maximizing the "smallness" of earlier positions, which have more weight in lexicographical comparison.

Learn more about Greedy patterns.

Solution Approach

The implementation follows a greedy traversal approach where we process each character position from left to right:

  1. Convert string to list: First, we convert the input string s to a list cs = list(s) to make it mutable, since strings in Python are immutable and we need to modify characters.

  2. Iterate through each position: We use enumerate(s) to get both the index i and character c1 at each position.

  3. Try smaller characters: For each position, we iterate through all lowercase letters using ascii_lowercase (which gives 'a' to 'z'). We only consider characters c2 that are smaller than the current character c1:

    • The condition if c2 >= c1: break stops the iteration once we reach characters that are not smaller than c1
  4. Calculate minimum distance: For each candidate character c2 that is smaller than c1, we calculate the minimum cyclic distance:

    d = min(ord(c1) - ord(c2), 26 - ord(c1) + ord(c2))
    • ord(c1) - ord(c2): Direct backward distance
    • 26 - ord(c1) + ord(c2): Forward distance with wrap-around
  5. Make the change if affordable: If the distance d is within our budget (d <= k):

    • Update the character at position i: cs[i] = c2
    • Deduct the cost from our budget: k -= d
    • Break from the inner loop to move to the next position
  6. Return the result: After processing all positions, we join the list back into a string using "".join(cs) and return it.

The algorithm ensures optimality because it greedily makes each position as small as possible from left to right, using the available budget efficiently. The time complexity is O(26n) where n is the length of the string, since for each character we potentially check up to 26 lowercase letters.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through the solution with s = "zbbz" and k = 3.

Initial state: cs = ['z', 'b', 'b', 'z'], k = 3

Position 0 (character 'z'):

  • We try to change 'z' to smaller characters starting from 'a'
  • For 'z' → 'a':
    • Direct backward distance: ord('z') - ord('a') = 25
    • Forward wrap-around distance: 26 - ord('z') + ord('a') = 26 - 25 + 0 = 1
    • Minimum distance: min(25, 1) = 1
    • Since 1 ≤ 3, we can make this change!
  • Update: cs[0] = 'a', k = 3 - 1 = 2
  • Result so far: ['a', 'b', 'b', 'z']

Position 1 (character 'b'):

  • We try to change 'b' to 'a' (the only smaller character)
  • For 'b' → 'a':
    • Direct backward distance: ord('b') - ord('a') = 1
    • Forward wrap-around distance: 26 - ord('b') + ord('a') = 26 - 1 + 0 = 25
    • Minimum distance: min(1, 25) = 1
    • Since 1 ≤ 2, we can make this change!
  • Update: cs[1] = 'a', k = 2 - 1 = 1
  • Result so far: ['a', 'a', 'b', 'z']

Position 2 (character 'b'):

  • We try to change 'b' to 'a'
  • For 'b' → 'a':
    • Minimum distance: 1 (as calculated above)
    • Since 1 ≤ 1, we can make this change!
  • Update: cs[2] = 'a', k = 1 - 1 = 0
  • Result so far: ['a', 'a', 'a', 'z']

Position 3 (character 'z'):

  • We have k = 0, so we cannot make any more changes
  • The character remains 'z'

Final result: "aaaz"

The algorithm successfully transformed "zbbz" into "aaaz" using exactly 3 units of distance, creating the lexicographically smallest possible string within the budget.

Solution Implementation

1class Solution:
2    def getSmallestString(self, s: str, k: int) -> str:
3        """
4        Returns the lexicographically smallest string by replacing characters in s
5        with a total distance cost not exceeding k.
6      
7        Args:
8            s: Input string to modify
9            k: Maximum total distance allowed for all character replacements
10      
11        Returns:
12            The lexicographically smallest possible string
13        """
14        # Import ascii_lowercase for iterating through 'a' to 'z'
15        from string import ascii_lowercase
16      
17        # Convert string to list for in-place modifications
18        char_list = list(s)
19      
20        # Process each character position from left to right
21        for i, original_char in enumerate(s):
22            # Try each possible replacement character from 'a' to 'z'
23            for replacement_char in ascii_lowercase:
24                # Skip if replacement is not lexicographically smaller
25                if replacement_char >= original_char:
26                    break
27              
28                # Calculate minimum distance between characters (considering wrap-around)
29                # Distance can be direct (c1 - c2) or wrap-around (26 - c1 + c2)
30                direct_distance = ord(original_char) - ord(replacement_char)
31                wrap_distance = 26 - ord(original_char) + ord(replacement_char)
32                min_distance = min(direct_distance, wrap_distance)
33              
34                # If we have enough budget, make the replacement
35                if min_distance <= k:
36                    char_list[i] = replacement_char
37                    k -= min_distance
38                    break
39      
40        # Convert list back to string and return
41        return "".join(char_list)
42
1class Solution {
2    public String getSmallestString(String s, int k) {
3        // Convert string to character array for in-place modification
4        char[] characters = s.toCharArray();
5      
6        // Process each character from left to right
7        for (int i = 0; i < characters.length; ++i) {
8            char originalChar = characters[i];
9          
10            // Try each character from 'a' up to (but not including) the original character
11            for (char targetChar = 'a'; targetChar < originalChar; ++targetChar) {
12                // Calculate the minimum distance between two characters
13                // Distance can be either direct (c1 - c2) or wrap around (26 - c1 + c2)
14                int distance = Math.min(
15                    originalChar - targetChar,           // Direct distance
16                    26 - originalChar + targetChar       // Wrap-around distance
17                );
18              
19                // If we have enough operations remaining to make this change
20                if (distance <= k) {
21                    // Replace the character with the smaller one
22                    characters[i] = targetChar;
23                    // Deduct the used operations from remaining count
24                    k -= distance;
25                    // Move to the next character position
26                    break;
27                }
28            }
29        }
30      
31        // Convert the modified character array back to string
32        return new String(characters);
33    }
34}
35
1class Solution {
2public:
3    string getSmallestString(string s, int k) {
4        // Iterate through each character in the string
5        for (int i = 0; i < s.size(); ++i) {
6            char currentChar = s[i];
7          
8            // Try to replace current character with a lexicographically smaller one
9            // Starting from 'a' up to (but not including) the current character
10            for (char targetChar = 'a'; targetChar < currentChar; ++targetChar) {
11                // Calculate the minimum distance to change currentChar to targetChar
12                // Two possible paths: direct distance or wrap around through 'z' to 'a'
13                int directDistance = currentChar - targetChar;
14                int wrapAroundDistance = 26 - currentChar + targetChar;
15                int minDistance = min(directDistance, wrapAroundDistance);
16              
17                // If we have enough operations remaining, make the replacement
18                if (minDistance <= k) {
19                    s[i] = targetChar;
20                    k -= minDistance;
21                    break;  // Move to the next character in the string
22                }
23            }
24        }
25      
26        return s;
27    }
28};
29
1/**
2 * Finds the lexicographically smallest string by replacing characters in the input string
3 * with a total distance of at most k
4 * @param s - The input string to modify
5 * @param k - The maximum total distance allowed for all character replacements
6 * @returns The lexicographically smallest possible string
7 */
8function getSmallestString(s: string, k: number): string {
9    // Convert string to character array for easier manipulation
10    const charArray: string[] = s.split('');
11  
12    // Iterate through each character position in the string
13    for (let i = 0; i < s.length; ++i) {
14        // Get ASCII code of current character
15        const currentCharCode = s[i].charCodeAt(0);
16      
17        // Try each possible lowercase letter starting from 'a' (ASCII 97)
18        // Stop before reaching the current character
19        for (let targetCharCode = 97; targetCharCode < currentCharCode; ++targetCharCode) {
20            // Calculate the minimum distance to transform current character to target
21            // Distance can be calculated in two ways:
22            // 1. Direct distance: currentCharCode - targetCharCode
23            // 2. Wrap-around distance: go backward from current char to 'z', then from 'a' to target
24            const directDistance = currentCharCode - targetCharCode;
25            const wrapAroundDistance = 26 - currentCharCode + targetCharCode;
26            const minDistance = Math.min(directDistance, wrapAroundDistance);
27          
28            // If we have enough remaining k to make this transformation
29            if (minDistance <= k) {
30                // Replace the character with the smaller one
31                charArray[i] = String.fromCharCode(targetCharCode);
32                // Decrease k by the distance used
33                k -= minDistance;
34                // Move to the next character position
35                break;
36            }
37        }
38    }
39  
40    // Join the character array back into a string
41    return charArray.join('');
42}
43

Time and Space Complexity

Time Complexity: O(n × |Σ|) where n is the length of the string s and |Σ| is the size of the character set (at most 26 for lowercase English letters).

The algorithm iterates through each character in the string s (outer loop runs n times). For each character position, it iterates through the lowercase alphabet checking for potential replacements (inner loop runs at most |Σ| = 26 times). The inner loop breaks as soon as it finds a valid replacement or when c2 >= c1, but in the worst case, it may check all 26 letters. All operations inside the nested loops (comparisons, arithmetic operations, and character replacement) take O(1) time.

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

The algorithm creates a list cs from the input string s, which requires O(n) space to store all characters. The variable ascii_lowercase is a constant string of 26 characters (O(1) space), and other variables like i, c1, c2, d, and k use O(1) space. The final string construction with "".join(cs) creates a new string of length n, but this is typically considered part of the output rather than auxiliary space.

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

Common Pitfalls

Pitfall 1: Not Considering All Possible Replacements

The Issue: The current implementation only tries to replace characters with lexicographically smaller ones. However, this misses opportunities where we might need to replace a character with a lexicographically larger one at an earlier position to save budget for more impactful changes at later positions.

Example: Consider s = "zzzzzz" with k = 3. The optimal answer is "aaazzz" (changing first 3 'z's to 'a's, each costing 1 due to wrap-around). However, if we had a string like "lmnxyz" with limited k, we might want to keep early characters unchanged or even change them to slightly larger characters if it helps us make more significant improvements to later positions.

The Real Issue: Actually, the main pitfall is that the code stops looking for replacements once it finds a character >= the original. This means it won't consider changing a character to itself (distance 0) or to a larger character that might have a small cyclic distance.

Corrected Solution:

class Solution:
    def getSmallestString(self, s: str, k: int) -> str:
        from string import ascii_lowercase
      
        char_list = list(s)
      
        for i, original_char in enumerate(s):
            best_char = original_char
            best_cost = 0
          
            # Try all possible characters, not just smaller ones
            for replacement_char in ascii_lowercase:
                # Calculate minimum cyclic distance
                diff = abs(ord(original_char) - ord(replacement_char))
                min_distance = min(diff, 26 - diff)
              
                # If this replacement is affordable and better
                if min_distance <= k:
                    # Choose the lexicographically smallest character within budget
                    if replacement_char < best_char:
                        best_char = replacement_char
                        best_cost = min_distance
                    # Break early if we found 'a' (can't do better)
                    if best_char == 'a':
                        break
          
            # Apply the best replacement found
            if best_char != original_char:
                char_list[i] = best_char
                k -= best_cost
      
        return "".join(char_list)

Pitfall 2: Incorrect Distance Calculation for Wrap-Around

The Issue: When calculating wrap-around distance, the formula 26 - ord(c1) + ord(c2) assumes going from c1 forward to c2. This works when c2 < c1, but if we're considering all possible replacements, we need a more general formula.

Correct Approach: Use min(abs(ord(c1) - ord(c2)), 26 - abs(ord(c1) - ord(c2))) which correctly handles all cases regardless of which character is larger.

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

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

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

Load More