luogu#P6019. [Ynoi2010] Brodal queue
[Ynoi2010] Brodal queue
题目背景
题目背景和题意无关,可以跳过
1.前言:
Brodal queue 在 1996 年由 Brodal 提出,是第一个满足每个操作都 worst case 而且达到了基于比较的堆的下界的数据结构
这里给出的 Brodal queue 是一个小根堆
该数据结构的一些特性:
-
维护了两棵树。
-
Brodal queue 是一种 violation heap,即允许存在一些节点不满足堆性质。
-
实现真的很复杂,常数真的很大。
2.一些记号:
Brodal queue 维护了两棵树 , ,他们的根是 和 。
定义:
: 的父亲节点,默认 且 。
:和 相关的一个权值。
: 的孩子中满足其 为 的个数。
: 中 为 的元素个数。
和 列表:维护所有违反堆性质的节点。
3.概述:
每个曾经作为 的根的点都维护 和 ,我们用 表示 节点的 集合, 表示 节点的 集合。
维护的是 的节点, 维护的是 的节点,这个是对插入当时的情况成立的,也就是说在经过结构修改操作之后不一定还满足这个性质。
是只加不删的, 是需要维护平衡的,所以只需要保证 的 中的点的 。
有以下的一些性质:
的性质:
S1. 叶子节点的 为 。
S2. 。
S3. 如果 ,则 。
S4. 只可能为 中的元素。
S5. 当 时 。
解释:
S1. 边界定义。
S2. 构成大根堆。
S3. 一个点有至少两个孩子的 是其 ,所以 的子树大小关于 的 至少是指数级的,所以 最多是 的。
S4. 有 上界,非 ,而总共有 种 ,所以每个节点的度数都是 的。
S5. 要么 比 小,要么 为空。
和 列表的性质:
O1. 是所有元素中的最小元。
O2. 如果 在 或者 中,则 ,即这个元素在被插入列表时是违背了堆性质的。
O3. 如果 ,那么存在一个 使得 , 属于 或 ,即所有违背堆性质的节点都在某个节点的 或 列表中。
O4. 对于所有 ,有 。
O5. 记 , 则 , 是一个常数。
解释:
O1. 我们要 求出最小值,所以规定了最小元是 。
O2. 被插入的时候是被插到 或者 中。
O3. 违反堆性质的点一定在另一个点的 或者 集合中。
O4. 有常数上界,所以 和 列表的大小是 的。
O5. 列表中的 有一个阶梯的下界。
对于 , 的额外性质:
R1. 对 ,有 。
R2. ,和前面提到的是同一个 。
R3. 如果 属于 则 。
解释:
R1. 对于 ,,每个 的孩子至少有 个。
R2. 的大小 有 的上界。
R3. 属于 的点比 小。
R2+O5可以推出如果 的 增大 ,我们就可以增加 个大的违反堆性质的节点,把他们加入 ,并且不违反 。
每次 DECREASEKEY 时 ,我们直接加一个新的违反堆性质的点到 或者。
为了避免有太多的违反堆性质的节点,我们递增地做两种不同的变换,主要为了维护 R2 和 O4 性质。
第一种:把 的儿子移动进入 ,变成 的孩子,使得 变大。
第二种:通过把 个 为 的违反堆性质的节点换成了 个 为 的违反堆性质的节点,从而减少 的大小。
Note:我们这里用到了很多可变长数组,默认可变长数组是严格 的,这个是平凡的所以不详细讲怎么实现了。
这里给出了一个作者称为 guide 的数据结构的实现。
4.Guide Data Structure:
用途:维护 , 性质,即对 的上界进行维护,大概是一个维护 进位的东西。
抽象出这部分要维护的东西,也就是我们这里讲的 guide 数据结构是要维护什么:
维护:一个长为 的 int 数组, (这里我们从右往左写序列)。
需要满足: , 是预设的一个阈值常数。
我们只能在这个数组上实现 REDUCE(i) 的操作,即 至少减少 , 至多增加 (实际上这里我们 只能减少 或者 , 只能增加 或者 )。
每次可能发生对一个任意位置 的 的 或者 操作,每次操作之后我们被允许通过 次 REDUCE 操作,使得我们需要维护的数组仍旧满足性质。
guide 干的事情就是 guide(向导) ,也就是说告诉我们要做什么样的 REDUCE 操作使得这个数组仍旧满足性质。
定义 是 ,当 碰到进入 的范围之前我们不考虑对其进行调整。
不妨设 ,我们对 维护这个 guide 即可。
把原序列分成一些块,考虑形如 这样的连续段,不在段内的元素只可能是单独的 或 。
对每个块我们新建一个节点,然后段内的每个元素都指向这个节点。
每个节点上记录块的开头位置。
不在块内的节点直接指向一个 null。
这里我们多个点共用一个 null ,存在多个 null。
有两个重要的性质:
-
给一个位置 ,我们可以最坏复杂度 找到 所属于的块内最左边的元素, 并且——
-
我们可以最坏复杂度 销毁掉一个给定的块,直接把那个存有值的点改成 null 即可。
这里写一下变换是如何实现的,由于是按照自己的理解写的,所以可能和原论文 有出入。
注意到我们可以 拆掉一个块。
前驱不在块内:
case1:
0 0
0 1
trivial
case2:
0 1
0 2
1 1
trivial
case3:
0 [2 1* 0]
0 [3 1* 0]
1 [1 1* 0]
拆掉这个块
1 1 1* 0
case4:
1 0
1 1
trivial
case5:
1 1
1 2
[2 0]
trivial
case6:
1 [2 1* 0]
1 [3 1* 0]
2 [1 1* 0]
[2 1 1* 0]
前驱是块尾:
case7:
[2 1* 0] 0
[2 1* 0] 1
trivial
case8:
[2 1* 0] 1
[2 1* 0] 2
[2 1* 1] 0
[2 1* 1 0]
case9:
[2 1* 0] [2 1* 0]
[2 1* 0] [3 1* 0]
[2 1* 1] [1 1* 0]
拆后面的块
[2 1* 1] 1 1* 0
拆前面的块
2 1* 1 1 1* 0
递归成块外的1加1的情况case 2,5,8
前驱在块内:
case10:
[2 1* 0]
[2 1* 1]
拆块
2 1* 1
递归成块外的1加1的情况case 2,5,8
case11:
[2 1* 1 1* 0]
[2 1* 2 1* 0]
拆部分块,通过把指针指到第二个2的位置
2 1* [2 1* 0]
递归成块外的1加1的情况case 2,5,8
注意到这里 是单调向右走的,除了每次操作可能可以往左边走一格,所以把一个块端点往右移动一段,或者向左移动 个位置是可行的。
加个内存池,我们需要的空间不超过 。
(*可能可以四毛子优化一下?)
5.link 和 delink
这个是两个基本操作,用于调节 Brodal queue。
link:
假设我们有 个节点 ,这三个节点不是根,且有相同的 。
经过 次比较之后,不妨我们设 是这三个中最小的。
然后我们可以把 , 变成 的最左儿子(因为是链表,只能在头插入),并且 的 自增 。
然后 和 都不是违反堆性质的节点,并且 仍然满足所有 S1-S5 和 O1-O5 的性质。
delink:
对一个点 ,如果 是 或者 ,那么把 的 为 的孩子和父亲的边断开,把它"切出来",然后 变成剩余孩子中的 ,这里切出来之后怎么办需要特殊说明。
如果 ,那么把 个 为 的孩子切出来。
delink 一个根节点的 为 的树总是会得到 或者 个根节点的 为 的树,和一个额外的根节点的 的树(这个树根节点的 可以是 的任意数)。
考虑如何维护 的孩子,使其满足R1( 对于 中每个 有个孩子)。
对 中每个 的孩子个数,使用两个guide,一个处理个数在 的孩子保证其个数 ,一个处理个数在 的孩子保证其个数 。
对于 为 和 的孩子需要特判处理。
对 的孩子用两个 guide 来维护,设其为 guide1 和 guide2。
guide1 维护的是 在 中的孩子,guide2 维护的是 在 中的孩子,这里我们令 ,是一个常数。
我们用 guide 中的元素 代指这个 guide 里面 为 的节点个数。
link 操作会让 guide1 里面的一个元素 减少 , 另一个元素 增加 ,注意到这里我们维护的是一个上界的形式,所以减少 和减少 是类似的。
delink 操作会让 guide2 里面的一个元素 减少 , 增加 或 , 还会多出一个 在 中的元素,这里维护的是下界形式,所以只用考虑 。
注:这里其实 会比 容易破坏下界, 会比 容易破坏上界,把 改成 ,或者加一些特判之后这个问题可以被解决,所以不专门讨论这两种情况了。
注:这里原论文给的界是 ,但是我们不知道如何达成,所以在这里讲 的情况。
新增孩子:
如果导致同 的孩子数 ,则从处理上界的 guide 得到需要执行的 次 reduce 操作(这导致了 guide 的实现的微小变化),对应于用 link 合并 个 为 的孩子,得到 个 为 的孩子。
注意此时孩子数减少 后变成 ,不影响处理下界的 guide。
如果这导致 为 , 的孩子过多,同样用 link 操作进行调整,这时就可能需要增加 的 了。
这里由于我们每次都是把"切出来"的节点变成 的孩子,所以不产生额外的违反堆性质的节点。
删除孩子:
类似于新增孩子,但此时 reduce 对应于 delink 操作(这里只考虑 的树变为 的 或 棵树),delink 产生的额外的 rank 不固定的树会在删除孩子完成之后被新增为 的孩子。
关于这里的常数 的解释:
我们考虑一个 guide 中维护的元素到了和这个 guide 的界差 的情况才需要 REDUCE ,不然不需要。
对于 guide1,这个界限就是 ,对于 guide2,这个界限就是 。
然后我们要让这次 REDUCE 之后不会导致另一个 guide 需要调整。
所以 ,,可以解出 ,故取 。
这里和原论文的 不一样,但也只是常数差别,不会对复杂度和正确性产生影响,所以不仔细讨论这个了。
对于 的新增/删除孩子,情况和 类似,不同之处是 delink 产生了 个新的破坏堆性质的点,这部分的详细内容会在后面提到。
定义 集合为 集合和 集合的并集,即潜在的违反堆性质的点的集合。
设计一个变换,用于减少 :
这一段是 的 guide 的 REDUCE 的方法,这个变换将 减少了至少 :
假设 是潜在的违反堆性质的点,满足 ,且 不是根或根的孩子。
首先检查 是否满足堆性质,若满足则从 或 列表中移除,否则:
若 不是兄弟:
不失一般性,设 ,交换 所在子树和 的某个 的兄弟 (由 S4 性质可知这样的 一定存在)所在子树(此操作不会增加 , 仍是 , 可能从 变为非 也可能状态不变),之后可设 。
若存在 的第三个兄弟,则将 移除并新增为 的孩子,这样 的 的儿子减少了一个,而且至少为 个,不违反 S4 性质。
若不存在 的第三个兄弟,则将 移除并新增为 的孩子,这样 的 的儿子个数变成 了,不违反 S4 性质。
若 则还需要移除 ,更新 的 ,并新增 为 的孩子, 子树原先的位置被从 移除的一个 的子树代替。
(这可能增加一个违反堆性质的点(需要加入 中),但同时 由于父亲变成了 ,所以将符合堆性质),这里还需要对 的 guide 进行一个 的位置 的操作。
所以至少减少 个 中的点,至多增加 个点到 中。
(由于 S2 性质保证了等 替换时,被替换的点和用于替换的点没有祖先关系,因此不会成环)
避免过多的违反堆性质的点出现:
当新增一个违反堆性质的点 时,若 则将其加入到 中,否则加入到 中。
对 也开个 guide,里面第 个元素就是 。
向 加点时,插入一个点 时,让 guide 中的第 个元素 ,然后通过 guide 得到需要进行的 reduce(k) 操作。
在 ,且其中至少 个不是 的孩子时,执行上述变换减少 。
若有超过 个是 的孩子,则将多出的点切下并新增为 的孩子(这不影响 的 guide)。
每次进行优先队列操作时:
若 非空,则通过将 个孩子从 移到 ,使 的 增加至少 ,这使得我们可以支付 新增的不超过 个 。
具体地,若 ,则把 的 最大的孩子切下并新增为 的孩子,最终当 没有孩子的时候,把 新增为 的孩子。
若 ,则切下 的一个 为 的孩子,将其delink并新增为 的孩子。
若 为空,则 不会有新增的 。
6.抽象数据结构
对于一个抽象数据结构——优先队列,我们可以使用 Brodal queue 实现,具体进行的操作为:
MakeQueue:T1=T2=null
FindMin(Q): return t1
Insert(Q,e): 转为Meld(Q,{T1=e,T2=null})
Meld(Q1,Q2):
不失一般性,设 $Q1.t1<Q2.t1$。
若 Q1.T1 的 rank 最大,则把其余的 $3$ 棵树新增为 Q1.t1 的孩子,Q1.T2 设为空,这个过程中不产生新的 $pv$
否则让 Q1.T2,Q2.T2 中 rank 最大的树成为新的 Q1.T2,其余的 $2$ 棵树新增为 Q1.t2 的孩子。
解释:如果 所在的那个树的 是这四个里面最大的,那就把其他几个树都插入到 所在的那个树的孩子里面,新的树里面 为空
否则 最大的那个树成为新的 ,除了 所在的树都插入这个的孩子里面, 所在的树成为
DecreaseKey(Q,e,e'):
修改权值后检查是否违反堆性质
如果违反堆性质,则将其加入pv中,此时pv大小+1
然后按前文避免过多的违反堆性质的点出现的方法来让pv减少至少1,使得pv大
小平衡
DeleteMin(Q):
将 t2 的孩子(O(log n) 个)全部切下,新增为 t1 的孩子;将 t2 变为 t1 的孩子;
删除 t1 得到 t1 的孩子,总共有 O(log n) 个。
遍历 t1 的孩子,V(t1) 和 W(t1),从而得到新的最小值t1'
若 t1' 不是 t1 的孩子,则与等 rank 的孩子交换,这可能产生 O(1) 个 pv。
然后将 V(t1),V(t1'),W(t1),W(t1') 合并为 W(t1'):
先将 V(t1') 置为空,然后使用变换令 w(t1',i)<=1,最后使 t1' 成为新的 t1
Delete(Q),e: 转为DecreaseKey(Q,e,-inf),DeleteMin(Q)
题目描述
给定一个长为 的序列 ,每个位置有一种颜色,有 次操作,支持:
1 l r x
:区间 的数都变成 。
2 l r
:查询有多少二元组 满足 ,且 。
本题强制在线,每次的 需要 上(上次答案 ),也就是说使用 unsigned int
数据类型存储上次的答案即可,如果之前没有询问,则上次答案为 。这里输出的答案不对 取模。
输入格式
第一行两个整数 。
第二行 个整数表示序列 。
之后 行,每行形如 1 l r x
或 2 l r
,表示上述的操作。
输出格式
对于每个 操作,输出一行一个整数表示答案。
10 12
6 9 9 4 7 8 10 4 9 2
2 1 4
1 0 5 0
2 3 6
2 10 9
1 7 9 2
2 7 9
1 2 7 1
1 2 11 4
2 6 10
1 3 12 0
1 14 14 15
2 7 12
1
3
0
3
6
16
提示
Idea:nzhtl1477,Solution:nzhtl1477&ccz181078,Code:ccz181078,Data:nzhtl1477
注意:本题采用捆绑测试,只有当你通过一个 subtask 中的所有测试点后,你才能拿到这个 subtask 的分数。
对于其中 的数据,为样例 1。
对于另外 的数据,没有修改操作。
对于另外 的数据,。
对于另外 的数据,每次修改的区间长度不超过 。
对于另外 的数据,保证数据随机。
对于 的数据,,,。