1663. Smallest String With A Given Numeric Value
Problem Description
You need to construct a string of lowercase letters with specific properties.
Given two integers n
and k
, you must create a string that:
- Has exactly
n
characters (all lowercase letters) - Has a total numeric value equal to
k
- Is the lexicographically smallest possible string meeting these requirements
The numeric value system works as follows:
- Each lowercase letter has a numeric value based on its position in the alphabet
'a'
= 1,'b'
= 2,'c'
= 3, ...,'z'
= 26- The numeric value of a string is the sum of all its characters' numeric values
For example, the string "abe"
has numeric value 1 + 2 + 5 = 8.
Lexicographically smallest means the string that would appear first in dictionary order. Between two strings, the one with characters closer to the beginning of the alphabet in earlier positions is considered smaller. For instance, "aaz"
is lexicographically smaller than "aba"
because at the first differing position (index 1), 'a'
comes before 'b'
.
The challenge is to balance these requirements: you want the smallest possible string alphabetically while ensuring the sum of character values equals exactly k
.
Intuition
To get the lexicographically smallest string, we want as many 'a'
characters as possible at the beginning of the string, since 'a'
is the smallest letter alphabetically.
Let's think about this step by step. If we start by filling all n
positions with 'a'
, we get a string with numeric value n
(since each 'a'
contributes 1). But we need the total value to be k
, so we're short by k - n
.
Now, how do we add this remaining value while keeping the string lexicographically smallest? The key insight is that we should modify characters from the end of the string, not the beginning. Why? Because changing characters at the end has less impact on lexicographic order than changing characters at the beginning.
For example, "aaz"
is smaller than "baa"
even though both have the same numeric value. This tells us we should keep the front characters as 'a'
and push larger characters toward the back.
The greedy strategy becomes clear: starting from the rightmost position, we should maximize each character's value (up to 'z'
which has value 26) before moving to the next position on the left. Since each 'a'
already contributes 1, we can upgrade an 'a'
to 'z'
by adding 25 to our total.
Working from right to left, we keep replacing 'a'
with 'z'
as long as we need to add 25 or more to reach our target. When the remaining value to add is less than 25, we know we can achieve it by upgrading the current 'a'
to some intermediate letter between 'a'
and 'z'
.
This approach guarantees the lexicographically smallest string because we maintain as many 'a'
characters as possible at the beginning while using larger characters only at the end where they have minimal impact on lexicographic ordering.
Learn more about Greedy patterns.
Solution Approach
The implementation follows a greedy approach by initially creating a string of all 'a'
characters, then strategically replacing characters from right to left.
Step 1: Initialize the string
ans = ['a'] * n
We create a list of n
characters, all initialized to 'a'
. This gives us the lexicographically smallest possible base string with numeric value n
.
Step 2: Calculate the deficit
i, d = n - 1, k - n
i
is set ton - 1
(the last index of the string)d
represents the deficit - how much more value we need to add to reachk
- Since we have
n
characters of'a'
(total value =n
), we need to addk - n
more
Step 3: Greedily fill from right to left
while d > 25: ans[i] = 'z' d -= 25 i -= 1
Starting from the rightmost position, we replace 'a'
with 'z'
whenever possible. Each replacement adds 25 to our total value (since 'z'
= 26 and 'a'
= 1, the difference is 25). We continue this process moving leftward as long as the remaining deficit d
is greater than 25.
Step 4: Handle the remaining deficit
ans[i] = chr(ord(ans[i]) + d)
Once the deficit d
is 25 or less, we can satisfy it by replacing the current 'a'
at position i
with an appropriate character. We add d
to the ASCII value of 'a'
to get the exact character needed. For example, if d = 3
, we replace 'a'
with 'd'
(since 'a'
+ 3 = 'd'
).
Step 5: Return the result
return ''.join(ans)
Convert the list of characters back to a string and return it.
This algorithm runs in O(n)
time and uses O(n)
space for storing the result string. The greedy approach ensures we get the lexicographically smallest string by keeping as many 'a'
characters as possible at the beginning of the string.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through an example with n = 5
and k = 73
.
Step 1: Initialize with all 'a's
- Create string:
['a', 'a', 'a', 'a', 'a']
- Current numeric value: 1 + 1 + 1 + 1 + 1 = 5
- We need total value of 73, so deficit
d = 73 - 5 = 68
Step 2: Start from the rightmost position
- Position
i = 4
(last index) - Deficit
d = 68
Step 3: Replace 'a's with 'z's from right to left
First iteration:
- Since
d = 68 > 25
, replaceans[4]
with 'z' - String becomes:
['a', 'a', 'a', 'a', 'z']
- Update:
d = 68 - 25 = 43
,i = 3
Second iteration:
- Since
d = 43 > 25
, replaceans[3]
with 'z' - String becomes:
['a', 'a', 'a', 'z', 'z']
- Update:
d = 43 - 25 = 18
,i = 2
Third iteration:
- Since
d = 18 < 25
, we exit the while loop
Step 4: Handle remaining deficit
- Current position
i = 2
, deficitd = 18
- Replace
ans[2]
with the character that's 18 positions after 'a' - 'a' + 18 = 's' (since 'a' is 1st letter, 's' is 19th letter)
- String becomes:
['a', 'a', 's', 'z', 'z']
Step 5: Return result
- Final string:
"aaszz"
Verification:
- Numeric value: 1 + 1 + 19 + 26 + 26 = 73 β
- Length: 5 characters β
- Lexicographically smallest: We have the maximum possible 'a's at the beginning β
This approach ensures we get "aaszz"
which is lexicographically smaller than alternatives like "abczz"
(which would also sum to 73) because we maximized the number of 'a's at the front of the string.
Solution Implementation
1class Solution:
2 def getSmallestString(self, n: int, k: int) -> str:
3 # Initialize result array with all 'a's (smallest possible characters)
4 result = ['a'] * n
5
6 # Start from the rightmost position
7 position = n - 1
8
9 # Calculate the excess value we need to distribute
10 # (k - n) because we already have n 'a's, each worth 1
11 excess_value = k - n
12
13 # Greedily place 'z' characters from right to left
14 # Each 'z' adds 25 to the value (since 'z'=26 and 'a'=1)
15 while excess_value > 25:
16 result[position] = 'z'
17 excess_value -= 25
18 position -= 1
19
20 # Place the remaining value at the current position
21 # Convert the character at current position from 'a' to the required character
22 result[position] = chr(ord(result[position]) + excess_value)
23
24 # Convert list to string and return
25 return ''.join(result)
26
1class Solution {
2 public String getSmallestString(int n, int k) {
3 // Initialize result array with all 'a's (minimum possible characters)
4 char[] result = new char[n];
5 Arrays.fill(result, 'a');
6
7 // Start from the rightmost position
8 int currentIndex = n - 1;
9
10 // Calculate remaining value to distribute after assigning 'a' to all positions
11 // Each 'a' has value 1, so n positions with 'a' gives us n total
12 int remainingValue = k - n;
13
14 // Greedily place 'z' characters from right to left
15 // Each 'z' adds 25 more than 'a' (26 - 1 = 25)
16 while (remainingValue > 25) {
17 result[currentIndex] = 'z';
18 currentIndex--;
19 remainingValue -= 25;
20 }
21
22 // Place the remaining value at the current position
23 // This creates a character between 'a' and 'z'
24 result[currentIndex] = (char) ('a' + remainingValue);
25
26 // Convert character array to string and return
27 return String.valueOf(result);
28 }
29}
30
1class Solution {
2public:
3 string getSmallestString(int n, int k) {
4 // Initialize result string with all 'a's (each 'a' has value 1)
5 string result(n, 'a');
6
7 // Start from the last position
8 int currentIndex = n - 1;
9
10 // Calculate the difference we need to add beyond the base sum of n 'a's
11 int remainingDifference = k - n;
12
13 // Greedily place 'z' characters from the end while we have enough difference
14 // Each 'z' adds 25 to the difference (26 - 1 = 25)
15 while (remainingDifference > 25) {
16 result[currentIndex] = 'z';
17 currentIndex--;
18 remainingDifference -= 25;
19 }
20
21 // Add the remaining difference to the current position
22 // This creates a character between 'a' and 'z'
23 result[currentIndex] += remainingDifference;
24
25 return result;
26 }
27};
28
1function getSmallestString(n: number, k: number): string {
2 // Initialize result array with all 'a's (each 'a' has value 1)
3 // Using array for easier character manipulation
4 const resultArray: string[] = new Array(n).fill('a');
5
6 // Start from the last position to place larger characters
7 let currentIndex: number = n - 1;
8
9 // Calculate the difference we need to add beyond the base sum of n 'a's
10 // Since we start with n 'a's (sum = n), we need to add (k - n) more
11 let remainingDifference: number = k - n;
12
13 // Greedily place 'z' characters from the end while we have enough difference
14 // Each 'z' adds 25 to the difference (26 - 1 = 25)
15 // This ensures lexicographically smallest string
16 while (remainingDifference > 25) {
17 resultArray[currentIndex] = 'z';
18 currentIndex--;
19 remainingDifference -= 25;
20 }
21
22 // Add the remaining difference to the current position
23 // This creates a character between 'a' and 'z'
24 // Convert the remaining difference to the appropriate character
25 if (remainingDifference > 0) {
26 resultArray[currentIndex] = String.fromCharCode(
27 'a'.charCodeAt(0) + remainingDifference
28 );
29 }
30
31 // Join the array to form the final string
32 return resultArray.join('');
33}
34
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string. The algorithm initializes an array of size n
with 'a' characters, which takes O(n)
time. Then it iterates through the array from the end, potentially visiting each position once in the worst case, which is also O(n)
. The while loop can run at most n
times since we start from index n-1
and decrement i
in each iteration until we've distributed all the extra value d
. Therefore, the overall time complexity is O(n)
.
The space complexity is O(n)
for the output string stored in the ans
array. However, if we exclude the space required for the answer (as mentioned in the reference), the space complexity is O(1)
since we only use a constant amount of extra space for variables i
and d
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error in Character Calculation
A common mistake is incorrectly calculating the character value when handling the remaining deficit. Developers might write:
result[position] = chr(ord('a') + excess_value)
instead of:
result[position] = chr(ord(result[position]) + excess_value)
While both work in this specific case (since result[position]
is always 'a'
at that point), the second form is more general and clearer in intent.
2. Integer Overflow in Character Conversion
When adding excess_value
to a character, there's a risk of creating an invalid character if not careful:
# Problematic if excess_value is too large
result[position] = chr(ord('a') + excess_value)
Solution: The algorithm already handles this by ensuring excess_value <= 25
before the character conversion, but it's crucial to maintain this invariant. Add validation:
if excess_value > 25:
raise ValueError("Invalid state: excess_value should not exceed 25")
result[position] = chr(ord('a') + excess_value)
3. Forgetting Edge Cases
The algorithm assumes valid inputs but might fail on edge cases:
- When
k < n
: Not enough value to create n characters (minimum would be n 'a's = n) - When
k > 26 * n
: Not enough capacity even with all 'z's
Solution: Add input validation:
def getSmallestString(self, n: int, k: int) -> str:
# Validate inputs
if k < n or k > 26 * n:
return "" # or raise an exception
# Rest of the implementation...
4. Using String Concatenation Instead of List
A performance pitfall is building the result using string concatenation:
# Inefficient approach
result = 'a' * n
for i in range(n-1, -1, -1):
if excess_value > 25:
result = result[:i] + 'z' + result[i+1:] # O(n) operation
excess_value -= 25
This creates new string objects repeatedly, leading to O(nΒ²) time complexity.
Solution: Always use a list for character manipulation and join at the end, as shown in the correct implementation.
5. Incorrect Deficit Calculation
Some might calculate the deficit as just k
instead of k - n
:
# Wrong: forgets that we already have n 'a's excess_value = k
This would result in a string with total value k + n
instead of k
.
Solution: Remember that initializing with n 'a's already gives us a value of n, so we only need to add k - n
more value.
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!