当前位置:网站首页>【区间和专题の前缀和】线段树(动态开点)运用题
【区间和专题の前缀和】线段树(动态开点)运用题
2022-06-21 17:50:00 【宫水三叶的刷题日记】
题目描述
这是 LeetCode 上的「732. 我的日程安排表 III」,难度为「困难」。
Tag : 「线段树(动态开点)」、「分块」、「线段树」
当
个日程安排有一些时间上的交叉时(例如
个日程安排都在同一时间内),就会产生
次预订。
给你一些日程安排
,请你在每个日程安排添加后,返回一个整数
,表示所有先前日程安排会产生的最大
次预订。
实现一个 MyCalendarThree 类来存放你的日程安排,你可以一直添加新的日程安排。
MyCalendarThree()初始化对象。int book(int start, int end)返回一个整数
,表示日历中存在的
次预订的最大值。
示例:
输入:
["MyCalendarThree", "book", "book", "book", "book", "book", "book"]
[[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]]
输出:
[null, 1, 1, 2, 3, 3, 3]
解释:
MyCalendarThree myCalendarThree = new MyCalendarThree();
myCalendarThree.book(10, 20); // 返回 1 ,第一个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。
myCalendarThree.book(50, 60); // 返回 1 ,第二个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。
myCalendarThree.book(10, 40); // 返回 2 ,第三个日程安排 [10, 40) 与第一个日程安排相交,所以最大 k 次预订是 2 次预订。
myCalendarThree.book(5, 15); // 返回 3 ,剩下的日程安排的最大 k 次预订是 3 次预订。
myCalendarThree.book(5, 10); // 返回 3
myCalendarThree.book(25, 55); // 返回 3
提示:
- 每个测试用例,调用
book函数最多不超过
次
线段树(动态开点)
和 731. 我的日程安排表 II 几乎完全一致,只需要将对「线段树」所维护的节点信息进行调整即可。
线段树维护的节点信息包括:
ls/rs: 分别代表当前节点的左右子节点在线段树数组tr中的下标;add: 懒标记;max: 为当前区间的最大值。
对于常规的线段树实现来说,都是一开始就调用 build 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要
的空间,并且这些空间在起始 build 空树的时候已经锁死。
如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 MLE 问题。
但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到
级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。
动态开点的优势在于,不需要事前构造空树,而是在插入操作 add 和查询操作 query 时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点
而言,我们不能通过 u << 1 和 u << 1 | 1 的固定方式进行访问,而要将节点
的左右节点所在 tr 数组的下标进行存储,分别记为 ls 和 rs 属性。对于
和
则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。
由于存在「懒标记」,线段树的插入和查询都是
的,因此我们在单次操作的时候,最多会创建数量级为
的点,因此空间复杂度为
,而不是
,而开点数的预估需不能仅仅根据
来进行,还要对常熟进行分析,才能得到准确的点数上界。
动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到
的空间,因此盲猜
的常数在
左右,保守一点可以直接估算到
,因此我们可以估算点数为
,其中
和
分别代表值域大小和查询次数。
当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( Java 的 128M 可以开到
以上)。
代码:
class MyCalendarThree {
class Node {
int ls, rs, add, max;
}
int N = (int)1e9, M = 4 * 400 * 20, cnt = 1;
Node[] tr = new Node[M];
void update(int u, int lc, int rc, int l, int r, int v) {
if (l <= lc && rc <= r) {
tr[u].add += v;
tr[u].max += v;
return ;
}
lazyCreate(u);
pushdown(u);
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].max;
lazyCreate(u);
pushdown(u);
int mid = lc + rc >> 1, ans = 0;
if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r);
if (r > mid) ans = Math.max(ans, query(tr[u].rs, mid + 1, rc, l, r));
return ans;
}
void lazyCreate(int u) {
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();
}
}
void pushdown(int u) {
tr[tr[u].ls].add += tr[u].add; tr[tr[u].rs].add += tr[u].add;
tr[tr[u].ls].max += tr[u].add; tr[tr[u].rs].max += tr[u].add;
tr[u].add = 0;
}
void pushup(int u) {
tr[u].max = Math.max(tr[tr[u].ls].max, tr[tr[u].rs].max);
}
public int book(int start, int end) {
update(1, 1, N + 1, start + 1, end, 1);
return query(1, 1, N + 1, 1, N + 1);
}
}
- 时间复杂度:令
为值域大小,本题固定为
,线段树的查询和增加复杂度均为
- 空间复杂度:令询问数量为
,复杂度为
带懒标记的分块(TLE)
实际上,还存在另外一种十分值得学习的算法:分块。
但该做法被奇怪的测评机制卡掉,是给每个样例都定了执行用时吗 ?
旧题解没有这种做法,今天补充的,我们可以大概讲讲「分块」算法是如何解决涉及「区间修改」,也就是带懒标记的问题。
对于本题,我们设定两个块数组 max 和 add,其中 max[idx] 含义为块编号为 idx 的连续区间的最大重复值,而 add[idx] 代表块编号的懒标记,同时使用「哈希表」记录下某些具体位置的覆盖次数。
然后我们考虑如何指定块大小,设定一个合理的块大小是减少运算量的关键。
通常情况下,我们会设定块大小为
,从而确保块内最多不超过
个元素,同时块的总个数也不超过
,即无论我们是块内暴力,还是操作多个块,复杂度均为
。因此对于本题而言,设定块大小为
(经试验,调成
能够通过较多样例),较为合适。
对于涉及「区间修改」的分块算法,我们需要在每次修改和查询前,先将懒标记下传到区间的每个元素中。
然后考虑如何处理「块内」和「块间」操作:
- 块内操作:
- 块内更新:直接操作
map进行累加,同时使用更新后的map来维护相应的max[idx]; - 块内查询:直接查询
map即可;
- 块内更新:直接操作
- 块间操作:
- 块间更新:直接更新懒标记
add[idx]即可,同时由于我们是对整个区间进行累加操作,因此最大值必然也会同步被累加,因此同步更新max[idx]; - 块间查询:直接查询
max[idx]即可。
- 块间更新:直接更新懒标记
代码:
class MyCalendarThree {
static int N = (int)1e9 + 10, M = 1200010, SZ = N / M + 10;
static int[] max = new int[M], add = new int[M];
static Map<Integer, Integer> map = new HashMap<>();
int minv = -1, maxv = -1;
int getIdx(int x) {
return x / SZ;
}
void add(int l, int r) {
pushdown(l); pushdown(r);
if (getIdx(l) == getIdx(r)) {
for (int k = l; k <= r; k++) {
map.put(k, map.getOrDefault(k, 0) + 1);
max[getIdx(k)] = Math.max(max[getIdx(k)], map.get(k));
}
} else {
int i = l, j = r;
while (getIdx(i) == getIdx(l)) {
map.put(i, map.getOrDefault(i, 0) + 1);
max[getIdx(i)] = Math.max(max[getIdx(i)], map.get(i));
i++;
}
while (getIdx(j) == getIdx(r)) {
map.put(j, map.getOrDefault(j, 0) + 1);
max[getIdx(j)] = Math.max(max[getIdx(j)], map.get(j));
j--;
}
for (int k = getIdx(i); k <= getIdx(j); k++) {
max[k]++; add[k]++;
}
}
}
int query(int l, int r) {
pushdown(l); pushdown(r);
int ans = 0;
if (getIdx(l) == getIdx(r)) {
for (int k = l; k <= r; k++) ans = Math.max(ans, map.getOrDefault(k, 0));
} else {
int i = l, j = r;
while (getIdx(i) == getIdx(l)) ans = Math.max(ans, map.getOrDefault(i++, 0));
while (getIdx(j) == getIdx(r)) ans = Math.max(ans, map.getOrDefault(j--, 0));
for (int k = getIdx(i); k <= getIdx(j); k++) ans = Math.max(ans, max[k]);
}
return ans;
}
void pushdown(int x) {
int idx = getIdx(x);
if (add[idx] == 0) return ;
int i = x, j = x + 1;
while (getIdx(i) == idx) map.put(i, map.getOrDefault(i--, 0) + add[idx]);
while (getIdx(j) == idx) map.put(j, map.getOrDefault(j++, 0) + add[idx]);
add[idx] = 0;
}
public MyCalendarThree() {
Arrays.fill(max, 0);
Arrays.fill(add, 0);
map.clear();
}
public int book(int start, int end) {
add(start + 1, end);
minv = minv == -1 ? start : Math.min(minv, start);
maxv = maxv == -1 ? end + 1 : Math.max(maxv, end + 1);
return query(minv, maxv);
}
}
- 时间复杂度:令
为块大小,复杂度为
- 空间复杂度:
,其中
为最大调用次数
最后
这是我们「刷穿 LeetCode」系列文章的第 No.732 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。
边栏推荐
猜你喜欢

Unity3D-后期处理 Post-process Volume Profile

MarkDown初级语法一文精通,兼容MarkText

Foreign capital staged a "successful flight" and domestic capital took over the offer. Is New Oriental online "square"?

Insert class collation

R language bug? report errors? As for the outcome of sub variables 0 and 1, the original content of the outcome variable is changed through the process of factor and numeric?

文末送书 | 李航老师新作!机器学习经典著作《统计学习方法》全新升级

The main data products of China's two Fengyun meteorological "new stars" will be open and shared with global users

Must the database primary key be self incremented? What scenarios do not suggest self augmentation?

Servlet learning (II)

【一起上水硕系列】Day One
随机推荐
Internet communication process
Dao与实体类的封装
What is an SSL certificate and what are the benefits of having an SSL certificate?
Leetcode (210) - Schedule II
ThreeJS飞机地球3D场景动画
Unity3D-后期处理 Post-process Volume Profile
动态加载资源之AssetDatabase
一篇文章彻底学会画数据流图
從“村辦企業”到“百億集團”,紅星實業何以完成“蝶變”?
插入类排序法
数据库主键一定要自增吗?有哪些场景不建议自增?
图像分类、AI 与全自动性能测试
jvm造轮子
Equals null pointer exception
Gartner 网络研讨会 “九问数字化转型” 会后感
How to apply for SSL certificate using IP
Get some eth from the faucet of the ropsten test network
homeassistant addons
8. get directory function / get file function -dir / -notdir
GOF mode-03-behavioral mode (bottom)