最长上升子序列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';
}
posted @   Iter-moon  阅读(151)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示