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

最长上升子序列

给定一个长度为 $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 100000$,
${−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 }

  2023-02-10补充更新。

  其实用原始的状态定义也是可以的,我更推荐这样理解。

  一样的,定义状态$f(i)$表示所有以第$i$个数结尾的严格递增的序列的最大长度。然后状态转移本质就是在前$i$个数中找出来所有满足$a_j < a_i$的$j$,然后在这些$j$中找到一个最大的$f(j)$。因此我们可以开个线段树来维护所有的$a_j$,在$a_j$处记录的值是$f(j)$,然后在$1 \sim a_i - 1$这个前缀中找到最大的$f(j)$。

  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:

$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

posted @ 2022-09-13 16:24  onlyblues  阅读(583)  评论(0编辑  收藏  举报
Web Analytics