题目链接:https://leetcode.cn/problems/longest-increasing-subsequence-ii/
题目大意:给出一个数组,求满足以下条件的子串的最长长度。
- 元素严格单调递增
- 相邻元素的差不超过
k
其中子串的定义是:从原数组中删除0或若干个元素形成的新数组(也就是说元素的相对顺序并不改变,只是中间的0个或几个元素给抽掉了)
思路:开始时还以为子串是连续的,后来发现可以中间抽掉元素…果然Hard超出了能力范围,估计又是什么神奇的DP。看了看题解,发现方法居然不止一种,于是挑了第一个先看看(https://leetcode.cn/problems/longest-increasing-subsequence-ii/solution/zhi-yu-xian-duan-shu-pythonjavacgo-by-en-p1gz/)
说实话第一个线段树就花了我一晚上理解,第二天再代入题目,大致知道了为什么这样是对的,但怎么想出来的我还是没有头绪。
首先将题目转化为DP,设数组为nums[]
,设
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示数组前
i
i
i个元素中以元素
j
j
j结尾的,满足题设要求的子串的最大长度。我们规定下标从1开始(其实这是为了方便之后的线段树构建)。显然初始时,所有f
的值都是
0
0
0。
如样例给的数组[4 2 1 4 3 4 5 8 15]
,
f
[
1
]
[
4
]
f[1][4]
f[1][4]就表示到第一个元素为止,以4
结尾的子串最大长度。
那么你会问:
f
[
1
]
[
4
]
f[1][4]
f[1][4]固然很好,有明确的意义,因为nums[1] == 4
,那么
f
[
1
]
[
3
]
f[1][3]
f[1][3]代表什么?其实就按照定义就好,它表示到第一个元素为止,以3
结尾的子串最大长度。因为这个例子的nums[1] == 4
,所以
f
[
1
]
[
3
]
f[1][3]
f[1][3]用不上,但其他例子如果以3
开头,不就用上了吗?
接下来写DP的状态转移方程:
- 若
j
≠
j\ne
j=
nums[i]
,则 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j]=f[i-1][j] f[i][j]=f[i−1][j]。
这是显然的,因为我们默认以
j
j
j结尾是最佳的,而当前扫描到的数组末尾元素nums[i]
不是最佳的,那么这个nums[i]
就毫无作用,即第i
个元素无影响,如同只有前i-1
个元素一样
- 若
j
=
j=
j=
nums[i]
,则 f [ i ] [ j ] = 1 + max j − k ≤ j ′ ≤ j − 1 f [ i − 1 ] [ j ′ ] f[i][j]=1+\max\limits_{j-k\le j\prime\le j-1}f[i-1][j\prime] f[i][j]=1+j−k≤j′≤j−1maxf[i−1][j′]。
此时与上一种情况相反,当前扫描到的数组末尾元素nums[i]
就是最佳的,那么它需要从
f
[
i
−
1
]
f[i-1]
f[i−1]中的某处转移过来。
j
−
k
j-k
j−k的来源是:相邻元素的差不超过k
。
j
−
1
j-1
j−1的来源是:严格单调递增
如题目给的k=3
,那么如果接下来放元素5
是最佳,那么上一个元素绝不可能是1
,也不可能是5
。
在给定范围内找到最大的长度,加上这个我们要放的nums[i]
的长度(就是1),完成转移。
当然,这个范围必须都是有效的,比如出现0和负数的话要比较下,保证范围合法有效。
注意到【转移】只会从
f
[
i
−
1
]
f[i-1]
f[i−1]到
f
[
i
]
f[i]
f[i],因为我们对nums[]
的扫描就是从左到右一个一个来的,所以
f
f
f第一个维度在写代码时可以忽略掉,
f
[
i
]
[
j
]
f[i][j]
f[i][j] => f[j]
。
等式右边有一个【区间最大值】,我们就可以引出这个解法的主角了:线段树。
线段树的每个节点都代表数组的一段区间,而且同一节点的两个儿子代表的区间是尽量平衡的(即两个区间长度要么相等,要么只差1)。
【注意!!!】这题中,我们要用线段树表示的数组,并不是所给数组nums[]
,而是我们的DP数组f[]
!这一点如果搞混了,那这个题解就完全糊涂了!
因为f[]
代表的是【以某元素结尾的满足条件的子串的最大长度】,因此线段树里每个节点存的元素也是这个意义。而线段树节点的区间[l, r]
,不存在节点里,只在调用函数时手动传进去。
线段树的查询和更新操作如下
// root 线段树当前节点下标
// l 当前节点代表的区间左边界
// r 当前节点代表的区间右边界
// i 要修改的数组元素下标
// val 要修改的数组元素的值
void Solution::updateTree(int root, int l, int r, int i, int val) {
if (l == r) {
mx[root] = val;
return;
}
int mid = (l + r) / 2;
if (i <= mid)
updateTree(root*2, l, mid, i ,val);
else
updateTree(root*2+1, mid+1, r, i, val);
mx[root] = mx[root*2] > mx[root*2+1] ? mx[root*2] : mx[root*2+1];
}
// root 线段树当前节点下标
// l 当前节点代表的区间左边界
// r 当前节点代表的区间右边界
// L 希望查询的范围的左边界
// R 希望查询的范围的右边界
int Solution::query(int root, int l, int r, int L, int R) {
if (L <= l && r <= R)
return mx[root];
int ret = 0;
int mid = (l + r) / 2;
if (L <= mid)
ret = query(root*2, l, mid, L, R);
if (R > mid)
ret = query(root*2+1, mid+1, r, L, R) > ret ? query(root*2+1, mid+1, r, L, R) : ret;
return ret;
}
还有些细节之后慢慢补充。。。
完整代码
#include "Solution.h"
using namespace std;
// root 线段树当前节点下标
// l 当前节点代表的区间左边界
// r 当前节点代表的区间右边界
// i 要修改的数组元素下标
// val 要修改的数组元素的值
void Solution::updateTree(int root, int l, int r, int i, int val) {
if (l == r) {
mx[root] = val;
return;
}
int mid = (l + r) / 2;
if (i <= mid)
updateTree(root*2, l, mid, i ,val);
else
updateTree(root*2+1, mid+1, r, i, val);
mx[root] = mx[root*2] > mx[root*2+1] ? mx[root*2] : mx[root*2+1];
}
// root 线段树当前节点下标
// l 当前节点代表的区间左边界
// r 当前节点代表的区间右边界
// L 希望查询的范围的左边界
// R 希望查询的范围的右边界
int Solution::query(int root, int l, int r, int L, int R) {
if (L <= l && r <= R)
return mx[root];
int ret = 0;
int mid = (l + r) / 2;
if (L <= mid)
ret = query(root*2, l, mid, L, R);
if (R > mid)
ret = query(root*2+1, mid+1, r, L, R) > ret ? query(root*2+1, mid+1, r, L, R) : ret;
return ret;
}
int Solution::lengthOfLIS(vector<int> &nums, int k) {
int up = *max_element(nums.begin(), nums.end());
mx.resize(4 * up);
for (int x : nums) {
if (x == 1)
updateTree(1, 1, up, 1, 1);
else {
int L = x - k > 1 ? x - k : 1;
int ret = 1 + query(1, 1, up, L, x - 1);
updateTree(1, 1, up, x, ret);
}
}
return mx[1];
}