力扣 3161. 块放置查询:线段树解法(Java 实现)

一、引言

LeetCode 3161 块放置查询问题是区间管理与数据结构应用的经典标杆题,也是面试中考察区间维护、动态更新与高效查询能力的核心考点。这道题的核心是破解 "在数轴上动态放置障碍物,并快速查询可放置指定长度块的最左侧位置" 的问题,而线段树是解决该问题的直观、通用的进阶思路 ------ 它从区间维护的直接实现入手,在时间 / 空间复杂度与实现复杂度上取得了很好的平衡,完美对应了数据结构思维从入门到高阶的成长路径。

本文会系统拆解线段树解法:通过线段树理清区间维护与区间查询的直观逻辑(覆盖全面、易理解,适合入门),并给出可直接运行的 Java 代码,帮你从原理到实现彻底啃下这道题。

二、线段树

2.1 概念

线段树是一种专门用来高效处理「区间问题」的数据结构,能快速完成「区间更新」和「区间查询」操作,时间复杂度基本稳定在 (O(log n))。

2.2 原理

线段树的本质是一棵完全二叉树,它把数组的区间不断「对半拆分」,直到拆成单个元素,再自底向上维护每个区间的信息。

举个简单例子:数组 [1, 3, 5, 7, 9, 11],求区间和

  1. 根节点 :代表整个数组区间 [0, 5],值是区间和 1+3+5+7+9+11=36

  2. 第一层子节点 :把根区间对半拆成 [0, 2](和为 9)和 [3, 5](和为 27)

  3. 第二层子节点 :再对半拆分,比如 [0, 2] 拆成 [0, 1](和 4)和 [2, 2](和 5)

  4. 叶子节点 :最后拆到单个元素,比如 [0,0](值 1)、[1,1](值 3)......

    复制代码
                     [0,5] = 36
                    /         \
             [0,2] = 9       [3,5] = 27
            /      \         /       \
     [0,1] = 4  [2,2] =5  [3,4]=16  [5,5]=11
    /      \

    [0,0]=1 [1,1]=3

  • 查询区间 :比如查询 [1,4] 的和,只需要把树中完全包含在 [1,4] 里的节点值加起来就行,不用遍历每个元素。
  • 更新节点 :比如把 [2,2] 的值改成 10,只需要从叶子节点向上更新所有包含该位置的父节点的和即可。

2.3 代码实现

2.3.1 Node版

java 复制代码
// 线段树节点类
class TreeNode {
    int l, r;       // 当前节点管的区间 [l, r]
    int val;        // 节点值(区间和)
    TreeNode left;  // 左孩子
    TreeNode right; // 右孩子

    public TreeNode(int l, int r) {
        this.l = l;
        this.r = r;
        this.val = 0;
        this.left = null;
        this.right = null;
    }
}

// 线段树(节点版)
public class SegmentTreeNode {
    private TreeNode root;
    private int[] nums;

    // 构造函数
    public SegmentTreeNode(int[] nums) {
        this.nums = nums;
        this.root = build(0, nums.length - 1);
    }

    // 1. 构建线段树(递归)
    private TreeNode build(int l, int r) {
        TreeNode node = new TreeNode(l, r);

        // 叶子节点
        if (l == r) {
            node.val = nums[l];
            return node;
        }

        // 分治
        int mid = (l + r) / 2;
        node.left = build(l, mid);
        node.right = build(mid + 1, r);

        // 合并孩子的值
        node.val = node.left.val + node.right.val;
        return node;
    }

    // 2. 单点更新
    public void update(int idx, int val) {
        update(root, idx, val);
    }

    private void update(TreeNode node, int idx, int val) {
        if (node.l == node.r) {
            node.val = val;
            return;
        }

        if (idx <= node.left.r) {
            update(node.left, idx, val);
        } else {
            update(node.right, idx, val);
        }

        // 更新完孩子,更新自己
        node.val = node.left.val + node.right.val;
    }

    // 3. 区间查询 [L, R]
    public int query(int L, int R) {
        return query(root, L, R);
    }

    private int query(TreeNode node, int L, int R) {
        // 完全包含
        if (L <= node.l && node.r <= R) {
            return node.val;
        }
        // 无交集
        if (node.r < L || node.l > R) {
            return 0;
        }
        // 递归查左右
        return query(node.left, L, R) + query(node.right, L, R);
    }
}

2.3.2 数组版

java 复制代码
public class SegmentTree {
    // 原数组
    private int[] nums;
    // 线段树数组(大小一般开 4 * 数组长度,足够用)
    private int[] tree;

    // 构造函数:传入原数组,自动构建线段树
    public SegmentTree(int[] nums) {
        this.nums = nums;
        int n = nums.length;
        tree = new int[4 * n];
        // 构建:根节点是 1,管理区间 [0, n-1]
        build(1, 0, n - 1);
    }

    // 1. 构建线段树
    // node:当前节点编号
    // l, r:当前节点管理的区间左右边界
    private void build(int node, int l, int r) {
        // 叶子节点:直接等于原数组值
        if (l == r) {
            tree[node] = nums[l];
            return;
        }
        // 分治:拆成左右两半
        int mid = (l + r) / 2;
        build(2 * node, l, mid);       // 左孩子
        build(2 * node + 1, mid + 1, r); // 右孩子
        // 父节点 = 左孩子 + 右孩子
        tree[node] = tree[2 * node] + tree[2 * node + 1];
    }

    // 2. 单点更新:把 index 位置改成 val
    public void update(int index, int val) {
        update(1, 0, nums.length - 1, index, val);
    }

    // 递归更新
    private void update(int node, int l, int r, int idx, int val) {
        // 找到叶子节点,修改
        if (l == r) {
            nums[idx] = val;
            tree[node] = val;
            return;
        }
        int mid = (l + r) / 2;
        // 要更新的点在左子树
        if (idx <= mid) {
            update(2 * node, l, mid, idx, val);
        } 
        // 在右子树
        else {
            update(2 * node + 1, mid + 1, r, idx, val);
        }
        // 更新完孩子,更新父亲
        tree[node] = tree[2 * node] + tree[2 * node + 1];
    }

    // 3. 区间查询:查询 [L, R] 的和
    public int query(int L, int R) {
        return query(1, 0, nums.length - 1, L, R);
    }

    // 递归查询
    private int query(int node, int l, int r, int L, int R) {
        // 当前区间完全在查询范围内,直接返回
        if (L <= l && r <= R) {
            return tree[node];
        }
        // 当前区间和查询区间无交集,返回 0
        if (r < L || l > R) {
            return 0;
        }
        // 有交集,拆分查询
        int mid = (l + r) / 2;
        int leftSum = query(2 * node, l, mid, L, R);
        int rightSum = query(2 * node + 1, mid + 1, r, L, R);
        return leftSum + rightSum;
    }
}

三、线段树解法

3.1 思路

题目简化成一句话:

数轴上不断放障碍物,每次问:能不能找到一段长度为 size 的连续空白?

暴力法:

  • 用一个数组 blocked[] 标记哪些位置有障碍物
  • 操作 1:blocked[x] = true,O (1) 搞定
  • 操作 2:从 0 遍历到 x,找最长的连续空白,看是否 ≥ sz,O (x) 搞定

线段树:

单点更新

  • 每次在位置 x 放障碍物,只需要在线段树里做一次单点更新,把 x 标记为 "已占用"
  • 线段树会从叶子节点向上,一路更新所有父节点维护的「最大连续空白长度」
  • 时间复杂度:O(logM),和暴力的 O (1) 差距不大,但为查询做了铺垫

区间查询

  • 每次查询 [0, x] 内是否有长度 ≥ sz 的空白,只需要查询线段树中 [0, x] 区间的「最大连续空白长度」
  • 线段树直接返回维护好的 maxLen,我们只需要判断 maxLen >= sz 即可
  • 时间复杂度:O(logM),比暴力的 O (x) 快了整整一个数量级

3.2 代码实现(Node版)

java 复制代码
class Solution {
    public List<Boolean> getResults(int[][] queries) {
        int max = 0;
        // 寻找最大的数,方便构建线段树
        for (int[] query : queries){
            max = Math.max(max , query[1]);
        }
        Node root = new Node(0 , max);
        List<Boolean> res = new ArrayList<>();
        for (int[] query : queries){
            if (query[0] == 1){
                update(root , query[1]);
            } else {
                res.add(query(root , query[1] , query[2]) >= query[2]);
            }
        }
        return res;
    }

    public class Node{
        // 左右边界
        int l , r;
        // 最大长度
        int maxLen;
        // 左边,右边连续长度
        int leftLen , rightLen;
        // 区间长度
        int len;
        // 左边界,右边界是否有屏障
        boolean leftFlag , rightFlag;
        // 左,右节点
        Node left , right;

        public Node(int l , int r){
            this.l = l;
            this.r = r;
            // 是r - l ,因为是区间不是点的数量
            len = rightLen = leftLen = maxLen = r - l;
        }
    }

    private void update(Node node , int x){
        if (x > node.r || x < node.l || node.l == node.r){
            // 不在区间的跳过
            return;
        }
        // 更新左右屏障标记
        node.rightFlag = node.rightFlag || node.r == x;
        node.leftFlag = node.leftFlag || node.l == x;
        // 只有本次标记有屏障才可以直接return,之前的不算
        if (node.r == x || node.l == x){
            return;
        }
        create(node);
        Node left = node.left;
        Node right = node.right;
        // 递归更新,找到边界
        update(left , x);
        update(right , x);
        // left.rightFlag = right.leftFlag , 边界都是同一个点
        boolean flag = left.rightFlag;
        // 如果中间有屏障,直接就是left.leftLen , 没有中间屏障并且left整个部分都没有屏障,就可以把right的部分加过来
        node.leftLen = left.leftLen + (left.leftLen == left.len && !flag ? right.leftLen : 0);
        node.rightLen = right.rightLen + (right.rightLen == right.len && !flag ? left.rightLen : 0);
        // 要么是子节点中最长的,要么是没有中间屏障的时候把两个加起来
        node.maxLen = Math.max (
            Math.max(left.maxLen , right.maxLen) , 
            flag ? 0 : left.rightLen + right.leftLen
        );
    }

    private int query(Node node , int x , int sz){
        if (node.l > x || sz > x){
            return 0;
        } else if (node.len == 0 || node.len == 1){
            //这种没有递归的必要
            return node.len;
        } else if (node.r <= x){
            // 已经完全包含在区间,可以直接返回
            return node.maxLen;
        }
        int mid = create(node);
        Node left = node.left;
        Node right = node.right;
        int res = query(left , x , sz);
        // 符合条件的都可以直接提前返回
        if (res >= sz || mid >= x){
            return res;
        }
        res = Math.max(res , query(right , x , sz));
        if (res >= sz){
            return res;
        }
        if (!left.rightFlag){
            // 注意不可以直接加right.leftLen , 还要注意边界的问题
            res = Math.max(res , left.rightLen + Math.min(x - mid , right.leftLen));
        }
        return res;
    }
    

    private int create(Node node){
        // 创造左右节点
        int mid = (node.l + node.r) >>> 1;
        if (node.left == null){
            node.left = new Node(node.l , mid);
        }
        if (node.right == null){
            // 此处不是mid + 1,因为是区间
            node.right = new Node(mid , node.r);
        }
        return mid;
    }
}

3.3 代码实现(数组版)

以上的这种实现需要我们手动去维护屏障,接下来我们可以使用TreeSet去存放屏障,底层也就是二分查找。

java 复制代码
class Solution {
    // 0 - x 内的最大空白块是多大
    int[] arr;

    public List<Boolean> getResults(int[][] queries) {
        int max = 0;
        for (int[] query : queries){
            max = Math.max(max , query[1]);
        }
        this.arr = new int[max << 2];
        // 放入两个哨兵,左右边界
        TreeSet<Integer> set = new TreeSet<>(List.of(0 , max));
        List<Boolean> res = new ArrayList<>();
        for (int[] query : queries){
            int x = query[1];
            // 找到左侧的上一个障碍物(因为arr存放的是距离上一个障碍物)
            int pre = set.floor(x - 1);
            if (query[0] == 1){
                // 找到右侧下一个屏障
                int next = set.ceiling(x);
                set.add(query[1]);
                // 1是根节点,左右范围是0-max
                update(1 , 0 , max , x , x - pre);
                update(1 , 0 , max , next , next - x);
            } else {
                // 长度要么是0-上一次屏障,要么是当前x - 上一次屏障(这样就包含了没有初始化数组的情况)
                res.add(Math.max(query(1 , 0 , max , pre) , x - pre) >= query[2]);
            }
        }
        return res;
    }

    private void update(int father , int left , int right , int x , int val){
        if (left == right){
            arr[father] = val;
            return;
        }
        int mid = (left + right) >>> 1;
        if (x <= mid){
            update(2 * father , left , mid , x , val);
        } else {
            update(2 * father + 1 , mid + 1 , right , x , val);
        }
        arr[father] = Math.max(arr[2 * father] , arr[2 * father + 1]);
    }

    private int query(int father , int left , int right , int x){
        if (right <= x){
            return arr[father];
        }
        int mid = (left + right) >>> 1;
        if (x <= mid){
            return query(2 * father , left , mid , x);
        }
        // 这里不需要拼接左子树的右边和右子树的左边
        // 因为此处存放的是距离上一个障碍物,而不是距离子树左子树的右边界,所以这种情况已经包含了
        return Math.max(arr[2 * father] , query(2 * father + 1 , mid + 1 , right , x));
    }
}

四、其他解法

力扣 3161. 块放置查询:树状数组+ 并查集解法(Java 实现)-CSDN博客

相关推荐
天天进步20151 小时前
Python全栈项目实战:从零构建校园心理健康咨询平台
面试·职场和发展
我命由我123451 小时前
Android 开发问题:MlKitException: An internal error occurred during initialization.
android·java·java-ee·android jetpack·android-studio·androidx·android runtime
CS创新实验室2 小时前
从顺序表到动态数组:数据结构的永恒基石与现代语言的优雅封装
数据结构·算法
888CC++2 小时前
java 并发编程
java·开发语言·python
无风听海2 小时前
JSON Web Token(JWT)完全指南
java·前端·json
Black蜡笔小新2 小时前
自动化AI算法训练服务器DLTM训推一体化平台助力农业生产管理实现安全智能化
人工智能·算法·自动化
JAVA社区3 小时前
Java高级全套教程(十一)—— Kubernetes 超详细企业级实战详解
java·运维·微服务·容器·面试·kubernetes
8Qi83 小时前
LeetCode 23. 合并 K 个升序链表 —— 小顶堆(PriorityQueue)
数据结构·算法·leetcode·链表·
kyriewen3 小时前
大厂面试新规:不会用AI编程,直接挂
前端·面试·ai编程