线段树优化最长上升子序列问题
作者:互联网
最长上升子序列
给定一个长度为 $N$ 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 $N$。
第二行包含 $N$ 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
$1 \leq N \leq 1000$,
${−10}^{9} \leq \text{数列中的数} \leq {10}^{9}$
输入样例:
7 3 1 2 1 8 5 6
输出样例:
4
解题思路
首先这是最基本的最长上升子序列问题,常规解法是动态规划,时间复杂度为$O(n^2)$。
定义状态$f(i)$表示所有以第$i$个数结尾的严格递增的序列,属性是最大值。现在序列的最后一个数已经确定了(就是第$i$个数),因此可以根据序列的倒数第二个数来进行集合的划分,状态转移方程为$$f(i) = \max_{1 \leq j < i}\{ {f(j)+1} \}$$
当然$j$要满足$a[j] < a[i]$,$a[i]$表示第$i$个位置上的数。同时还需要注意到如果以第$i$个数结尾的序列长度为$1$,也就是只选择$a[i]$这个数(没有倒数第二个数),此时应该有$f(i)=1$,因此枚举到$i$时需要进行初始化$f(i)=1$(理解为没有倒数第二数的情况)。
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 }
如果$n$扩大到${10}^{5}$,很明显上面的做法肯定是会超时的,下面讲如何用线段树来优化。
最长上升子序列 II
给定一个长度为 $N$ 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 $N$。
第二行包含 $N$ 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
$1 \leq N \leq 1000$,
${−10}^{9} \leq \text{数列中的数} \leq {10}^{9}$
输入样例:
7 3 1 2 1 8 5 6
输出样例:
4
解题思路
好吧其实题面和上一题完全是一样的,就是$N$的数据范围扩大到${10}^5$。
做法其实有两种,一种是贪心加二分,时间复杂度可以做到$O(n \log n)$。另外一种还是动态规划,不过要用线段树来优化,时间复杂度也是$O(n \log n)$,这里主要是讲如何用线段树来优化的(另外一种做法有空再写,挖个坑先)。
这里的状态定义就和前面的不一样了。定义状态$f(i)$表示所有以数值$i$为结尾的严格递增的序列,属性是最大值。这里的$i$不是指第$i$个数,而是一个明确的值(即某个位置上的数值)。一样根据序列倒数第二个数值的不同来划分集合,因此状态转移为$$f(i) = \max_{1 \leq j < i}\{ {f(j)+1} \}$$
看上去好像跟第一题那个状态转移方程一样,但要明确的是第一题的$i,j$是指数的下标,而这一题的$i,j$是指数值的大小。
可以发现每次更新状态时,就是求一个区间的最值(等号右边),以及进行单点修改(等号左边),因此可以想到用线段树来维护。
线段树维护的是状态数组$f$各个区间的最值。一开始初始化线段树的时候有$f(i)$为$0$,因为此时还没有任何一个数构成序列,对应的线段树所维护的各个区间的最值为$0$。
还没有完,注意到数的取值范围是$\left[ {{−10}^{9}, {10}^{9}} \right]$,因此需要进行离散化。
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 }
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:
$1 \leq nums.length \leq {10}^{5}$
$1 \leq {nums[i], k} \leq {10}^{5}$
解题思路
可以发现就是上一题的扩展,序列不仅要保证严格单调递增,同时还有满足任意两个相邻的数的差不超过$k$。做法和上面那题几乎一样,不过由于数值的取值范围很小,因此不需要进行离散化。
状态定义$f(i)$与上一题一样,表示所有以数值$i$为结尾的严格递增的序列,属性是最大值。状态转移方程就变成了$$f(i) = \max_{i - k \leq j \leq i - 1}\{ {f(j)+1} \}$$
比赛的时候一直想着最原始的最长上升子序列问题的状态定义(即第一题的状态定义),结果用贪心加二分的做法一直没写出来,实在是没想到会用这种方式来定义状态。
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
标签:10,nums,int,线段,leq,subsequence,序列,最长 来源: https://www.cnblogs.com/onlyblues/p/16689556.html