ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

线段树优化最长上升子序列问题

2022-09-13 16:30:28  阅读:191  来源: 互联网

标签:10 nums int 线段 leq subsequence 序列 最长


最长上升子序列

给定一个长度为 $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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有