其他分享
首页 > 其他分享> > 线段树优化最长上升子序列问题

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

作者:互联网

最长上升子序列

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

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