354. 俄罗斯套娃信封问题

考点

  • LIS
  • 排序优化

思路

一、问题抽象与形式化描述

给定 \(n\) 个信封,每个信封由一对整数表示: \[ (w_i, h_i) \] 其中 \(w_i\) 为宽度,\(h_i\) 为高度。

若存在一组信封序列 \[ (w_{i_1}, h_{i_1}), (w_{i_2}, h_{i_2}), \dots, (w_{i_k}, h_{i_k}) \] 满足 \[ w_{i_1} < w_{i_2} < \dots < w_{i_k}, \quad h_{i_1} < h_{i_2} < \dots < h_{i_k}, \] 则称该序列可以形成一组“俄罗斯套娃”。

目标:求满足上述条件的最长序列长度。


二、二维严格递增问题的本质困难

该问题本质是一个 二维偏序集上的最长链问题

  • 宽度必须严格递增
  • 高度必须严格递增

直接进行二维 DP: \[ dp[i] = \max_{j<i,\ w_j<w_i,\ h_j<h_i} (dp[j]+1) \] 时间复杂度为 \(O(n^2)\),在 \(n \le 10^5\) 的约束下不可行。

因此,必须通过结构性重排(排序)将二维约束降维。


三、核心思想:通过排序消去一个维度

3.1 排序的目标

排序的目的并非“整理数据”,而是:

人为制造一个“单调扫描顺序”,使其中一个严格条件自动成立

如果信封按宽度 \(w\) 排序,那么在扫描顺序中: \[ w_1 \le w_2 \le \dots \le w_n \] 此时,宽度条件已被部分满足,问题转化为:

在满足宽度顺序的前提下,寻找高度的最长严格递增子序列(LIS)

但这里隐藏着一个关键陷阱。


四、排序设计中的核心陷阱与优化

4.1 错误的直觉:双关键字同时升序

若使用排序规则: \[ (w \uparrow,\ h \uparrow) \] 则对于同宽信封: \[ (w, h_1), (w, h_2), \quad h_1 < h_2 \] 在 LIS 过程中,高度序列是递增的,算法会错误地将它们同时选入。

然而这违反了问题的严格宽度递增条件: \[ w \not< w \] 结论

同宽信封在 LIS 阶段必须被强制“互斥”。


4.2 关键优化:同宽高度逆序排序

正确的排序策略是: \[ \boxed{ \text{宽度 } w \text{ 升序,若 } w \text{ 相同,则高度 } h \text{ 降序} } \] 形式化写为: \[ (w_i < w_j) \ \lor\ (w_i = w_j \land h_i > h_j) \]

直观解释

  • 宽度升序:保证扫描顺序中宽度不下降
  • 同宽高度降序:破坏高度的递增性

于是对于同宽信封: \[ (w, 5), (w, 4), (w, 3) \] 高度序列是严格递减的,LIS 不可能选中多个


4.3 这一设计的数学意义

排序后得到的序列满足:

  • 任意被 LIS 同时选中的两个元素 \(i < j\),必然有 \[ w_i < w_j \] (因为若 \(w_i = w_j\),则 \(h_i > h_j\),无法递增)

因此:

在高度上做 LIS,即等价于在二维条件下做最长合法嵌套序列

这一步是整个算法正确性的核心。


五、降维后的算法流程

5.1 排序

1
sort(envelopes.begin(), envelopes.end(), tmp());

排序规则:

  • a[0] < b[0]
  • 若相等,a[1] > b[1]

5.2 在高度序列上求 LIS(O(n log n))

定义数组: \[ f[k] = \text{长度为 } k+1 \text{ 的严格递增子序列的最小结尾高度} \] 对每个高度 \(h\)

  • \(f\) 中二分查找第一个 \(\ge h\) 的位置
  • \(h\) 覆盖该位置(或扩展数组)
1
auto it = lower_bound(f.begin(), f.end(), h);

使用 lower_bound 的原因是:

  • 要求 严格递增
  • 相等高度不允许扩展序列

六、完整算法正确性总结

该算法正确性的三层保证:

  1. 排序阶段
    • 宽度升序:消除宽度回退
    • 同宽高度降序:消除非法同宽嵌套
  2. LIS 阶段
    • 只在高度上判断严格递增
    • 自动保证宽度也严格递增
  3. 复杂度
    • 排序:\(O(n \log n)\)
    • LIS:\(O(n \log n)\)
    • 总复杂度:\(O(n \log n)\)

七、AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
static const int maxn = 1e5 + 5;
struct tmp {
bool operator()(const vector<int>& a, const vector<int>& b) {
if (a[0] != b[0])
return a[0] < b[0];
return a[1] > b[1];
}
};
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(), envelopes.end(), tmp());
vector<int> f;
for (vector<int>& x : envelopes) {
auto it = lower_bound(f.begin(), f.end(), x[1]);
if (it == f.end())
f.push_back(x[1]);
else
*it = x[1];
}
return f.size();
}
};