LeetCode 1663 - Smallest String With A Given Numeric Value
The problem asks us to construct a lowercase string of length n such that the sum of the numeric values of its character
Difficulty: 🟡 Medium
Topics: String, Greedy
Solution
Problem Understanding
The problem asks us to construct a lowercase string of length n such that the sum of the numeric values of its characters equals k, while also ensuring the resulting string is lexicographically smallest.
Each lowercase letter has a numeric value equal to its position in the alphabet:
a = 1b = 2c = 3- ...
z = 26
For example, the string "abe" has a numeric value of:
1 + 2 + 5 = 8
We are given two integers:
n, the required length of the stringk, the required total numeric value
Our task is to return the smallest lexicographical string satisfying both constraints.
Lexicographical ordering behaves like dictionary ordering. A string is smaller if it has a smaller character at the earliest position where the strings differ. This detail is extremely important because it tells us how to optimize the construction.
For example, consider:
"aay"→ starts with"aa""aby"→ starts with"ab"
Since 'a' < 'b' at the second character, "aay" is lexicographically smaller.
The challenge is not merely finding any valid string, but the smallest possible one.
The constraints provide important insight:
1 <= n <= 10^5n <= k <= 26 * n
The lower bound k >= n guarantees that a solution exists, because we can always fill the string with 'a', which contributes 1 each.
The upper bound k <= 26 * n guarantees we never need more than 'z' in any position.
Since n can be as large as 100,000, exponential or brute-force enumeration is impossible. We need a solution close to linear time.
Several edge cases immediately stand out:
- When
k = n, every character must be'a' - When
k = 26 * n, every character must be'z' - When only a small adjustment beyond all
'a'characters is needed - When large values force multiple trailing
'z'characters - When the optimal string contains a mix of
'a', one middle character, and several'z'characters
The problem guarantees a valid answer always exists, so we never need to handle impossible inputs.
Approaches
Brute Force Approach
A brute-force solution would try all possible lowercase strings of length n, calculate their numeric values, and return the lexicographically smallest valid one.
We could imagine generating strings recursively:
- At each position, try every character from
'a'to'z' - Continue until length
n - Compute total numeric value
- Keep the smallest valid candidate
This works conceptually because it explores every possible solution and guarantees correctness.
However, the number of possible strings is:
$$26^n$$
Even for small values of n, this becomes computationally impossible.
For example:
n = 10gives roughly1.4 × 10^14possibilitiesn = 100000is unimaginably large
Thus, brute force is entirely infeasible.
Key Insight for an Optimal Solution
To obtain the lexicographically smallest string, we want the leftmost characters to be as small as possible.
Since 'a' is the smallest letter, our first instinct should be:
- Fill the string with
'a' - Distribute the remaining value strategically
Initially, an all-'a' string has value:
$$n$$
So we define:
$$remaining = k - n$$
This represents the extra numeric value we still need.
Now comes the greedy observation:
To minimize lexicographical order, we should place larger letters as far right as possible.
Why?
Because earlier characters have more impact on lexicographical ordering than later ones.
For example:
"aay"is smaller than"aya"
Even though both contain the same letters, the earlier 'a' makes the first string smaller.
So we greedily work from right to left, adding as much value as possible to each character.
Each position already contributes 1 because it starts as 'a'.
A character can gain at most:
$$26 - 1 = 25$$
extra value.
Thus, for each position from right to left:
- Add as much as possible, capped at
25 - Convert the character accordingly
- Reduce the remaining value
This greedy strategy guarantees lexicographical minimality.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(26^n) | O(n) | Enumerates all possible strings |
| Optimal Greedy | O(n) | O(n) | Greedily fills from right to left |
Algorithm Walkthrough
- Start by assuming every character is
'a'.
Since 'a' contributes 1, an all-'a' string has total value n.
2. Compute the extra value still needed.
We calculate:
$$remaining = k - n$$
This tells us how much value must be added beyond the baseline 'a' characters.
3. Create a character array of size n, initialized with 'a'.
We use an array because strings are immutable in many languages, and we will modify characters efficiently. 4. Traverse from the rightmost position to the leftmost.
We move right to left because larger letters should appear later to keep the string lexicographically smallest. 5. At each position, add as much extra value as possible.
Each character can gain at most 25 extra points.
Compute:
$$extra = \min(25, remaining)$$ 6. Update the current character.
Convert the character from 'a' to:
$$chr(ord('a') + extra)$$
Examples:
0 → 'a'1 → 'b'25 → 'z'
- Subtract the added value.
Update:
$$remaining -= extra$$ 8. Continue until all extra value has been distributed. 9. Join the character array into a string and return it.
Why it works
The key invariant is that earlier characters are always kept as small as possible.
At every step, we greedily assign the maximum possible value to the rightmost available position. Since lexicographical order prioritizes earlier characters, delaying large letters toward the end guarantees minimality.
Any alternative solution placing a larger character earlier would necessarily be lexicographically larger.
Therefore, this greedy strategy always produces the smallest valid string.
Python Solution
class Solution:
def getSmallestString(self, n: int, k: int) -> str:
chars = ['a'] * n
remaining = k - n
for i in range(n - 1, -1, -1):
if remaining == 0:
break
extra = min(25, remaining)
chars[i] = chr(ord('a') + extra)
remaining -= extra
return ''.join(chars)
The implementation begins by initializing an array of 'a' characters of length n. This gives us the smallest possible starting point while contributing a baseline value of n.
We then calculate remaining = k - n, which represents how much extra numeric value must still be distributed.
Next, we iterate from the last index toward the beginning. For each position, we greedily assign as much extra value as possible, capped at 25, because 'a' already contributes 1 and 'z' contributes 26.
The character is updated using ASCII arithmetic with ord() and chr(). Finally, once all extra value has been assigned, we join the character list into the final string.
Go Solution
func getSmallestString(n int, k int) string {
chars := make([]byte, n)
for i := 0; i < n; i++ {
chars[i] = 'a'
}
remaining := k - n
for i := n - 1; i >= 0; i-- {
if remaining == 0 {
break
}
extra := remaining
if extra > 25 {
extra = 25
}
chars[i] = byte('a' + extra)
remaining -= extra
}
return string(chars)
}
The Go implementation follows the exact same greedy strategy.
Since strings in Go are immutable, we use a []byte slice to efficiently modify characters in place. Each position starts as 'a', and we traverse from right to left, distributing extra value greedily.
Unlike Python, Go does not provide min() for integers directly, so we implement the cap manually using a conditional statement.
Integer overflow is not a concern because the constraints remain well within Go's integer limits.
Worked Examples
Example 1
Input:
n = 3, k = 27
Start with:
aaa
Baseline value:
3
Remaining:
27 - 3 = 24
| Step | Index | Remaining Before | Extra Added | Character | Current String |
|---|---|---|---|---|---|
| Initial | - | 24 | - | - | aaa |
| 1 | 2 | 24 | 24 | y |
aay |
| 2 | 1 | 0 | 0 | a |
aay |
| 3 | 0 | 0 | 0 | a |
aay |
Final answer:
"aay"
Numeric value:
1 + 1 + 25 = 27
Example 2
Input:
n = 5, k = 73
Start with:
aaaaa
Baseline:
5
Remaining:
73 - 5 = 68
| Step | Index | Remaining Before | Extra Added | Character | Current String |
|---|---|---|---|---|---|
| Initial | - | 68 | - | - | aaaaa |
| 1 | 4 | 68 | 25 | z |
aaaaz |
| 2 | 3 | 43 | 25 | z |
aaazz |
| 3 | 2 | 18 | 18 | s |
aaszz |
| 4 | 1 | 0 | 0 | a |
aaszz |
| 5 | 0 | 0 | 0 | a |
aaszz |
Final answer:
"aaszz"
Numeric value:
1 + 1 + 19 + 26 + 26 = 73
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | We traverse the string once |
| Space | O(n) | We store the character array |
The algorithm performs a single pass from right to left through n positions. Every operation inside the loop is constant time, so the total runtime is linear.
The space complexity is O(n) because we explicitly maintain an array of characters before converting it into a string.
Test Cases
class Solution:
def getSmallestString(self, n: int, k: int) -> str:
chars = ['a'] * n
remaining = k - n
for i in range(n - 1, -1, -1):
if remaining == 0:
break
extra = min(25, remaining)
chars[i] = chr(ord('a') + extra)
remaining -= extra
return ''.join(chars)
sol = Solution()
assert sol.getSmallestString(3, 27) == "aay" # Example 1
assert sol.getSmallestString(5, 73) == "aaszz" # Example 2
assert sol.getSmallestString(1, 1) == "a" # Smallest input
assert sol.getSmallestString(1, 26) == "z" # Single max character
assert sol.getSmallestString(5, 5) == "aaaaa" # Minimum possible k
assert sol.getSmallestString(3, 78) == "zzz" # Maximum possible k
assert sol.getSmallestString(2, 27) == "az" # One z at the end
assert sol.getSmallestString(2, 28) == "bz" # Middle distribution
assert sol.getSmallestString(4, 30) == "aabz" # Mixed letters
assert sol.getSmallestString(6, 31) == "aaaaaz" # Mostly a's
assert len(sol.getSmallestString(100000, 100000)) == 100000 # Large n
assert len(sol.getSmallestString(100000, 2600000)) == 100000 # Large max case
| Test | Why |
|---|---|
(3, 27) |
Validates Example 1 |
(5, 73) |
Validates Example 2 |
(1, 1) |
Smallest legal input |
(1, 26) |
Single maximum-value character |
(5, 5) |
Minimum possible k |
(3, 78) |
Maximum possible k |
(2, 27) |
Ensures rightmost greedy placement |
(2, 28) |
Tests partial distribution |
(4, 30) |
Verifies mixed character composition |
(6, 31) |
Tests one large trailing character |
(100000, 100000) |
Large-scale minimum case |
(100000, 2600000) |
Large-scale maximum case |
Edge Cases
Edge Case 1: Minimum Possible Value
When k = n, every character must be 'a'.
For example:
n = 5, k = 5
A buggy implementation might still try modifying characters unnecessarily. Our implementation correctly computes:
remaining = 0
Since no extra value is needed, the loop exits immediately and returns "aaaaa".
Edge Case 2: Maximum Possible Value
When:
k = 26 * n
every character must become 'z'.
For example:
n = 3, k = 78
The algorithm repeatedly adds 25 extra value to every position, eventually producing:
"zzz"
The greedy logic naturally handles this without any special-case code.
Edge Case 3: Partial Fill in the Middle
Sometimes the answer contains many 'a' characters, one intermediate character, and several 'z' characters.
Example:
n = 5, k = 52
Start with:
aaaaa
Remaining:
47
We fill from the right:
- last character →
z(+25) - second-last →
w(+22)
Result:
aaawz
This case is easy to get wrong if characters are filled left to right, because that would produce a lexicographically larger string. By filling right to left, our implementation guarantees the smallest possible result.