二分求解LIS
用暴力或树状数组求解 LIS 长度的方法都很直观易懂,而还有一种利用二分求解 LIS 的算法,经典但抽象,这里作记录以备忘。
暴力 / 树状数组求 LIS 的本质思想都是固定末尾元素位置,最大化链的长度;而二分求解的思路却相反——它的本质思想是固定链的长度,最小化末尾元素的值。
显然对于末尾元素更小的序列,更有利于 LIS 长度的扩展。
状态定义
\(dp_{i}\):考虑长度为 \(i\) 的链(严格上升子序列),末尾元素的最小值。
状态转移
一个比较关键的发现:\(dp\) 数组是严格单调递增的。因此我们可以在 \(dp\) 数组上二分以加速递推。
我们仍然按照原序列顺序依次遍历每个 \(a_{i}\),考虑 \(a_{i}\) 可以更新哪些 \(dp_{x}\)。由于 \(a_{i}\) 是当前序列中最靠后的元素,因此对于当前任何前 \(i-1\) 个元素构成的子序列,后面都可以接上 \(a_{i}\)。于是,对于前面的所有严格上升子序列,只要末尾元素 \(< a_{i}\),就一定可以将 \(a_{i}\) 接到其后面以实现 LIS 长度的扩展。我们先二分找到当前 \(dp\) 数组中第一个 \(\ge a_{i}\) 的位置 \(j\)(\(dp_{j} \ge a_{i}\)) ,对于 \(dp\) 数组的更新,我们分几种情况讨论:
- 未找到 \(dp_{k} \ge a_{i}\):即所有 \(dp_{k} < a_{i}\),说明对于当前所有的上升子序列,都可以往后添加 \(a_{i}\)。此时 LIS 的长度会因为 \(a_{i}\) 的添加而增加 \(1\),故:\(dp_{len(LIS)+1} \leftarrow a_{i}\)。
否则说明 \(\exists k\),使得 \(dp_{k} \ge a_{i}\),二分可查找到结果。
- 对于 \(\forall k < j\) 的 \(dp_{k}\):\(dp_{k} < a_{i}\) 均成立,显然 \(a_{i}\) 在序列中的出现不会影响它们的值,因此无需作任何修改。
- 对于 \(\forall k = j\) 的 \(dp_{k}\):对于长度恰好为 \(j\) 的严格上升子序列,此时第 \(j-1\) 个元素一定 \(< a_{i}\)(否则就不是二分到这个位置了);而 \(dp_{k} \ge a_{i}\),将 \(dp_{k}\) 替换为 \(a_{i}\) 一定不劣,故 \(dp_{k} \leftarrow a_{i}\)。
- 对于 \(\forall k > j\) 的 \(dp_{k}\):由于这些上升子序列的第 \(k-1\) 个元素值一定 \(\ge a_{i}\),无法用 \(a_{i}\) 替换这些上升子序列的末尾元素,因此无需作任何修改。
综上,我们会发现,二分查找到相应位置后,我们最多只会添加 / 修改 \(1\) 个值,故总复杂度为 \(O(n\log n)\)。
code:
# 找 a[1~n] 的最长严格上升子序列
n = 6
a = [1, 3, 2, 4, 4, 5]
dp = []
for i in range(n):
if len(dp) == 0 or dp[-1] < a[i]:
dp.append(a[i])
else:
pos = bisect.bisect_left(dp, a[i])
dp[pos] = a[i]
print(len(dp)) # 4
若求解的是最长不下降子序列,只需要略做修改:
# 找 a[1~n] 的最长非严格上升子序列
n = 6
a = [1, 3, 2, 4, 4, 5]
dp = []
for i in range(n):
if len(dp) == 0 or dp[-1] <= a[i]: #upd1
dp.append(a[i])
else:
pos = bisect.bisect_right(dp, a[i]) #upd2
dp[pos] = a[i]
print(len(dp)) # 5

浙公网安备 33010602011771号