个人练习-Leetcode-2407. Longest Increasing Subsequence II

题目链接: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[i1][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+jkjj1maxf[i1][j]

此时与上一种情况相反,当前扫描到的数组末尾元素nums[i]就是最佳的,那么它需要从 f [ i − 1 ] f[i-1] f[i1]中的某处转移过来。
j − k j-k jk的来源是:相邻元素的差不超过k
j − 1 j-1 j1的来源是:严格单调递增
如题目给的k=3,那么如果接下来放元素5是最佳,那么上一个元素绝不可能是1,也不可能是5
在给定范围内找到最大的长度,加上这个我们要放的nums[i]的长度(就是1),完成转移。
当然,这个范围必须都是有效的,比如出现0和负数的话要比较下,保证范围合法有效。

注意到【转移】只会从 f [ i − 1 ] f[i-1] f[i1] 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];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值