NlogN 求最长不下降子序列(LIS)
最长不下降子序列是一道非常经典的题目,我们假设题目如下:
有一个数组 $ a $,现在要从中抽取一个严格上升的子序列(子序列就是你可以从原序列里删除一些数,保留一部分数得到的新数列)(严格上升也就是不能相等只能递增),现在要求出这个子序列最长有多长?
举例来说,假设有一个数组 a = [10, 9, 2, 5, 3, 7, 101, 18]
。这个数组的最长上升子序列是 [2, 3, 7, 101]
,这是从原数组中第3, 5, 6, 7个位置选取得到的,长度为4。(当然最后一个数字换成 18 也可以)
平方做法
通常来讲,平方做法很容易想到,我们开一个数组 $ f $ ,用 $ f_i $ 表示以 $ a_i $ 结尾的 LIS 的长度。我们从左往右计算每一个 $ f_i $ ,每一个 $ f_i $ 可能由 \(f_1, f_2...f_{i-1}\)得到。比方说假如 $ a_i > a_j $,那么 \(a_i\) 就可以接到 $ f_{i-1} $ 表示的序列后面,长度加一,枚举所有之前的 f,取最大值,最终就可以计算出来。
#include <vector> #include <algorithm> using namespace std; int longestIncreasingSubsequence(const vector<int>& a) { int n = a.size(); vector<int> f(n, 1); // 初始化每个元素的LIS长度为1 for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { if (a[i] > a[j]) { f[i] = max(f[i], f[j] + 1); } } } return *max_element(f.begin(), f.end()); // 返回f中的最大值,即最长上升子序列的长度 } int main() { vector<int> a = {10, 9, 2, 5, 3, 7, 101, 18}; int result = longestIncreasingSubsequence(a); return result; }
N log N 做法
更高效的方法是通过维护一个数组 $ tail $ 来实现,其中 $ tail_i $ 存储长度为 i 的所有上升子序列中末尾最小的元素(比如 $ tail_4 $ 表示所有长度为 4 的 LIS 里最后一个数字最小能达到多少)。在更新过程中,我们使用二分搜索来确定一个元素应当放置在 $ tail $ 中的位置。
我们还是从左向右来枚举,对于每个元素 $ a_i $,在 \(tail\) 中找到第一个大于 $ a_i $ 的位置。
- 如果找到这样的位置,即表示之前有一个序列的末尾可以被替换,所以修改其 $ tail $ 值为新 $ a_i $
- 如果没有找到,说明 $ a_i $ 比之前所有数字都大,那么把 $ a_i $ 追加到 $ tail $ 末尾,最长的上升序列长度成功加1.
对于 $ tail $ 来说,下标越大,条件越难满足,则能达到的最小值越大,所以 $ tail $ 显然是严格递增的,这不难理解,而每一次操作也维持了 $ tail $ 的单调性。
举例来说,现在\(tail = [3, 5, 6, 19, 25]\),而当前 $ a_i $ 为 8,可以找到 $ tail_4 = 19$ 这表明在我之前出现过一个长度为4,以 19 结尾的序列,以及一个长度为3,以小于8结尾的序列,那么这个长度为3的序列可以接上8,替换掉原来的19,一定更优
#include <vector> #include <algorithm> using namespace std; int longestIncreasingSubsequence(const vector<int>& a) { vector<int> tail; for (int x : a) { auto it = lower_bound(tail.begin(), tail.end(), x); if (it == tail.end()) { tail.push_back(x); // 如果x大于tail中所有元素,追加到末尾 } else { *it = x; // 替换为更小的值,保持序列最小 } } return tail.size(); // tail的大小就是最长上升子序列的长度 } int main() { vector<int> a = {10, 9, 2, 5, 3, 7, 101, 18}; int result = longestIncreasingSubsequence(a); return result; }
获得具体的数列
除了得到最长长度,此方法也是支持把序列获取到的,增加一个数组,每一次更新 tail 时,来记录每个元素的前一个元素的索引,这里附加上 chatgpt 的代码:
def longest_non_decreasing_subsequence(arr): from bisect import bisect_right if not arr: return 0, [] dp = [] # 存放各长度最长不下降子序列的最小结尾元素的值 predecessor = [-1] * len(arr) # 前驱元素的索引 indices = [] # 存放各长度最长不下降子序列的最小结尾元素的索引 for i, x in enumerate(arr): pos = bisect_right([arr[idx] for idx in indices], x) if pos < len(indices): indices[pos] = i predecessor[i] = indices[pos-1] if pos > 0 else -1 else: indices.append(i) predecessor[i] = indices[-2] if len(indices) > 1 else -1 # 回溯找到最长不下降子序列 n = len(indices) sequence = [0] * n k = indices[-1] for j in range(n-1, -1, -1): sequence[j] = arr[k] k = predecessor[k] return n, sequence # 示例 arr = [10, 9, 2, 5, 3, 7, 101, 18] length, sequence = longest_non_decreasing_subsequence(arr) print("Length:", length) # 输出长度 print("Sequence:", sequence) # 输出序列
严格上升和不下降
要从求最长严格上升子序列变为求最长不下降子序列(也就是允许序列中的元素相等),主要修改的部分就是二分查找部分。在严格上升的场景中,tail 不会出现相等值,我们寻找的是第一个大于等于当前元素的位置,而在不下降的场景中,要找到的是第一个大于当前元素的位置。
举例子来说,当要求变为严格不下降,$ tail $ 就有可能出现相等值,假设 $ tail = [1, 2, 2, 3, 3, 3, 5] $,且下一个 \(a_i\) 为 2。那么应该更新 \(a_2\) 第一个3。而扩展 $ tail $ 的条件也放宽为大于等于,具体代码就不再展示。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步