Leetcode 300. 最长递增子序列

2025/04/12

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

线段树

var lengthOfLIS = function (nums) {
  // dp[i][j] 为前i个数字,结尾为j的数字的 最长递增子序列的长度
  // dp[i][j]可以由 0~i-1中dp[i-1][j'] 转移过来,其中 j' < j
  // 又因为前i个数字的状态都是由前i-1项转移过来的,所以在我们从前往后遍历过程中,可以省略这个维度
  //
  n = Math.max(...nums) + 10001

  // 开4*n的原因
  // 需要维护的区间个数是  m时(m是2的n次方) 需要的节点个数是 1+2+4+...+m = 2*m-1
  // 如果m不是2的n次方,例如m=10,维护的区间如下图所示,可以发现他不是满二叉树,
  // 那么,我们需要覆盖所有层,也就是覆盖最后一个不完整的层,那么我们就需要知道最后一层的节点个数
  // 我们可以先求出上一层满节点的个数 应该是log(m),那么下一层的节点个数就是2*log(m)
  // 所以,此时将整个树看成满二叉树时,需要的节点个数是 2*(2*log(m)) - 1 = 4*m -1
  // 又,线段树从1开始计数,所以节点个数还需要增加1,也就是 4*m -1 + 1 = 4*m

  /*        1
           /      \
          2        3         m=2 需要维护3个节点
         / \      /   \   
        4   5    6     7     m=4 需要维护7个节点
       / \ / \  / \    / \ 
       8 910 11 12 13 14 15   m=8 需要维护15个节点

       // m+10    
       1                           10
       |___________________________|                   1-10                                  1
       1           56                                /      \                  
       |___________||______________|              1-5        6-10                        2        3
       1    34     56      89      10            /  \       /   \
       |____||_____||______||______|            1-3 4-5    6-8  9-10                   4   5     6     7   
       1 2 3  4  5  6  7 8   9 10               / \  / \  / \    / \               
       |_||_||_||_| |__||_| |_||_|           1-2  3  4 5 6-7 8  9  10                 8 9 10 11 12 13 14 15
      1  2          6  7                     / \         / \         
     |_||_|        |_||_|                   1  2        6   7                      16 17          
    */
  let mx = Array(4 * n).fill(0)

  // o表示当前节点, [l, r]表示当前节点对应区间, idx加到哪, val加什么值
  // modify(1, 1, n, idx, val) 第一个一表示从根节点进去,区间为[1, n], 以及要插入的idx和val
  function modify(o, l, r, idx, val) {
    if (l == r) {
      mx[o] = val
      return
    }
    m = (l + r) >> 1
    if (idx <= m) modify(2 * o, l, m, idx, val)
    else modify(2 * o + 1, m + 1, r, idx, val)
    mx[o] = Math.max(mx[o * 2], mx[o * 2 + 1])
  }

  //  l,r对应当前区间, L和R对应query区间
  //  self.modify(1, 1, n, idx, val) 第一个一表示从根节点进去,区间为[1, n], 以及要查询的[L, R]
  function query(o, l, r, L, R) {
    if (L <= l && r <= R) {
      return mx[o]
    }
    res = 0
    m = (l + r) >> 1
    if (L <= m) res = query(o * 2, l, m, L, R)
    if (R > m)
      res = Math.max(
        res,
        query(o * 2 + 1, m + 1, r, L, R)
      )
    return res
  }
  for (let i = 0; i < nums.length; i++) {
    nums[i] += 10001 // 这里就是保证了nums均为正值,然后采用正常逻辑query和modify即可。
    // 查询前i-1项数字中以小于x结尾的最长递增子序列的长度
    res = 1 + query(1, 1, n, 1, nums[i] - 1)
    // 更新结尾在1-nums[i]范围的最长递增子序列
    modify(1, 1, n, nums[i], res)
  }
  return mx[1]
}

动态开点线段树

/**
 * @param {number[]} nums
 * @return {number}
 */
class Node {
  constructor() {
    this.left = this.right = null
    this.val = this.lazy = 0
  }
}
let N = 1e9
class SegTree {
  constructor() {
    this.root = new Node()
  }
  query(l, r) {
    return this._query(this.root, 0, N, l, r)
  }
  _query(node, left, right, l, r) {
    if (l <= left && right <= r) return node.val
    this.pushDown(node)
    let mid = (left + right) >> 1
    let res = 0
    if (l <= mid)
      res = this._query(
        node.left,
        left,
        mid,
        l,
        r
      )
    if (r > mid)
      res = Math.max(
        res,
        this._query(
          node.right,
          mid + 1,
          right,
          l,
          r
        )
      )
    return res
  }
  pushDown(node) {
    if (node.left == null) node.left = new Node()
    if (node.right == null)
      node.right = new Node()
    if (node.lazy == 0) return
    node.left.val = node.lazy
    node.right.val = node.lazy
    node.left.lazy = node.lazy
    node.right.lazy = node.lazy
    node.lazy = 0
  }
  pushUp(node) {
    node.val = Math.max(
      node.left.val,
      node.right.val
    )
  }
  update(l, r, val) {
    this._update(this.root, 0, N, l, r, val)
  }
  _update(node, left, right, l, r, val) {
    if (l <= left && right <= r) {
      node.val = val
      node.lazy = val
      return
    }
    this.pushDown(node)
    let mid = (left + right) >> 1
    if (l <= mid)
      this._update(
        node.left,
        left,
        mid,
        l,
        r,
        val
      )
    if (r > mid)
      this._update(
        node.right,
        mid + 1,
        right,
        l,
        r,
        val
      )
    this.pushUp(node)
  }
}
var lengthOfLIS = function (nums) {
  let tr = new SegTree(),
    res = 0
  // f[i]以数字i为结尾的最长自增子序列的长度
  // 10 9 2 5 3 7 101 18
  /* 递推过程
    从前往后一次遍历数组
    10    
        看前面有没有以小于10结尾的序列,发现没有,返回1
    10 9 
        1 看前面有没有以小于9结尾的序列,发现没有,返回1 
    10 9 2
        看前面有没有以小于2结尾的序列,发现没有,返回1 
    10 9 2 5  
        看前面有没有以小于5结尾的序列,发现2,返回max(f[2]+1,f[5]) = 2
    10 9 2 5 3
        看前面有没有以小于3结尾的序列,发现2,返回max(f[2]+1,f[3]) = 2
    10 9 2 5 3 7
        看前面有没有以小于7结尾的序列,发现2,5,3,尝试将2、5、3结尾的序列和7连接,比较形成的递增子序列的长度,27 257 237,返回最大长度3
    10 9 2 5 3 7 101
        看前面有没有以小于101结尾的序列,发现2,5,3,7尝试将2、5、3、7结尾的序列和7连接,比较形成的递增子序列的长度,返回最大长度4
    10 9 2 5 3 7 101 18
        看前面有没有以小于18结尾的序列,发现2,5,3,7尝试将2、5、3、7结尾的序列和7连接,比较形成的递增子序列的长度,返回最大长度4
    */
  for (let x of nums) {
    x += 10005
    let len = tr.query(0, x - 1) + 1
    tr.update(x, x, len)
    if (len > res) res = len
  }
  return res
}

Post Directory