Swift 实战:用最长递增子序列算法解“俄罗斯套娃信封”问题(LeetCode 354)
文章目录
- 摘要
- 描述
- 解决方案
- 解析问题和答案
- 细节解释
- 示例和结果
- 时间复杂度
- 空间复杂度
- 总结
摘要
这道题表面是“套娃游戏”,实际上是一个二维排序 + 最长递增子序列(LIS)的经典组合题。我们要在一堆信封中,找出能一层套一层的最大数量。本文会从零开始分析题目、拆解解法、实现一个 Swift 可运行 Demo,并结合实际场景聊聊为什么这种思路很常见。看完之后,你不仅能秒杀这题,还能举一反三,处理各种二维排序 + LIS 的场景。
描述
题目给我们一组信封,每个信封用 [w, h]
表示宽度和高度。要求在不旋转信封的前提下,找出能层层套入的最大信封数量。
规则:
- 信封 A 可以套进信封 B,当且仅当
wA < wB && hA < hB
。 - 不能旋转信封(也就是不能交换宽高)。
- 要求返回最多能套多少层。
示例 1:
输入: [[5,4],[6,4],[6,7],[2,3]]
输出: 3
解释: [2,3] -> [5,4] -> [6,7]
示例 2:
输入: [[1,1],[1,1],[1,1]]
输出: 1
数据范围:
- 1 <= envelopes.count <= 10^5
- 1 <= wi, hi <= 10^5
解决方案
这题的高效解法是:
-
排序处理宽度
- 按
width
升序排序。 - 如果宽度相同,按
height
降序排序(防止宽相等但高递增的情况被误当成可套娃)。
- 按
-
在高度序列上找 LIS
- 排序后,问题变成了在“高度”序列中找最长严格递增子序列。
- 高度序列的 LIS 长度,就是最大套娃数量。
-
二分优化 LIS
- 常规 LIS 是 O(n²),但这里 n 可达 10^5,需要用二分法把复杂度降到 O(n log n)。
这个套路其实在很多二维比较的题目里都适用,比如“安排会议”、“选拔球员”等等。
解析问题和答案
下面是完整可运行的 Swift 代码。为了方便跑样例,我在同一个文件里加了测试函数。
import Foundationstruct RussianDollEnvelopes {func maxEnvelopes(_ envelopes: [[Int]]) -> Int {guard !envelopes.isEmpty else { return 0 }// 1. 按宽升序,高降序排序let sortedEnvelopes = envelopes.sorted { a, b inif a[0] == b[0] {return a[1] > b[1] // 宽相等时,高度降序} else {return a[0] < b[0] // 否则宽升序}}// 2. 提取高度序列let heights = sortedEnvelopes.map { $0[1] }// 3. 在 heights 上找 LIS(严格递增)return lengthOfLIS(heights)}// 二分优化的 LISprivate func lengthOfLIS(_ nums: [Int]) -> Int {var dp = [Int]()for num in nums {// 找到第一个 >= num 的位置var left = 0var right = dp.countwhile left < right {let mid = (left + right) / 2if dp[mid] < num {left = mid + 1} else {right = mid}}if left < dp.count {dp[left] = num} else {dp.append(num)}}return dp.count}
}
细节解释
-
为什么宽相等时要按高降序?
假设有[3,3]
和[3,4]
,如果按高升序,LIS 会错误地认为[3,3] -> [3,4]
可套娃,但宽相等所以不行。降序能破坏这种错误递增。 -
二分法找位置
dp
数组是“当前长度的递增子序列的最小尾值”,二分查找能 O(log n) 定位替换位置。 -
时间复杂度
- 排序 O(n log n)
- LIS O(n log n)
- 总体 O(n log n),n = envelopes.count
-
空间复杂度
- 排序需要 O(log n) 栈空间
dp
最多长度为 n,O(n) 空间
示例和结果
func testRussianDollEnvelopes() {let solver = RussianDollEnvelopes()let case1 = [[5,4],[6,4],[6,7],[2,3]]print("Case 1 Result:", solver.maxEnvelopes(case1)) // 3let case2 = [[1,1],[1,1],[1,1]]print("Case 2 Result:", solver.maxEnvelopes(case2)) // 1let case3 = [[4,5],[4,6],[6,7],[2,3],[1,1]]print("Case 3 Result:", solver.maxEnvelopes(case3)) // 4let case4 = [[2,100],[3,200],[4,300],[5,500],[5,400],[5,250],[6,370],[6,360],[7,380]]print("Case 4 Result:", solver.maxEnvelopes(case4)) // 5
}testRussianDollEnvelopes()
输出示例:
Case 1 Result: 3
Case 2 Result: 1
Case 3 Result: 4
Case 4 Result: 5
这几组用例覆盖了常规、重复、边界和复杂排序的情况。
时间复杂度
- 排序:O(n log n)
- LIS(二分法):O(n log n)
- 总体:O(n log n)
可以轻松处理 n=10^5 级别的数据。
空间复杂度
- 额外空间主要是存放
dp
数组和排序临时空间:O(n)。 - 没有额外递归开销,空间使用很稳定。
总结
这题其实是“二维 LIS”的经典模板:
- 先排序破坏非法情况:宽升序 + 宽相等高降序。
- 在一维上做 LIS:把问题降维到 O(n log n) 的 LIS。
- 实现细节很关键:高降序是防错的核心,二分法是提速的关键。
现实中,这种“两个维度都要递增”的需求很常见,比如信封套娃、球员身高体重选拔、会议时间安排等。掌握这个套路,你可以应对各种类似的二维排序 + LIS 问题。