LeetCode 1946 - Largest Number After Mutating Substring
This problem asks us to construct the lexicographically largest possible numeric string after optionally applying a single transformation operation on one contiguous substring of the input string num.
Difficulty: 🟡 Medium
Topics: Array, String, Greedy
Solution
Problem Understanding
This problem asks us to construct the lexicographically largest possible numeric string after optionally applying a single transformation operation on one contiguous substring of the input string num. Each digit in num can be transformed independently using a fixed mapping array change, where change[d] defines the replacement digit for digit d.
We are allowed to choose at most one substring and apply the transformation to every digit inside that substring exactly once. Digits outside the chosen substring remain unchanged. The goal is to choose both the substring boundaries and whether to apply the operation at all such that the resulting string represents the largest possible integer in lexicographical order.
The input constraints indicate that num can be up to length 100,000, which strongly suggests that any quadratic or worse solution will not be feasible. The change array has fixed size 10, meaning digit transformations are constant-time lookups.
Several edge cases are important. If no digit transformation improves the number, the correct answer is the original string. If multiple substrings could improve the result, we must ensure that we choose the one that maximizes the final value under the constraint that only one contiguous segment can be mutated. Another subtle case occurs when transformations initially increase digit value but later decrease it, which defines the boundary of the valid substring.
Approaches
The brute-force approach considers every possible substring of num. For each substring, we simulate applying the digit transformation and construct a new string. We then compare all results and choose the maximum lexicographically. This works because it exhaustively checks all valid operations, ensuring correctness. However, it is far too slow because there are O(n^2) substrings, and each transformation costs O(n), leading to O(n^3) in a naive implementation or at best O(n^2), both of which are too large for n = 100,000.
The key insight is that we do not need to try all substrings. Instead, we observe that once we decide to start mutating at a position, we should continue mutating as long as the transformation does not reduce the digit value. The moment the transformation becomes worse than the original digit, we must stop, because extending further would only make the number smaller, and we are allowed only one contiguous segment.
Thus, the optimal solution is greedy: we find the first position where applying change improves the digit, start mutation there, and continue while it remains non-decreasing. This ensures maximal lexicographical gain.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n³) | O(n) | Try all substrings and simulate transformations |
| Optimal Greedy | O(n) | O(n) | Single pass to find best valid mutation window |
Algorithm Walkthrough
The optimal algorithm is based on scanning the string once and carefully deciding where the mutation segment starts and ends.
- Convert the string
numinto a list of characters so we can modify it efficiently. This avoids repeated string concatenation, which would be costly in Python. - Initialize a boolean flag
startedto track whether we have begun applying the mutation. Initially, we have not started because we have not found a beneficial starting point. - Iterate through each index
iof the string from left to right. At each position, compare the original digitnum[i]with its transformed valuechange[num[i]]. - If we have not started a mutation and the transformed digit is greater than the original digit, we begin the mutation at this index. From this point onward, we apply the transformation to every digit until we decide to stop.
- Once mutation has started, we continue applying it as long as
change[num[i]]is greater than or equal tonum[i]. This ensures we do not reduce the value of the number. - If we encounter a digit where
change[num[i]]is strictly less thannum[i], we stop the mutation immediately. From this point forward, all remaining digits stay unchanged, because only one contiguous substring is allowed. - After processing the entire string, convert the list of characters back into a string and return it.
Why it works: The algorithm enforces a single contiguous segment that is maximally beneficial. The greedy choice ensures we start as early as possible for maximum lexicographical gain, and we stop exactly when continuation would harm the result. This creates an optimal window that cannot be improved by shifting boundaries.
Python Solution
from typing import List
class Solution:
def maximumNumber(self, num: str, change: List[int]) -> str:
digits = list(num)
started = False
for i in range(len(digits)):
d = ord(digits[i]) - ord('0')
transformed = change[d]
if not started:
if transformed > d:
started = True
digits[i] = str(transformed)
else:
if transformed >= d:
digits[i] = str(transformed)
else:
break
return "".join(digits)
The implementation directly follows the greedy strategy. We iterate once over the string, decide when to start mutation, and apply transformation while it remains non-decreasing. The started flag ensures we maintain a single contiguous segment constraint.
Go Solution
func maximumNumber(num string, change []int) string {
runes := []byte(num)
started := false
for i := 0; i < len(runes); i++ {
d := int(runes[i] - '0')
transformed := change[d]
if !started {
if transformed > d {
started = true
runes[i] = byte(transformed + '0')
}
} else {
if transformed >= d {
runes[i] = byte(transformed + '0')
} else {
break
}
}
}
return string(runes)
}
In Go, we use a byte slice instead of a string because strings are immutable. Converting to a byte slice allows in-place modification. The logic mirrors the Python solution exactly, with explicit integer-to-byte conversion for digit handling.
Worked Examples
Example 1
Input: num = "132", change = [9,8,5,0,3,6,4,2,6,8]
We process step by step:
At index 0, digit is 1, transformed is 8. Since 8 > 1, we start mutation here.
At index 0:
| i | digit | change[d] | action |
|---|---|---|---|
| 0 | 1 | 8 | start, replace with 8 |
At index 1:
| i | digit | change[d] | action |
|---|---|---|---|
| 1 | 3 | 0 | stop because 0 < 3 |
Final string becomes "832".
Example 2
Input: num = "021", change = [9,4,3,5,7,2,1,9,0,6]
At index 0, digit 0 maps to 9, which is beneficial, so we start immediately.
At index 0:
| i | digit | change[d] | action |
|---|---|---|---|
| 0 | 0 | 9 | start, replace |
At index 1:
| i | digit | change[d] | action |
|---|---|---|---|
| 1 | 2 | 3 | continue |
At index 2:
| i | digit | change[d] | action |
|---|---|---|---|
| 2 | 1 | 4 | continue |
Final result is "934".
Example 3
Input: num = "5", change = [1,4,7,5,3,2,5,6,9,4]
At index 0, digit 5 maps to 2, which is worse. We never start mutation.
Result remains "5".
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Single pass through the string with constant-time digit mapping |
| Space | O(n) | Output is stored in a mutable character array |
The time complexity is linear because each character is visited at most once, and each operation inside the loop is O(1). Space complexity is linear due to conversion into a mutable structure for in-place updates.
Test Cases
assert Solution().maximumNumber("132", [9,8,5,0,3,6,4,2,6,8]) == "832" # basic improvement case
assert Solution().maximumNumber("021", [9,4,3,5,7,2,1,9,0,6]) == "934" # full substring mutation
assert Solution().maximumNumber("5", [1,4,7,5,3,2,5,6,9,4]) == "5" # no beneficial change
assert Solution().maximumNumber("10", [0,1,2,3,4,5,6,7,8,9]) == "20" # single digit start only
assert Solution().maximumNumber("333", [3,3,3,3,3,3,3,3,3,3]) == "333" # all equal mapping
assert Solution().maximumNumber("13", [1,2,3,4,5,6,7,8,9,0]) == "23" # partial improvement
assert Solution().maximumNumber("987654", [9,9,9,9,9,9,9,9,9,9]) == "987654" # no change improves
| Test | Why |
|---|---|
| "132" case | verifies standard greedy start/stop behavior |
| "021" case | verifies full substring mutation |
| "5" case | verifies no mutation case |
| "10" case | tests boundary where only first digit triggers |
| "333" case | tests equality handling |
| "13" case | tests partial improvement window |
| "987654" case | tests no-op when mapping is non-improving |
Edge Cases
One important edge case is when no digit in the string can be improved by its mapping. In this case, the algorithm must never start the mutation window, and the original string must be returned unchanged. The started flag ensures this because it is only set when a strict improvement is found.
Another edge case occurs when the improvement starts at the first character and continues through the entire string. This happens when change[d] >= d for all digits after the first beneficial start. The algorithm handles this naturally by never encountering a break condition.
A third edge case involves early improvement followed by a sudden decrease in mapped value. This is critical because it defines the correct stopping boundary. The algorithm explicitly breaks at the first position where change[d] < d after mutation has started, ensuring that we do not violate the single-substring constraint or reduce the lexicographical value of the result.