【宫水三叶的刷题日记】715. Range 模块
题目描述
这是 LeetCode 上的 715. Range 模块 ,难度为 困难。
Tag : 「线段树」、「线段树(动态开点)」
Range
模块是跟踪数字范围的模块。设计一个数据结构来跟踪表示为 半开区间 的范围并查询它们。
半开区间 表示所有 的实数 x
。
实现 RangeModule
类:
RangeModule()
初始化数据结构的对象。
void addRange(int left, int right)
添加 半开区间[left, right)
,跟踪该区间中的每个实数。添加与当前跟踪的数字部分重叠的区间时,应当添加在区间[left, right)
中尚未跟踪的任何数字到该区间中。
boolean queryRange(int left, int right)
只有在当前正在跟踪区间[left, right)
中的每一个实数时,才返回true
,否则返回false
。
void removeRange(int left, int right)
停止跟踪 半开区间[left, right)
中当前正在跟踪的每个实数。
示例 1:
输入["RangeModule", "addRange", "removeRange", "queryRange", "queryRange", "queryRange"][[], [10, 20], [14, 16], [10, 14], [13, 15], [16, 17]]
输出[null, null, null, true, false, true]
解释RangeModule rangeModule = new RangeModule();rangeModule.addRange(10, 20);rangeModule.removeRange(14, 16);rangeModule.queryRange(10, 14); 返回 true (区间 [10, 14) 中的每个数都正在被跟踪)rangeModule.queryRange(13, 15); 返回 false(未跟踪区间 [13, 15) 中像 14, 14.03, 14.17 这样的数字)rangeModule.queryRange(16, 17); 返回 true (尽管执行了删除操作,区间 [16, 17) 中的数字 16 仍然会被跟踪)
提示:
在单个测试用例中,对 addRange
、queryRange
和removeRange
的调用总数不超过 次
基本分析
令 为 addRange
、queryRange
和 removeRange
的调用总数, 为值域大小。
由于值域过大,我们无法直接使用空间大小固定为 的常规线段树,而要采用「动态开点」的方式,其中动态开点的方式有两种 :「需要进行估点的数组实现」和「无须估点的动态指针」。
设计 Node
节点维护什么信息:
ls
和rs
分别指向左右区间子节点(当采用「估点数组」方式时,记录的是左右区间子节点在线段树数组中的下标;在「动态指针」方式时,记录的是左右区间子节点对象);
sum
为记录当前区间有多少个整数被追踪;
add
为懒标记,当add = -1
代表removeRange
懒标记,当add = 1
则代表addRange
懒标记。
线段树(动态开点)- 数组估点
对于常规的线段树实现来说,都是一开始就调用 build
操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 的空间,并且这些空间在起始 build
空树的时候已经锁死。
如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 MLE
问题。
但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到 级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。
动态开点的优势在于,不需要事前构造空树,而是在插入操作 add
和查询操作 query
时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点 而言,我们不能通过 u << 1
和 u << 1 | 1
的固定方式进行访问,而要将节点 的左右节点所在 tr
数组的下标进行存储,分别记为 ls
和 rs
属性。对于 和 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。
由于存在「懒标记」,线段树的插入和查询都是 的,因此我们在单次操作的时候,最多会创建数量级为 的点,因此空间复杂度为 ,而不是 ,而开点数的预估需不能仅仅根据 来进行,还要对常熟进行分析,才能得到准确的点数上界。
动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 的空间,因此盲猜 的常数在 左右,保守一点可以直接估算到 ,因此我们可以估算点数为 ,其中 和 分别代表值域大小和查询次数。
当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( Java
的 128M
可以开到 以上)。
代码:
class RangeModule { class Node { int ls, rs, sum, add; } int N = (int)1e9 + 10, M = 500010, cnt = 1; Node[] tr = new Node[M]; void update(int u, int lc, int rc, int l, int r, int v) { int len = rc - lc + 1; if (l <= lc && rc <= r) { tr[u].sum = v == 1 ? len : 0; tr[u].add = v; return ; } pushdown(u, len); int mid = lc + rc >> 1; if (l <= mid) update(tr[u].ls, lc, mid, l, r, v); if (r > mid) update(tr[u].rs, mid + 1, rc, l, r, v); pushup(u); } int query(int u, int lc, int rc, int l, int r) { if (l <= lc && rc <= r) return tr[u].sum; pushdown(u, rc - lc + 1); int mid = lc + rc >> 1, ans = 0; if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r); if (r > mid) ans += query(tr[u].rs, mid + 1, rc, l, r); return ans; } void pushdown(int u, int len) { if (tr[u] == null) tr[u] = new Node(); if (tr[u].ls == 0) { tr[u].ls = ++cnt; tr[tr[u].ls] = new Node(); } if (tr[u].rs == 0) { tr[u].rs = ++cnt; tr[tr[u].rs] = new Node(); } if (tr[u].add == 0) return; if (tr[u].add == -1) { tr[tr[u].ls].sum = tr[tr[u].rs].sum = 0; } else { tr[tr[u].ls].sum = len - len / 2; tr[tr[u].rs].sum = len / 2; } tr[tr[u].ls].add = tr[tr[u].rs].add = tr[u].add; tr[u].add = 0; } void pushup(int u) { tr[u].sum = tr[tr[u].ls].sum + tr[tr[u].rs].sum; } public void addRange(int left, int right) { update(1, 1, N - 1, left, right - 1, 1); } public boolean queryRange(int left, int right) { return query(1, 1, N - 1, left, right - 1) == right - left; } public void removeRange(int left, int right) { update(1, 1, N - 1, left, right - 1, -1); }}
时间复杂度: addRange
、queryRange
和removeRange
操作复杂度均为
空间复杂度:
线段树(动态开点)- 动态指针
利用「动态指针」实现的「动态开点」可以有效避免数组估点问题,更重要的是可以有效避免 new
大数组的初始化开销,对于 LC 这种还跟你算所有样例总时长的 OJ 来说,在不考虑 static
优化/全局数组优化 的情况下,动态指针的方式要比估点的方式来得好。
代码:
class RangeModule { class Node { Node ls, rs; int sum, add; } int N = (int)1e9 + 10; Node root = new Node(); void update(Node node, int lc, int rc, int l, int r, int v) { int len = rc - lc + 1; if (l <= lc && rc <= r) { node.sum = v == 1 ? len : 0; node.add = v; return ; } pushdown(node, len); int mid = lc + rc >> 1; if (l <= mid) update(node.ls, lc, mid, l, r, v); if (r > mid) update(node.rs, mid + 1, rc, l, r, v); pushup(node); } int query(Node node, int lc, int rc, int l, int r) { if (l <= lc && rc <= r) return node.sum; pushdown(node, rc - lc + 1); int mid = lc + rc >> 1, ans = 0; if (l <= mid) ans = query(node.ls, lc, mid, l, r); if (r > mid) ans += query(node.rs, mid + 1, rc, l, r); return ans; } void pushdown(Node node, int len) { if (node.ls == null) node.ls = new Node(); if (node.rs == null) node.rs = new Node(); if (node.add == 0) return ; int add = node.add; if (add == -1) { node.ls.sum = node.rs.sum = 0; } else { node.ls.sum = len - len / 2; node.rs.sum = len / 2; } node.ls.add = node.rs.add = add; node.add = 0; } void pushup(Node node) { node.sum = node.ls.sum + node.rs.sum; } public void addRange(int left, int right) { update(root, 1, N - 1, left, right - 1, 1); } public boolean queryRange(int left, int right) { return query(root, 1, N - 1, left, right - 1) == right - left; } public void removeRange(int left, int right) { update(root, 1, N - 1, left, right - 1, -1); }}
时间复杂度: addRange
、queryRange
和removeRange
操作复杂度均为
空间复杂度:
最后
这是我们「刷穿 LeetCode」系列文章的第 No.715
篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
本文由 mdnice 多平台发布
【宫水三叶的刷题日记】715. Range 模块相关推荐
- 【宫水三叶的刷题日记】961. 在长度 2N 的数组中找出重复 N 次的元素
题目描述 这是 LeetCode 上的 961. 在长度 2N 的数组中找出重复 N 次的元素 ,难度为 简单. Tag : 「模拟」.「计数」.「构造」.「哈希表」 给你一个整数数组 nums ,该 ...
- 【宫水三叶的刷题日记】1022. 从根到叶的二进制数之和
题目描述 这是 LeetCode 上的 1022. 从根到叶的二进制数之和 ,难度为 简单. Tag : 「DFS」.「BFS」.「二叉树」.「树的遍历」 给出一棵二叉树,其上每个结点的值都是 或 ...
- 【宫水三叶的刷题日记】1037. 有效的回旋镖(简单)
题目描述 这是 LeetCode 上的 1037. 有效的回旋镖 ,难度为 简单. Tag : 「计算几何」.「数学」 给定一个数组 points,其中 表示 X-Y 平面上的一个点,如果这些点构 ...
- 【宫水三叶的刷题日记】497. 非重叠矩形中的随机点(中等)
题目描述 这是 LeetCode 上的 497. 非重叠矩形中的随机点 ,难度为 中等. Tag : 「前缀和」.「二分」.「随机化」 给定一个由非重叠的轴对齐矩形的数组 rects,其中 表示 是第 ...
- 【宫水三叶的刷题日记】732. 我的日程安排表 III
题目描述 这是 LeetCode 上的 「732. 我的日程安排表 III」 ,难度为 「困难」. Tag : 「线段树(动态开点)」.「分块」.「线段树」 当 个日程安排有一些时间上的交叉时(例如 ...
- 【宫水三叶的刷题日记】209. 长度最小的子数组(中等)
题目描述 这是 LeetCode 上的 209. 长度最小的子数组 ,难度为 中等. Tag : 「前缀和」.「二分」 给定一个含有 n 个正整数的数组和一个正整数 target. 找出该数组中满足其 ...
- 【宫水三叶的刷题日记】730. 统计不同回文子序列(困难)
题目描述 这是 LeetCode 上的 730. 统计不同回文子序列 ,难度为 困难. Tag : 「区间 DP」.「动态规划」 给定一个字符串 s,返回 s 中不同的非空「回文子序列」个数 . 通过 ...
- 【宫水三叶的刷题日记】508. 出现次数最多的子树元素和
题目描述 这是 LeetCode 上的 508. 出现次数最多的子树元素和 ,难度为 中等. Tag : 「树的遍历」.「DFS」.「哈希表」 给你一个二叉树的根结点 root,请返回出现次数最多的子 ...
- 【宫水三叶的刷题日记】468. 验证IP地址
题目描述 这是 LeetCode 上的 468. 验证IP地址 ,难度为 中等. Tag : 「模拟」.「双指针」 给定一个字符串 queryIP.如果是有效的 IPv4 地址,返回 "IP ...
最新文章
- 终于把XGBoost总结写出来了!
- Matlab调用函数实现CIC滤波器
- Eclipse将引用了第三方jar包的Java项目打包成jar文件
- python修改xml标签的值_对python修改xml文件的节点值方法详解
- TensorFlow HOWTO 1.4 Softmax 回归
- 总结下2018年,我们归纳了几种将对智能安防产生影响的技术发展趋势
- windows设置开机启动项
- 30 岁成 AI 顶尖科学家,这位阿里副总裁厉害了
- python游戏开发框架_2018年Python主流框架有哪些?最流行的Python框架
- 14. Magento路由分发过程解析(二):Standard路由对象
- 自动阅卷系统/自动阅卷机/网上阅卷
- 怎样花两年时间去面试一个人(转自MIND HACKS)
- 单片机实习音乐播放器的源码
- vmware-nat模式下网络模型
- 美团财报电话会:将专注于创造外卖和到店业务的协同效应
- js获取上一个页面url
- CSS教程:认真学习haslayout
- Vs —— 转移Qt项目后环境问题
- USB-PD快充和QC快充的区别
- 华为 / 小米都看不惯,微信 iOS / 安卓 8.0.30 正式版终于修复转发文件多次保存问题