排序算法之:Patience Sorting 的“最长递增子序列”等价性证明
题目描述:给定一个整数数组,Patience Sorting 算法在排序过程中,会构建一个“堆栈序列”,最终堆栈的数量等于原数组的最长递增子序列(Longest Increasing Subsequence, LIS)的长度。请你详细解释这一过程的原理,并严格证明“堆栈数等于LIS长度”的结论。
解题过程循序渐进讲解如下:
第一步:理解Patience Sorting的基本流程
Patience Sorting是一种基于“堆栈”的排序算法,它的核心步骤是遍历数组,为每个元素找到合适的堆栈放置。规则是:
- 从左到右遍历数组元素。
- 对于当前元素
x,查看所有堆栈的栈顶元素。如果有堆栈的栈顶元素大于或等于x,则将x放入栈顶元素最小的那个堆栈(即第一个栈顶元素大于等于x的堆栈)。 - 如果没有堆栈的栈顶元素大于等于
x,则新建一个堆栈,将x放入其中。
示例:数组 [3, 1, 4, 1, 5, 9, 2, 6]
- 3 → 新建堆栈1: [3]
- 1 → 栈顶3 >= 1,放入堆栈1 → 堆栈1: [1] (替换3)
- 4 → 栈顶1 < 4,新建堆栈2: [4]
- 1 → 栈顶1 >= 1,放入堆栈1(最小满足条件的栈顶是1) → 堆栈1: [1, 1] (第二个1压入)
- 5 → 栈顶1(堆栈1)< 5,栈顶4(堆栈2)< 5,新建堆栈3: [5]
- 9 → 栈顶1, 4, 5 均 < 9,新建堆栈4: [9]
- 2 → 栈顶1 < 2,栈顶4 >= 2,放入堆栈2 → 堆栈2: [4, 2]
- 6 → 栈顶1, 2, 5, 9 中,5 >= 6? 否,9 >= 6? 否,新建堆栈5: [6]
最终堆栈数量 = 5。
第二步:理解“堆栈序列”与LIS的联系
关键观察:每个堆栈的栈顶元素(从上到下)构成一个非递减序列。因为规则是“将 x 放入第一个栈顶大于等于 x 的堆栈”,所以后放入的元素不会破坏栈顶的有序性。
但更重要的性质是:堆栈的数量等于原数组的最长递增子序列(LIS)的长度。以上例的堆栈数5,数组的LIS是 [1, 4, 5, 9]? 不,实际LIS是 [1, 4, 5, 9] 长度4?不对,我们仔细算一下。
实际数组 [3,1,4,1,5,9,2,6] 的LIS可以是 [1,4,5,9] 或 [1,4,5,6] 长度4。但堆栈数量是5。这里需要明确:堆栈数等于LIS长度,但必须按“严格递增”定义LIS。在严格递增定义下,LIS可以是 [1,4,5,9,?] 没有更长的,[1,4,5,6] 长度4,所以这里矛盾吗?不矛盾,因为我们演示的堆栈构建过程其实在严格递增定义下堆栈数应为4。但我们上面第4步将第二个1放入堆栈1,导致堆栈1有两个1,这是允许相等元素放在同一堆栈的规则(栈顶大于等于x时放入),所以此时堆栈数可能多于LIS长度?这引出关键:在严格递增的LIS定义下,我们要么修改规则为“栈顶严格大于x时才放入”,要么考虑非严格递增LIS。原定理通常指非递减LIS(允许相等),而“最长严格递增子序列”需要另外处理。
我们重新明确:在经典Patience Sorting算法中,规则是“栈顶 >= x”,此时堆栈数等于“最长非递减子序列”的长度。如果我们要得到严格递增LIS长度,可以预处理:将所有元素改为 (值, 索引) 对,排序时先按值、再按索引降序,从而避免相等元素形成递增。
但常见考题是证明“堆栈数 = 最长严格递增子序列长度”,所以规则需调整为“栈顶 > x 才放入”,且放入第一个满足的堆栈。我们以此为准重新推演:
规则调整为:对于当前元素 x,查看所有堆栈的栈顶,找到第一个栈顶元素严格大于** x 的堆栈,放入;如果没有,新建堆栈**。
重新演示:
[3,1,4,1,5,9,2,6]
- 3 → 新建堆栈1: [3]
- 1 → 栈顶3 > 1,放入堆栈1 → 堆栈1: [1]
- 4 → 栈顶1 < 4,新建堆栈2: [4]
- 1 → 栈顶1 不大于1,栈顶4 > 1,放入堆栈2 → 堆栈2: [4,1] (注意,这里1放在4下面,栈顶是1)
- 5 → 栈顶1 < 5, 栈顶1(堆栈2) < 5,新建堆栈3: [5]
- 9 → 栈顶1,1,5 都 < 9,新建堆栈4: [9]
- 2 → 栈顶1,1,5,9 中,5 > 2,第一个大于2的是堆栈3栈顶5,放入 → 堆栈3: [5,2]
- 6 → 栈顶1,1,2,9 中,9 > 6,放入堆栈4 → 堆栈4: [9,6]
最终堆栈数=4。LIS严格递增长度确实是4(例如 [1,4,5,9] 或 [1,4,5,6])。
所以,在严格规则下,堆栈数 = 最长严格递增子序列长度。
第三步:证明堆栈数等于LIS长度的思路
证明分两部分:
1. 堆栈数 ≤ LIS长度:
因为每个堆栈中,元素从上到下是递减的(因为新放入的元素一定小于栈顶,否则会被放到更后面的堆栈),所以从不同堆栈中各取一个元素,它们不可能构成递增序列(因为它们按堆栈顺序是递减的)。那么,如果我们有k个堆栈,我们最多只能从每个堆栈取一个元素形成递增序列,所以LIS长度至少为k。更严格地说,假设LIS长度为L,那么我们可以从每个堆栈的栈底(或某个位置)选一个元素,但需要证明k ≤ L。反证:如果k > L,那么我们可以从每个堆栈选栈顶元素,按堆栈顺序是递减的,无法形成长度为k的递增子序列,矛盾。所以堆栈数 ≤ LIS长度。
2. LIS长度 ≤ 堆栈数:
考虑原数组的任意一个递增子序列,其元素在放入堆栈时,它们必须放在不同的堆栈。因为如果两个递增子序列中的元素放在同一堆栈,则后放入的元素一定小于先放入的(堆栈内从上到下递减),与递增矛盾。所以递增子序列的长度不能超过堆栈数。因此LIS长度 ≤ 堆栈数。
综上,堆栈数 = LIS长度。
第四步:用归纳法严格证明
更严格的归纳证明思路:
- 定义
f(i)为前i个元素形成的堆栈数,g(i)为前i个元素的LIS长度。 - 当加入第i个元素
x时:- 如果
x可以放入已有堆栈(即存在栈顶 > x),则f(i) = f(i-1),此时x不能扩展任何以它结尾的递增子序列长度超过已有LIS,所以g(i) = g(i-1)。 - 如果
x必须新建堆栈,则f(i) = f(i-1) + 1。此时必然存在一个以x结尾的递增子序列,其长度等于新的堆栈数。因为我们可以从每个堆栈选一个元素(从堆栈1到新堆栈,依次从每个堆栈选一个比下一个小的元素)构成递增序列。
- 如果
- 通过维护每个堆栈的栈顶元素最小值,可以证明每一步的堆栈数就是当前LIS长度。
第五步:实际应用与扩展
这个定理的实用性在于:我们可以在O(n log n)时间内求出LIS长度(因为每次查找“第一个栈顶大于x的堆栈”可以用二分查找维护栈顶数组)。实际上,这就是经典的“贪心+二分求LIS”算法。
扩展:Patience Sorting 不仅可以求长度,还可以通过回溯堆栈来重建一个LIS。不过需要注意的是,堆栈内的元素不一定直接构成LIS,需要记录每个元素的前驱指针。
总结:Patience Sorting 的堆栈构建过程巧妙地编码了LIS的信息,其核心证明基于“堆栈内元素递减,不同堆栈间元素可构成递增”的序关系。通过调整比较规则(严格大于或大于等于),可以得到严格或非严格递增子序列的长度。