Problem overview
maxNumber(nums1, nums2, k) receives two IntArray values whose elements are digits from 0 to 9, plus a length k with 1 <= k <= nums1.size + nums2.size. It must build the largest possible number of exactly k digits, using digits from both arrays, and return its digits as an IntArray. Digits taken from the same array must keep their original relative order, but digits from different arrays can be interleaved in any way.
For nums1 = [3, 4, 6, 5], nums2 = [9, 1, 2, 5, 8, 3] and k = 5 the answer is [9, 8, 6, 5, 3]: the digits 9, 8 and 3 come from nums2, and the 6 and 5 from nums1. For nums1 = [6, 7], nums2 = [6, 0, 4] and k = 5 all digits must be used, and the best order is [6, 7, 6, 0, 4].
Since all candidates have the same length, "larger number" means larger as a digit sequence compared from the left: the first position where two candidates differ decides.
Approach: split, shrink, merge, compare
The answer takes some number i of digits from nums1 and the remaining k - i from nums2. Nobody knows the right i in advance, so the solution tries every split from 0 to k, skipping those that ask for more digits than an array has. For each split, the problem breaks into three smaller ones:
- Best
idigits ofnums1. Choose the largest number of exactlyidigits that keeps their order. Do the same fork - idigits ofnums2. - Merge the two results into the largest possible interleaving.
- Compare this candidate against the best seen so far and keep the larger.
The best subsequence uses a stack, in the same spirit as removing digits to make a number as small as possible, turned the other way around. Read the digits left to right. If the digit on top of the stack is smaller than the new digit, throwing the old one away and letting the new one move left makes the number bigger, so pop it. The only limit is that you must still be able to end with exactly size digits: a pop is allowed only if the digits already on the stack, plus the current digit and all digits still to come, add up to more than size. A digit that arrives when the stack is already full is dropped.
For [9, 1, 2, 5, 8, 3] and size = 3: 9 is pushed; 1 is pushed; 2 pops 1; 5 pops 2; 8 pops 5; 3 doesn't beat the 8, and the stack has room, so it is pushed. The result is [9, 8, 3].
The merge repeatedly takes the first digit of whichever remaining sequence is greater. When the next digits are equal, looking at them alone isn't enough, so the comparison goes further and compares the whole remaining tails. For [6, 7] and [6, 0, 4] both start with 6, but the first tail continues with 7 and the second with 0, so the 6 should come from the first array. Taking it from the second would produce [6, 6, 7, 0, 4], which is smaller than [6, 7, 6, 0, 4].
For the first example, the splits give these candidates:
| digits from nums1 (i) | from nums2 | best of each | merged |
|---|---|---|---|
| 0 | 5 | [] and [9, 2, 5, 8, 3] | 92583 |
| 1 | 4 | [6] and [9, 5, 8, 3] | 96583 |
| 2 | 3 | [6, 5] and [9, 8, 3] | 98653 |
| 3 | 2 | [4, 6, 5] and [9, 8] | 98465 |
| 4 | 1 | [3, 4, 6, 5] and [9] | 93465 |
The split i = 5 is skipped, because nums1 has only four digits. The largest candidate is 98653, the expected answer.
Step by step
- Start with an empty best.
var best = IntArray(0)loses to any candidate, so the first candidate replaces it. - Try every split.
for (i in 0..k)uses an inclusive range:idigits come fromnums1,k - ifromnums2. - Skip impossible splits. Neither array can give more digits than it has.
- Shrink each array.
maxSubsequencereturns the bestidigits ofnums1asfirstand the bestk - idigits ofnums2assecond. - Merge them into
candidate, ak-digit array. - Keep the larger.
isGreater(candidate, 0, best, 0)compares both from index 0, and a bigger candidate replacesbest. - Return
bestafter the loop. - Write
maxSubsequence(nums, size). It's aprivatehelper that returns the largestsize-digit subsequence ofnums. - Allocate the stack.
IntArray(size)is enough, because the stack never holds more thansizedigits.counttracks how many are in use, so the top isstack[count - 1]. - Visit each index.
nums.indicesprovides the position, which is needed to know how many digits are left. - Pop while it helps. Pop when the stack isn't empty, its top is smaller than
nums[i], andcount + nums.size - i > size, meaning enough digits would still remain. - Popping is just
count--. The old digit stays in the array and the next push overwrites it. - Push only if there is room:
count < size. - Store the digit at
stack[count]and increasecount. - Return the stack.
- Write
merge(a, b). Anotherprivatehelper, which interleaves two arrays into the largest number that uses all digits. - Allocate
resultwitha.size + b.sizeslots. - Keep two cursors.
iwalksaandjwalksb. - Loop until both are used up:
i < a.size || j < b.size. Each turn takes exactly one digit. - Compare the tails.
isGreater(a, i, b, j)says whether what is left ofabeats what is left ofb. - If so, take from
a: storea[i]atresult[i + j], sincei + jdigits are already placed, and advancei. - Otherwise take from
b. When the tails are equal, either choice gives the same number. - Return
result. - Write
isGreater(a, aStart, b, bStart). It compares the tail ofafromaStartwith the tail ofbfrombStart. - Copy the starts into
vars namediandj, because parameters are read-only in Kotlin. - Skip equal digits. The loop runs while both tails have digits and the digits at
iandjare equal. - Advance both cursors on each equal pair.
- Decide.
awins ifbran out first, or ifastill has a digit that is larger thanb[j].
The solution in Kotlin
class Solution {
fun maxNumber(nums1: IntArray, nums2: IntArray, k: Int): IntArray {
var best = IntArray(0)
for (i in 0..k) {
if (i <= nums1.size && k - i <= nums2.size) {
val first = maxSubsequence(nums1, i)
val second = maxSubsequence(nums2, k - i)
val candidate = merge(first, second)
if (isGreater(candidate, 0, best, 0)) {
best = candidate
}
}
}
return best
}
private fun maxSubsequence(nums: IntArray, size: Int): IntArray {
val stack = IntArray(size)
var count = 0
for (i in nums.indices) {
while (count > 0 && stack[count - 1] < nums[i] && count + nums.size - i > size) {
count--
}
if (count < size) {
stack[count] = nums[i]
count++
}
}
return stack
}
private fun merge(a: IntArray, b: IntArray): IntArray {
val result = IntArray(a.size + b.size)
var i = 0
var j = 0
while (i < a.size || j < b.size) {
if (isGreater(a, i, b, j)) {
result[i + j] = a[i]
i++
} else {
result[i + j] = b[j]
j++
}
}
return result
}
private fun isGreater(a: IntArray, aStart: Int, b: IntArray, bStart: Int): Boolean {
var i = aStart
var j = bStart
while (i < a.size && j < b.size && a[i] == b[j]) {
i++
j++
}
return j == b.size || (i < a.size && a[i] > b[j])
}
}
Kotlin details worth a look:
0..kincludesk. To exclude the end, use0 until k. The solution needs the inclusive form because taking allkdigits fromnums1is a valid split.IntArray(size)starts filled with zeros. That matters inmaxSubsequence. Without thecount + nums.size - i > sizeguard, the stack can finish short: for[1, 9, 2]andsize = 3the code would pop the 1 and return[9, 2, 0], with a zero that came from the array's initial value and not from the input.- Short-circuit operators. In
isGreater,j == b.size || (i < a.size && a[i] > b[j])only readsa[i]after checkingi < a.sizeand only readsb[j]afterj == b.sizewas found false. Reordering the conditions could cause an index error. - Parameters are
val.aStartcan't be incremented, so the helper copies it intovar i. - Returning an array hands over the same array that was built, with no copy. The function replaces
bestby reference when a candidate wins. privatehelpers are called frommaxNumberby name, because they sit in the same class.
Complexity
Time. There are at most k + 1 splits. For each one, maxSubsequence makes one pass over each array, so O(m + n) where m and n are the array sizes. Each merge produces k digits, and each of those turns can call isGreater, which may walk a long run of equal digits: up to O(k) per call, so a merge costs O(k²) in the worst case. Comparing the candidate with best costs O(k). The total is therefore bounded by O(k × (m + n + k²)).
Space: O(k). At any time the function holds the two stacks, the merged candidate and best, each of at most k digits.
Edge cases and common mistakes
kequalsm + n. Only one split is possible, with every digit used. The merge alone decides the answer, as in[6, 7]and[6, 0, 4].- One side contributes nothing.
i = 0ori = kmeans one array gives no digits. The empty array fromIntArray(0)merges fine. - Ties in the merge. Comparing only the next digit and then choosing
bon a tie gives a smaller number for[6, 7]and[6, 0, 4]. Compare the whole tails. - Skipping the "enough digits left" guard. The stack could end with fewer than
sizedigits, and zeros would fill the gap. - Turning digits into a number. With up to 1000 digits, the value doesn't fit in an
Intor aLong. Compare digit by digit, asisGreaterdoes. - Trying impossible splits. Without the size check,
maxSubsequencewould be asked for more digits than the array holds.
Testing
[3, 4, 6, 5],[9, 1, 2, 5, 8, 3],k = 5gives[9, 8, 6, 5, 3].[6, 7],[6, 0, 4],k = 5gives[6, 7, 6, 0, 4].[3, 9],[8, 9],k = 3gives[9, 8, 9].
