LeetCode 321. 拼接最大数
题目描述


题意分析
从两个数字数组中一共选出
k个数字,按某种方式交错拼成最大的数,返回长度为k的数字数组。同一来源数组中被选数字的相对顺序必须保持,但可以跳过不选的数字;两个来源之间没有固定的先后关系。因此不能把所有数字合在一起排序,也不能要求每个来源选出的部分连续。所有候选长度都为
k,比较大小等价于比较字典序:从左到右找到第一处不同,那里较大的数字就决定整个候选更大。
解法:枚举分配 + 单调栈 + 后缀贪心归并
核心思路
[!blue]
先枚举两个来源各取多少个。设第一数组取
take1个,第二数组就取k - take1个。为保证两边都取够且不超长,take1的范围是max(0, k - nums2.length)到min(k, nums1.length),这样覆盖全部合法分配。固定分配后,每个来源可以独立选择该长度的最大子序列。理由是:固定另一来源和交错位置不变,把某一侧换成字典序更大的同长度子序列,会在该侧第一次不同的输出位置使总结果更大,不会影响此前字符。因此较小的同长度子序列不可能比最大子序列更有利。
从长度为
N的数组选length个数,相当于允许删除drop = N - length个。用栈保存已选前缀,遇到更大的新数字时,只要还可删除,就弹出较小栈顶,让更大数字靠前。这个替换改善了第一处不同的位置,而删除额度保证仍能留够目标长度;数字相等时保留较早的那个即可。栈未满时加入当前数字,已满时跳过它,并同样消耗一个删除额度。弹出旧值和跳过新值都是删除,必须统一计数。额度用完后,即使后面出现更大数字,也不能继续弹出,否则可能凑不够长度;因此最终子序列不一定整体单调。
两条最大子序列还需要合并。若当前数字不同,先取较大者;若相同,必须比较完整的剩余后缀,直到第一处不同,选择后续字典序更大的一侧。前面的相同数字暂时无法拉开差距,提前推进较大的后缀,才能更早把它较大的后续数字放入结果。只看当前位并任意打破平局,可能让较小的后继挡在更大后继之前。
后缀比较时,若一侧已经耗尽,另一侧非空后缀更大;完全相同则任选一侧。每次只取所选后缀的第一个数字,然后重新比较新的两个后缀,保证两侧原有顺序不变。最后把不同分配得到的候选互相比字典序,保留最大结果。
解题步骤
- 计算第一数组可取数量的合法上下界,逐个枚举。
- 对每种分配,分别用删除额度控制的栈选出两边定长最大子序列。
- 用两个下标指向子序列开头,每次比较剩余后缀,取较大后缀的首位加入候选。
- 某一侧耗尽后,继续取另一侧,直到候选长度为
k。- 将候选与历史最好结果按字典序比较,枚举全部分配后返回最优数组。
代码实现
class Solution {
public int[] maxNumber(int[] nums1, int[] nums2, int k) {
// 取数分配必须让两侧都不超过自身长度,枚举覆盖全部合法分配。
int lower = Math.max(0, k - nums2.length);
int upper = Math.min(k, nums1.length);
int[] best = new int[k];
for (int take1 = lower; take1 <= upper; take1++) {
int[] part1 = maxSubsequence(nums1, take1);
int[] part2 = maxSubsequence(nums2, k - take1);
int[] candidate = merge(part1, part2);
if (greater(candidate, 0, best, 0)) {
best = candidate;
}
}
return best;
}
private int[] maxSubsequence(int[] nums, int length) {
int[] stack = new int[length];
int top = 0;
// 删除额度保证最终能留够目标长度,额度耗尽后不能再弹出。
int drop = nums.length - length;
for (int num : nums) {
while (top > 0 && drop > 0 && stack[top - 1] < num) {
top--;
drop--;
}
// 栈满后跳过当前值也消耗一次删除额度,不能把额度留给后面重复删除。
if (top < length) {
stack[top++] = num;
} else {
drop--;
}
}
return stack;
}
private int[] merge(int[] nums1, int[] nums2) {
int[] merged = new int[nums1.length + nums2.length];
int index1 = 0;
int index2 = 0;
for (int i = 0; i < merged.length; i++) {
// 首位相同时继续比较整个剩余后缀,选择字典序更大的一侧。
if (greater(nums1, index1, nums2, index2)) {
merged[i] = nums1[index1++];
} else {
merged[i] = nums2[index2++];
}
}
return merged;
}
// 完全相等也返回真,只决定相同候选的取用顺序,不影响最终最大值。
private boolean greater(int[] nums1, int index1, int[] nums2, int index2) {
while (index1 < nums1.length && index2 < nums2.length && nums1[index1] == nums2[index2]) {
index1++;
index2++;
}
return index2 == nums2.length || (index1 < nums1.length && nums1[index1] > nums2[index2]);
}
}
func maxNumber(nums1 []int, nums2 []int, k int) []int {
// 取数分配必须让两侧都不超过自身长度,枚举覆盖全部合法分配。
lower := 0
if k-len(nums2) > lower {
lower = k - len(nums2)
}
upper := k
if len(nums1) < upper {
upper = len(nums1)
}
best := make([]int, k)
for take1 := lower; take1 <= upper; take1++ {
part1 := maxSubsequence(nums1, take1)
part2 := maxSubsequence(nums2, k-take1)
candidate := mergeMax(part1, part2)
if greaterSeq(candidate, 0, best, 0) {
best = candidate
}
}
return best
}
func maxSubsequence(nums []int, length int) []int {
stack := make([]int, 0, length)
// 删除额度保证最终能留够目标长度,额度耗尽后不能再弹出。
drop := len(nums) - length
for _, num := range nums {
for len(stack) > 0 && drop > 0 &&
stack[len(stack)-1] < num {
stack = stack[:len(stack)-1]
drop--
}
// 栈满后跳过当前值也消耗一次删除额度,不能把额度留给后面重复删除。
if len(stack) < length {
stack = append(stack, num)
} else {
drop--
}
}
return stack
}
func mergeMax(nums1 []int, nums2 []int) []int {
merged := make([]int, len(nums1)+len(nums2))
index1, index2 := 0, 0
for i := range merged {
// 首位相同时继续比较整个剩余后缀,选择字典序更大的一侧。
if greaterSeq(nums1, index1, nums2, index2) {
merged[i] = nums1[index1]
index1++
} else {
merged[i] = nums2[index2]
index2++
}
}
return merged
}
// 完全相等也返回真,只决定相同候选的取用顺序,不影响最终最大值。
func greaterSeq(nums1 []int, index1 int, nums2 []int, index2 int) bool {
for index1 < len(nums1) && index2 < len(nums2) &&
nums1[index1] == nums2[index2] {
index1++
index2++
}
return index2 == len(nums2) ||
(index1 < len(nums1) && nums1[index1] > nums2[index2])
}
复杂度分析
设两个输入长度为
m、n,合法分配数量为A,且A <= k + 1。
- 时间复杂度:$O(A(m + n + k^2))$。每种分配的两次子序列选择为 $O(m + n)$,因为每个数字最多入栈、出栈各一次;归并
k位时,每次比较剩余后缀最坏检查 $O(k)$ 个数字,因此归并最坏为 $O(k^2)$。- 空间复杂度:$O(k)$,两条子序列总长为
k,候选与历史最优也都长k,只同时保存常数份候选,不保留所有分配结果。
关键点总结
[!green]
- 枚举数量分配解决跨来源选择,单边最大子序列解决各自保序,后缀归并解决交错顺序。
- 删除额度同时约束弹栈和跳过,既让较大数字靠前,又保证最终长度。
- 相等首位不能决定归并方向,比较完整后缀后也只消费一位。
易错点总结
[!yellow]
- 无条件枚举
0..k,会出现某一来源需要取出超过自身长度的数字;应先限制分配范围。- 为了更大的数字无限弹栈,会丢掉必须保留的位置,最终不足指定长度。
- 栈满后跳过当前数却不扣删除额度,会让后面继续使用已经花掉的额度。
- 首位相等时固定选一侧,会忽略更靠后的首次差异,得到较小的候选。
- 将已经耗尽的后缀判为更大,会在归并中访问越界;非空后缀应优先。
- 比较过程中找到差异后,一次性复制所选一侧的全部剩余数字,会错过与另一侧重新交错的机会。
- 只尝试某一种取数比例,不能覆盖最优答案可能采用的另一种分配。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 402. 移掉 K 位数字 | 中等 | 同样从序列中删掉不利元素得到固定长度最优子序列,本题取最大并合并两个来源。 |
| 1754. 构造字典序最大的合并字符串 | 中等 | 合并时同样需要比较剩余后缀而非只比较当前字符,否则相同首字符会导致选错。 |