线段树优化最长上升子序列问题
最长上升子序列
给定一个长度为 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 。
第二行包含 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
,
输入样例:
7 3 1 2 1 8 5 6
输出样例:
4
解题思路
首先这是最基本的最长上升子序列问题,常规解法是动态规划,时间复杂度为。
定义状态表示所有以第个数结尾的严格递增的序列,属性是最大值。现在序列的最后一个数已经确定了(就是第个数),因此可以根据序列的倒数第二个数来进行集合的划分,状态转移方程为
当然要满足,表示第个位置上的数。同时还需要注意到如果以第个数结尾的序列长度为,也就是只选择这个数(没有倒数第二个数),此时应该有,因此枚举到时需要进行初始化(理解为没有倒数第二数的情况)。
AC代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N = 1010; 5 6 int a[N]; 7 int f[N]; 8 9 int main() { 10 int n; 11 scanf("%d", &n); 12 for (int i = 1; i <= n; i++) { 13 scanf("%d", a + i); 14 } 15 16 int ret = 0; 17 for (int i = 1; i <= n; i++) { 18 f[i] = 1; // 序列中只有a[i]这个数 19 for (int j = 1; j < i; j++) { 20 if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1); // 要满足a[j]<a[i]才可以接到a[i]的后面 21 } 22 ret = max(ret, f[i]); 23 } 24 25 printf("%d", ret); 26 27 return 0; 28 }
如果扩大到,很明显上面的做法肯定是会超时的,下面讲如何用线段树来优化。
最长上升子序列 II
给定一个长度为 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 。
第二行包含 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
,
输入样例:
7 3 1 2 1 8 5 6
输出样例:
4
解题思路
好吧其实题面和上一题完全是一样的,就是的数据范围扩大到。
做法其实有两种,一种是贪心加二分,时间复杂度可以做到。另外一种还是动态规划,不过要用线段树来优化,时间复杂度也是,这里主要是讲如何用线段树来优化的(另外一种做法有空再写,挖个坑先)。
这里的状态定义就和前面的不一样了。定义状态表示所有以数值为结尾的严格递增的序列,属性是最大值。这里的不是指第个数,而是一个明确的值(即某个位置上的数值)。一样根据序列倒数第二个数值的不同来划分集合,因此状态转移为
看上去好像跟第一题那个状态转移方程一样,但要明确的是第一题的是指数的下标,而这一题的是指数值的大小。
可以发现每次更新状态时,就是求一个区间的最值(等号右边),以及进行单点修改(等号左边),因此可以想到用线段树来维护。
线段树维护的是状态数组各个区间的最值。一开始初始化线段树的时候有为,因为此时还没有任何一个数构成序列,对应的线段树所维护的各个区间的最值为。
还没有完,注意到数的取值范围是,因此需要进行离散化。
AC代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N = 1e5 + 10; 5 6 struct Node { 7 int l, r, maxv; 8 }tr[N * 4]; 9 int a[N]; 10 int xs[N], sz; 11 12 int find(int x) { 13 int l = 1, r = sz; 14 while (l < r) { 15 int mid = l + r >> 1; 16 if (xs[mid] >= x) r = mid; 17 else l = mid + 1; 18 } 19 return l; 20 } 21 22 void build(int u, int l, int r) { 23 if (l == r) { 24 tr[u] = {l, r, 0}; // 初始化各个区间的最值为0 25 } 26 else { 27 int mid = l + r >> 1; 28 build(u << 1, l, mid); 29 build(u << 1 | 1, mid + 1, r); 30 tr[u] = {l, r, 0}; 31 } 32 } 33 34 void modify(int u, int x, int c) { 35 if (tr[u].l == x && tr[u].r == x) { 36 tr[u].maxv = c; 37 } 38 else { 39 if (x <= tr[u].l + tr[u].r >> 1) modify(u << 1, x, c); 40 else modify(u << 1 | 1, x, c); 41 tr[u].maxv = max(tr[u << 1].maxv, tr[u << 1 | 1].maxv); 42 } 43 } 44 45 int query(int u, int l, int r) { 46 if (tr[u].l >= l && tr[u].r <= r) return tr[u].maxv; 47 int mid = tr[u].l + tr[u].r >> 1, maxv = 0; 48 if (l <= mid) maxv = query(u << 1, l, r); 49 if (r >= mid + 1) maxv = max(maxv, query(u << 1 | 1, l, r)); 50 return maxv; 51 } 52 53 int main() { 54 int n; 55 scanf("%d", &n); 56 for (int i = 1; i <= n; i++) { 57 scanf("%d", a + i); 58 xs[++sz] = a[i]; 59 } 60 61 // 离散化 62 sort(xs + 1, xs + sz + 1); 63 sz = unique(xs + 1, xs + sz + 1) - xs - 1; 64 65 build(1, 1, sz); 66 67 for (int i = 1; i <= n; i++) { 68 int x = find(a[i]); // a[i]映射到值x 69 // 查询f[1]~f[x-1]的最值,同时修改f[x]的值 70 modify(1, x, query(1, 1, x - 1) + 1); 71 // 可以分开写成 72 // int t = query(1, 1, x - 1); 73 // modify(1, x, t + 1); 74 } 75 76 printf("%d", tr[1].maxv); // 枚举f[1~sz]的最值,可以发现tr[1]就维护了1~sz的最值 77 78 return 0; 79 }
2023-02-10补充更新。
其实用原始的状态定义也是可以的,我更推荐这样理解。
一样的,定义状态表示所有以第个数结尾的严格递增的序列的最大长度。然后状态转移本质就是在前个数中找出来所有满足的,然后在这些中找到一个最大的。因此我们可以开个线段树来维护所有的,在处记录的值是,然后在这个前缀中找到最大的。
AC代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N = 1e5 + 10; 5 6 int a[N]; 7 int f[N]; 8 int xs[N], sz; 9 struct Node { 10 int l, r, maxv; 11 }tr[N * 4]; 12 13 int find(int x) { 14 int l = 1, r = sz; 15 while (l < r) { 16 int mid = l + r >> 1; 17 if (xs[mid] >= x) r = mid; 18 else l = mid + 1; 19 } 20 return l; 21 } 22 23 void build(int u, int l, int r) { 24 if (l == r) { 25 tr[u] = {l, r, 0}; 26 } 27 else { 28 int mid = l + r >> 1; 29 build(u << 1, l, mid); 30 build(u << 1 | 1, mid + 1, r); 31 tr[u] = {l, r, 0}; 32 } 33 } 34 35 void modify(int u, int x, int c) { 36 if (tr[u].l == tr[u].r) { 37 tr[u].maxv = c; 38 } 39 else { 40 if (x <= tr[u].l + tr[u].r >> 1) modify(u << 1, x, c); 41 else modify(u << 1 | 1, x, c); 42 tr[u].maxv = max(tr[u << 1].maxv, tr[u << 1 | 1].maxv); 43 } 44 } 45 46 int query(int u, int l, int r) { 47 if (tr[u].l >= l && tr[u].r <= r) return tr[u].maxv; 48 int mid = tr[u].l + tr[u].r >> 1, maxv = 0; 49 if (l <= mid) maxv = query(u << 1, l, r); 50 if (r >= mid + 1) maxv = max(maxv, query(u << 1 | 1, l, r)); 51 return maxv; 52 } 53 54 int main() { 55 int n; 56 scanf("%d", &n); 57 for (int i = 1; i <= n; i++) { 58 scanf("%d", a + i); 59 xs[++sz] = a[i]; 60 } 61 sort(xs + 1, xs + sz + 1); 62 sz = unique(xs + 1, xs + sz + 1) - xs - 1; 63 build(1, 1, sz); 64 for (int i = 1; i <= n; i++) { 65 int x = find(a[i]); 66 f[i] = query(1, 1, x - 1) + 1; 67 modify(1, x, f[i]); 68 } 69 printf("%d", *max_element(f + 1, f + n + 1)); 70 71 return 0; 72 }
另外,求前缀最大值也可以用树状数组来实现,AC代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N = 1e5 + 10; 5 6 int a[N]; 7 int xs[N], sz; 8 int f[N]; 9 int tr[N]; 10 11 int find(int x) { 12 int l = 1, r = sz; 13 while (l < r) { 14 int mid = l + r >> 1; 15 if (xs[mid] >= x) r = mid; 16 else l = mid + 1; 17 } 18 return l; 19 } 20 21 int lowbit(int x) { 22 return x & -x; 23 } 24 25 void add(int x, int c) { 26 for (int i = x; i <= sz; i += lowbit(i)) { 27 tr[i] = max(tr[i], c); 28 } 29 } 30 31 int query(int x) { 32 int ret = 0; 33 for (int i = x; i; i -= lowbit(i)) { 34 ret = max(ret, tr[i]); 35 } 36 return ret; 37 } 38 39 int main() { 40 int n; 41 scanf("%d", &n); 42 for (int i = 1; i <= n; i++) { 43 scanf("%d", a + i); 44 xs[++sz] = a[i]; 45 } 46 sort(xs + 1, xs + sz + 1); 47 sz = unique(xs + 1, xs + sz + 1) - xs - 1; 48 for (int i = 1; i <= n; i++) { 49 int x = find(a[i]); 50 f[i] = query(x - 1) + 1; 51 add(x, f[i]); 52 } 53 printf("%d", *max_element(f + 1, f + n + 1)); 54 55 return 0; 56 }
Longest Increasing Subsequence II
You are given an integer array nums and an integer k .
Find the longest subsequence of nums that meets the following requirements:
- The subsequence is strictly increasing and
- The difference between adjacent elements in the subsequence is at most k .
Return the length of the longest subsequence that meets the requirements.
A subsequence is an array that can be derived from another array by deleting some or no elements without changing the order of the remaining elements.
Example 1:
Input: nums = [4,2,1,4,3,4,5,8,15], k = 3 Output: 5 Explanation: The longest subsequence that meets the requirements is [1,3,4,5,8]. The subsequence has a length of 5, so we return 5. Note that the subsequence [1,3,4,5,8,15] does not meet the requirements because 15 - 8 = 7 is larger than 3.
Example 2:
Input: nums = [7,4,5,1,8,12,4,7], k = 5 Output: 4 Explanation: The longest subsequence that meets the requirements is [4,5,8,12]. The subsequence has a length of 4, so we return 4.
Example 3:
Input: nums = [1,5], k = 1 Output: 1 Explanation: The longest subsequence that meets the requirements is [1]. The subsequence has a length of 1, so we return 1.
Constraints:
解题思路
可以发现就是上一题的扩展,序列不仅要保证严格单调递增,同时还有满足任意两个相邻的数的差不超过。做法和上面那题几乎一样,不过由于数值的取值范围很小,因此不需要进行离散化。
状态定义与上一题一样,表示所有以数值为结尾的严格递增的序列,属性是最大值。状态转移方程就变成了
比赛的时候一直想着最原始的最长上升子序列问题的状态定义(即第一题的状态定义),结果用贪心加二分的做法一直没写出来,实在是没想到会用这种方式来定义状态。
AC代码如下:
1 const int N = 1e5 + 10; 2 3 class Solution { 4 public: 5 struct Node { 6 int l, r, maxv; 7 }tr[N * 4]; 8 9 void build(int u, int l, int r) { 10 if (l == r) { 11 tr[u] = {l, r, 0}; 12 } 13 else { 14 int mid = l + r >> 1; 15 build(u << 1, l, mid); 16 build(u << 1 | 1, mid + 1, r); 17 tr[u] = {l, r, 0}; 18 } 19 } 20 21 void modify(int u, int x, int c) { 22 if (tr[u].l == x && tr[u].r == x) { 23 tr[u].maxv = c; 24 } 25 else { 26 if (x <= tr[u].l + tr[u].r >> 1) modify(u << 1, x, c); 27 else modify(u << 1 | 1, x, c); 28 tr[u].maxv = max(tr[u << 1].maxv, tr[u << 1 | 1].maxv); 29 } 30 } 31 32 int query(int u, int l, int r) { 33 if (tr[u].l >= l && tr[u].r <= r) return tr[u].maxv; 34 int mid = tr[u].l + tr[u].r >> 1, maxv = 0; 35 if (l <= mid) maxv = query(u << 1, l, r); 36 if (r >= mid + 1) maxv = max(maxv, query(u << 1 | 1, l, r)); 37 return maxv; 38 } 39 40 int lengthOfLIS(vector<int>& nums, int k) { 41 build(1, 1, N - 1); 42 for (int &it : nums) { 43 modify(1, it, query(1, max(1, it - k), it - 1) + 1); 44 } 45 return tr[1].maxv; 46 } 47 };
参考资料
【力扣周赛 310】最长上升子序列+线段树优化DP | LeetCode 算法刷题:https://www.bilibili.com/video/BV1it4y1L7kL
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/16689556.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效