最长上升子序列LIS 详解+变形+拓展
最长上升子序列(LIS):
定义:
1 | 最长上升子序列(LIS)是一个序列中,找到一个子序列,使得这个子序列的元素是严格递增的,且该子序列的长度最大 |
*子串和子序列的差别:
1 2 | 子串: 元素的连续性,必须是相邻的 子序列:元素的相对顺序,可以不连续 |
动态规划 O(n ^ 2)
1 | 定义 dp[i] 表示以 a[i] 为结尾的最长递增子序列的长度 |
解释和操作:
1 | 对于每个元素a[i],找到所有小于a[i]的元素,并更新dp[i]为这些元素的最长递增子序列长度加1,最后返回dp数组中的最大值 |
样例分析:
1 2 3 4 5 6 7 8 9 10 | [1, 7, 5, 6, 9, 2, 4] 为例: 初始化 dp = [1, 1, 1, 1, 1, 1, 1] 流程: i=1:nums[1] = 7,dp = [1, 2, 1, 1, 1, 1, 1] i=2:nums[2] = 5,dp = [1, 2, 2, 1, 1, 1, 1] i=3:nums[3] = 6,dp = [1, 2, 2, 3, 1, 1, 1] i=4:nums[4] = 9,dp = [1, 2, 2, 3, 4, 1, 1] i=5:nums[5] = 2,dp = [1, 2, 2, 3, 4, 2, 1] i=6:nums[6] = 4,dp = [1, 2, 2, 3, 4, 2, 3] 最终 dp 数组是 [1, 2, 2, 3, 4, 2, 3],所以 LIS 长度是 4。 |
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void LIS_dp() { int n; cin >> n; vector < int > a(n + 1); for ( int i = 1; i <= n; i++) { cin >> a[i]; } vector < int > dp(n + 1, 1); for ( int i = 1; i <= n; i++) { for ( int j = 1; j < i; j++) { if (a[i] > a[j]) { dp[i] = max(dp[i], dp[j] + 1); } } } cout << *max_element(dp.begin() + 1, dp.end()) << '\n' ; } |
二分 复杂度(n * logn)
1 | 二分查找来维护一个递增子序列 |
解释和操作:
1 2 | 通过维护一个辅助数组来动态跟踪递增子序列,并用二分查找优化插入位置,减少时间复杂度为 O(nlogn),最终实现的是寻找和更新递增子序列的长度 |
样例分析:
1 2 3 4 5 6 7 8 9 10 11 12 | 数组:[1, 7, 5, 6, 9, 2, 4] 在二分查找优化版本中,我们维护一个辅助数组 lis 来动态存储当前的递增子序列,并通过二分查找插入或替换元素。 初始化:lis = [] # 为空 流程: 1:lis 为空,直接插入 1 → lis = [1] 7:7 大于 lis 最后一个元素 1,直接插入 → lis = [1, 7] 5:5 小于 7,用二分查找找到插入位置,替换 7 → lis = [1, 5] 6:6 大于 5,直接插入 → lis = [1, 5, 6] 9:9 大于 6,直接插入 → lis = [1, 5, 6, 9] 2:2 小于 5,用二分查找找到插入位置,替换 5 → lis = [1, 2, 6, 9] 4:4 小于 6,用二分查找找到插入位置,替换 6 → lis = [1, 2, 4, 9] 最终 lis 数组是 [1, 2, 4, 9],其长度为 4,这就是最长递增子序列的长度 |
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | void LIS_lower_bound() { int n; cin >> n; vector < int > ans; for ( int i = 1; i <= n; i++) { int x; cin >> x; if (ans.empty() || x > ans.back()) { ans.emplace_back(x); } else { auto it = lower_bound(ans.begin(), ans.end(), x) - ans.begin(); ans[it] = x; } } cout << ans.size() << '\n' ; } |
注意点:
1 | lis 并不是实际的最长递增子序列,而是正确长度 |
变形1:
俄罗斯套娃信封问题 (二维LIS)
传送门:https://leetcode.cn/problems/russian-doll-envelopes/description/
题意缩减:
1 | 我们需要找到可以嵌套的信封,形成的最长递增子序列 |
题意和做法:
1 | 从一维的最长递增子序列考虑, 那么会得到 对信封的宽度进行升序排列,如果宽度相同,对高度进行降序排列,然后在高度上寻找最长递增子序列 |
样例分析:
1 2 3 4 5 6 7 8 9 10 | [(2,3),(5,4),(6,7),(6,4)] 按照宽排序: 得到[3, 4, 7, 4] (省去了宽度只看高度) 初始化:lis = [] 遍历高度序列: 3:lis 为空,插入 3 → lis = [3] 4:4 大于 3,插入 4 → lis = [3, 4] 7:7 大于 4,插入 7 → lis = [3, 4, 7] 4:4 等于 lis[1],用二分查找替换 → lis = [3, 4, 7] 最终 lis 数组是 [3, 4, 7],其长度为 3,即最多可以嵌套 3 个信封。 |
Code:
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 : int maxEnvelopes(vector<vector< int >>& a) { sort(a.begin(), a.end(), [&]( auto a, auto b) { if (a[0] == b[0]) { return a[1] > b[1]; } return a[0] < b[0]; }); vector < int > ans; for ( auto pair : a) { int x = pair[1]; if (ans.empty() || x > ans.back()) { ans.push_back(x); } else { auto it = lower_bound(ans.begin(), ans.end(), x) - ans.begin(); ans[it] = x; } } return ans.size(); } }; |
变形二:
最长数对链
传送门:
https://leetcode.cn/problems/maximum-length-of-pair-chain/description/?envType=study-plan-v2&envId=dynamic-programming
题意:
找出链的LIS的最长递增子序列
数组的查询与更新分离模式: 关键如何对链操作进行类似LIS操作
1 2 3 4 | 查询部分:判断新数对 [x, y] 中的 x 是否大于当前 ans 数组的最后一个元素,如果是,则表示这个数对可以跟随在当前的链之后, 因此直接添加 y 到 ans 数组中,扩展链。 更新部分:如果 x 小于或等于 ans 中的最后一个元素,那么需要用二分查找 lower_bound 来寻找 ans 数组中第一个大于等于 x 的位置, 并且更新这个位置的 right 为较小的 y,确保 ans 数组中的值保持最优 |
样例分析:
1 2 3 4 5 6 7 | pairs = [[1, 2], [2, 3], [3, 4]] 为例: pairs 按照第一个元素排序后仍为 [[1, 2], [2, 3], [3, 4]]。 初始 ans = []。 [1, 2]:ans 为空,直接将 2 添加到 ans,即 ans = [2]。 [2, 3]:2 不大于 ans.back(),通过 lower_bound 找到第一个大于等于 2 的位置并更新为 3,即 ans = [3]。 [3, 4]:3 不大于 ans.back(),通过 lower_bound 找到第一个大于等于 3 的位置并更新为 4,即 ans = [4]。 结果:ans 数组大小为 2,表示最长数对链的长度为 2。 |
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Solution { public : int findLongestChain(vector<vector< int >>& a) { int n = a.size(); sort(a.begin(), a.end()); vector < int > ans; for ( auto pair : a) { int x = pair[0], y = pair[1]; if (ans.empty() || x > ans.back()) { ans.push_back(y); } else { auto it = lower_bound(ans.begin(), ans.end(), x) - ans.begin(); ans[it] = min(ans[it], y); } } return ans.size(); } }; |
变形3:
最长不下降子序列(LNIS)
定义:
1 | 在原有的条件中LNDS 允许序列中的元素相等 |
修改:
1 2 3 | LIS ,使用的是 lower_bound 函数查找 严格大于等于 当前元素的第一个位置进行替换,而 LNDS 则是寻找 大于 当前元素的第一个位置,确保相等的元素也能包含在序列中 lower_bound 查找第一个 大于等于 当前元素的索引,并替换该位置的值 那么使用 upper_bound 查找第一个 大于 当前元素的索引,这样相等的元素不会被替换,可以保留在当前序列中 |
样例分析:
1 2 3 4 5 6 7 8 | 子序列为 [1, 2, 4]。 初始化 lnds = []。 1:lnds 为空,插入 1 → lnds = [1]。 3:3 > 1,插入 3 → lnds = [1, 3]。 3:3 == lis.back(),通过 upper_bound 找到第一个 大于 3 的位置(即末尾),插入 3 → lnds = [1, 3, 3]。 2:2 < 3,通过 upper_bound 找到第一个大于 2 的位置(即第二个位置),替换该位置的 3 → lnds = [1, 2, 3]。 4:4 > 3,插入 4 → lnds = [1, 2, 3, 4]。 最终 LNDS 长度为 4,子序列为 [1, 2, 3, 4]。 |
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void LNIS () { int n; cin >> n; vector < int > ans; for ( int i = 1; i <= n; i++) { int x; cin >> x; if (ans.empty() || x >= ans.back()) { ans.push_back(x); } else { auto it = upper_bound(ans.begin(), ans.end(), x) - ans.begin(); ans[it] = x; } } cout << ans.size() << '\n' ; } |