LeetCode 面试题 17.08. 马戏团人塔
题目描述

题意分析
每个人的身高与体重由两个数组的相同下标给出。叠塔时,上方的人必须同时更矮、更轻,求最多能选多少人;相同身高或相同体重都不能上下接续。
解法:排序 + 最长递增子序列
核心思路
[!blue]
从塔顶到塔底看,人选的身高和体重都要严格递增。先把每个人的两个属性配成一对,按身高升序排列,再从排序后的体重序列中寻找最长严格递增子序列,就能消去身高这一维的查找。
但身高升序只保证不下降,不能让同高的人被同时选中。因此同身高时必须按体重降序排列:这一组内部体重不会上升,任何严格递增子序列都最多选一个人。这样选出的序列必然身高、体重都严格递增;反过来,任何合法人塔的身高本就严格递增,排序后仍能按原来的上下顺序选出,所以这个转换不会丢失最优解。
对体重维护
tails[k],表示已扫描人员中,长度为k + 1的严格递增子序列能够取得的最小末尾体重。同样的长度,末尾越轻,未来越容易接上更重的人,因此只保留最小结尾就足够。
tails严格递增:一个更长的递增序列去掉末尾后,所得较短序列的末尾一定更小,而较短长度的最优尾值只会更小。有了这个顺序,就可以二分查找当前体重w应放的位置,即第一个tails[pos] >= w的下标。若
pos > 0,前一项tails[pos - 1] < w,可以在那条长度为pos的序列后接上当前人,得到长度为pos + 1、末尾为w的新候选。用w替换原尾值会改善或保持同长度的结尾;若pos已越过所有有效尾值,就能把当前最长序列再延长一层。相同体重只会替换,不会增加长度。最后返回
tails的有效长度。各位置记录的是不同长度下的最优结尾,它们不一定来自同一条实际人选序列,但不影响长度计算。
解题步骤
- 配对同一个人的身高与体重,按身高升序、同高体重降序排序。
- 按顺序处理每个人的体重,在
tails的有效区间内二分第一个不小于它的值。- 找到位置就替换尾值;若位置在有效区间末尾,就追加新尾值并增加长度。
- 返回最终有效长度;输入为空时没有候选,结果为零。
代码实现
// 身高升序、同身高体重降序排序后,在体重上求严格递增的最长子序列。
class Solution {
public int bestSeqAtIndex(int[] height, int[] weight) {
int n = height.length;
int[][] people = new int[n][2];
for (int i = 0; i < n; i++) {
people[i][0] = height[i];
people[i][1] = weight[i];
}
Arrays.sort(
people,
(a, b) -> {
if (a[0] != b[0]) {
return a[0] - b[0];
}
// 同身高按体重降序,严格递增子序列不会选同组两人。
return b[1] - a[1];
});
int[] tails = new int[n];
int size = 0;
for (int[] person : people) {
int w = person[1];
int lo = 0;
// 只搜索已使用的尾值范围,不读取剩余空槽。
int hi = size;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (tails[mid] < w) {
lo = mid + 1;
} else {
hi = mid;
}
}
// 降低同长度的末尾,或在末尾扩展新的长度。
tails[lo] = w;
if (lo == size) {
size++;
}
}
return size;
}
}
import "sort"
// 身高升序、同身高体重降序排序后,在体重上求严格递增的最长子序列。
func bestSeqAtIndex(height []int, weight []int) int {
people := make([][2]int, len(height))
for i := 0; i < len(height); i++ {
people[i] = [2]int{
height[i],
weight[i],
}
}
sort.Slice(people, func(i, j int) bool {
if people[i][0] != people[j][0] {
return people[i][0] < people[j][0]
}
// 同身高按体重降序,严格递增子序列不会选同组两人。
return people[i][1] > people[j][1]
})
tails := make([]int, 0, len(people))
for _, p := range people {
w := p[1]
// 只搜索已使用的尾值范围。
lo, hi := 0, len(tails)
for lo < hi {
mid := (lo + hi) / 2
if tails[mid] < w {
lo = mid + 1
} else {
hi = mid
}
}
// 越过全部已有尾值才能增加长度,否则只替换。
if lo == len(tails) {
tails = append(tails, w)
} else {
tails[lo] = w
}
}
return len(tails)
}
复杂度分析
- 时间复杂度:$O(n\log(n+1))$,其中
n为人数。排序和对每人体重进行二分均在这个上界内。- 空间复杂度:$O(n)$,用于配对数组与最小结尾表;原身高、体重数组不被修改。
关键点总结
[!green]
- 身高升序消去一维,同高体重降序阻止非法的同高接续。
- 同长度只保留更小的末尾,因为它给后续留下的选择不会更少。
- 求严格递增序列,应找第一个大于等于当前体重的位置。
易错点总结
[!yellow]
- 同高时体重也升序,会把同高的多个人错误地选进人塔。
- 二分找第一个严格大于当前体重的位置,会让相同体重增加长度。
- 身高、体重分别独立排序,会破坏每个人的属性对应关系。
tails不是最终人选名单,不能直接把表内各项当作同一条真实人塔输出。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 354. 俄罗斯套娃信封问题 | 困难 | 身高与体重对应信封的两维,等高时体重降序后做严格LIS,避免把同高者同时选入。 |
| 面试题 08.13. 堆箱子 | 困难 | 箱子题有三维严格限制且累加高度,本题只有两维并最大化人数。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!