Lexicographically Smallest String After Operations With Constraint
MedPrompt
You are given a string s and an integer k.
Define a function distance(s1, s2) between two strings s1 and s2 of the same length n as:
- The sum of the minimum distance between
s1[i]ands2[i]when the characters from'a'to'z'are placed in a cyclic order, for alliin the range[0, n - 1].
For example, distance("ab", "cd") == 4, and distance("a", "z") == 1.
You can change any letter of s to any other lowercase English letter, any number of times.
Return a string denoting the lexicographically smallest string t you can get after some changes, such that distance(s, t) <= k.
Example 1:
Input: s = "zbbz", k = 3
Output: "aaaz"
Explanation:
Change s to "aaaz". The distance between "zbbz" and "aaaz" is equal to k = 3.
Example 2:
Input: s = "xaxcd", k = 4
Output: "aawcd"
Explanation:
The distance between "xaxcd" and "aawcd" is equal to k = 4.
Example 3:
Input: s = "lol", k = 0
Output: "lol"
Explanation:
It's impossible to change any character as k = 0.
Constraints:
1 <= s.length <= 1000 <= k <= 2000sconsists only of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
A less efficient but valid approach is to use dynamic programming with memoization. This method systematically explores all possible transformations to find the lexicographically smallest one. We define a recursive function that, for each position in the string, tries every possible character change and recursively solves for the rest of the string. Memoization is used to store the results for subproblems to avoid redundant computations.
Algorithm
- Create a memoization table
memo[n+1][k+1]initialized to a null/sentinel value. - Define a recursive function
solve(i, k):- If
i == n, return"". - If
memo[i][k]is not null, return it. - Initialize
best_result = null. - For each character
cfrom 'a' to 'z':- Calculate
cost = distance(s[i], c). - If
k >= cost:- Recursively call
recursive_suffix = solve(i + 1, k - cost). - Form
candidate = c + recursive_suffix. - If
best_resultis null orcandidateis lexicographically smaller thanbest_result, updatebest_result = candidate.
- Recursively call
- Calculate
- Store
best_resultinmemo[i][k]and return it.
- If
- The final answer is the result of the initial call
solve(0, k).
Walkthrough
The core idea is to build the solution from left to right, making the optimal choice at each position by exploring all possibilities. We define a function solve(i, k) which computes the lexicographically smallest suffix t[i:] given a remaining budget k. To compute solve(i, k), we try setting t[i] to every character c from 'a' to 'z'. For each choice c, we calculate the cost d = distance(s[i], c). If we have enough budget (k >= d), we recursively find the best suffix for the rest of the string: suffix = solve(i + 1, k - d). We then form a candidate result c + suffix and compare it with the best result found so far for the state (i, k). To avoid recomputing results for the same state (i, k), we use a 2D array for memoization. The final answer is the result of the initial call solve(0, initial_k).
class Solution { private String s; private String[][] memo; public String smallestString(String s, int k) { this.s = s; if (k == 0) return s; this.memo = new String[s.length()][k + 1]; return solve(0, k); } private String solve(int index, int remainingK) { if (index == s.length()) { return ""; } if (memo[index][remainingK] != null) { return memo[index][remainingK]; } String bestResult = null; for (char c = 'a'; c <= 'z'; c++) { int cost = distance(s.charAt(index), c); if (remainingK >= cost) { String suffix = solve(index + 1, remainingK - cost); String currentResult = c + suffix; if (bestResult == null || currentResult.compareTo(bestResult) < 0) { bestResult = currentResult; } } } return memo[index][remainingK] = bestResult; } private int distance(char c1, char c2) { int diff = Math.abs(c1 - c2); return Math.min(diff, 26 - diff); }}Complexity
Time
O(n² * k * C), where n is the length of the string, k is the initial budget, and C is the alphabet size (26). The state space is O(n * k). For each state, we iterate through C characters, and string operations (concatenation and comparison) take up to O(n) time.
Space
O(n² * k) to store the memoized suffix strings. Each of the O(n * k) states can store a string of length up to n.
Trade-offs
Pros
Guarantees finding the optimal solution by exhaustively exploring the valid search space.
Provides a structured, albeit inefficient, way to think about the problem.
Cons
Extremely high time and space complexity, making it infeasible for the given constraints.
Repeated string manipulations (concatenation, comparison) are very slow.
The most efficient way to solve this problem is with a greedy approach. Since we want the lexicographically smallest string, we should process the string from left to right and make each character as small as possible. For each character, we attempt to change it to 'a', the smallest possible character. If we have enough budget k, we perform this change and update our budget. If not, we use all of our remaining budget to make the current character as small as we can, which exhausts the budget for any further changes.
Algorithm
- Convert the input string
sinto a character array,result_chars. - Iterate from
i = 0tos.length() - 1:- If
k == 0, break the loop. - Let
c = result_chars[i]. - Calculate
dist_to_a = min(c - 'a', 26 - (c - 'a')). - If
k >= dist_to_a:- Set
result_chars[i] = 'a'. - Decrement
kbydist_to_a.
- Set
- Else:
- We cannot reach 'a'. The smallest character we can reach is
c - k. - Set
result_chars[i] = (char)(c - k). - Set
k = 0.
- We cannot reach 'a'. The smallest character we can reach is
- If
- Convert
result_charsback to a string and return it.
Walkthrough
The problem asks for the lexicographically smallest string. This property strongly suggests a greedy approach. To make a string as small as possible, we should try to make its characters as small as possible, starting from the leftmost character. We iterate through the input string s from left to right (index i = 0 to n-1). At each position i, we try to make the character t[i] as small as possible, which is 'a'. We calculate the cost to change s[i] to 'a'. The cyclic distance to 'a' from a character c is min(c - 'a', 26 - (c - 'a')). If our remaining budget k is sufficient to cover this cost, we make the change: set t[i] = 'a' and update k by subtracting the cost. If the budget is not sufficient, we cannot change s[i] to 'a'. To still get the lexicographically smallest character possible at this position, we must use our entire remaining budget k to move s[i] as close to 'a' as possible. This means changing s[i] to the character s[i] - k. After this operation, our budget k becomes 0. Since the budget is now exhausted, all subsequent characters of t must be the same as in s. The loop can terminate early once k becomes 0. This greedy strategy works because making a character at an earlier position smaller will never prevent us from making a character at a later position smaller, thus the local optimal choices lead to a global optimum.
class Solution { public String smallestString(String s, int k) { if (k == 0) { return s; } char[] resultChars = s.toCharArray(); for (int i = 0; i < resultChars.length; i++) { if (k == 0) { break; } char originalChar = resultChars[i]; // The distance to 'a' is the value of the character minus 'a' // or 26 minus that value, whichever is smaller. int distToA = Math.min(originalChar - 'a', 26 - (originalChar - 'a')); if (distToA <= k) { resultChars[i] = 'a'; k -= distToA; } else { // If we can't reach 'a', use all remaining k to get as close as possible. // The smallest character is achieved by subtracting from the current one. resultChars[i] = (char) (originalChar - k); k = 0; } } return new String(resultChars); }}Complexity
Time
O(n), where n is the length of the string. We perform a single pass through the string.
Space
O(n) to store the character array for the result string. This is because strings are immutable in Java. If modifying the input string were allowed, space complexity would be O(1).
Trade-offs
Pros
Highly efficient with linear time complexity.
Simple and straightforward to implement.
Optimal for the given problem constraints.
Cons
The correctness of the greedy choice might require some reasoning to be fully convinced, although it is intuitive for this problem.
Solutions
Solution
class Solution {public String getSmallestString(String s, int k) { char[] cs = s.toCharArray(); for (int i = 0; i < cs.length; ++i) { char c1 = cs[i]; for (char c2 = 'a'; c2 < c1; ++c2) { int d = Math.min(c1 - c2, 26 - c1 + c2); if (d <= k) { cs[i] = c2; k -= d; break; } } } return new String(cs); }}class Solution {public: string getSmallestString(string s, int k) { for (int i = 0; i < s.size(); ++i) { char c1 = s[i]; for (char c2 = 'a'; c2 < c1; ++c2) { int d = min(c1 - c2, 26 - c1 + c2); if (d <= k) { s[i] = c2; k -= d; break; } } } return s; }};class Solution: def getSmallestString(self, s: str, k: int) -> str: cs = list(s) for i, c1 in enumerate(s): for c2 in ascii_lowercase: if c2 >= c1: break d = min(ord(c1) - ord(c2), 26 - ord(c1) + ord(c2)) if d <= k: cs[i] = c2 k -= d break return "" . join(cs)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.