LOJ#120. 持久化序列
鉴于基本上没怎么考过可持久化平衡树,可能很久都没写了。。所以放了一道模板题来练手。福利题,可自行跳过
值得一提的是可持久化非旋Treap在split时新建点之后,左树的右链和右树的左链都是新建的点,因此merge这两棵树时不需要再新建点(如果不需要分裂的版本的话)。(好像这是基操?)
按版本树dfs用可撤销数据结构(splay)也可以过
UOJ#173. 【WC2016】鏖战表达式
求表达式的值,有个显然的想法是每次找到优先级最小的运算符,然后分成两部分递归求解。
需要维护一棵运算树,发现其实就是一棵treap,只不过堆的关键字换成了运算符的优先级。
非旋treap可以可持久化,但是此时merge两个根优先级不同的树时谁在上面是定的,但是注意到运算符个数
k
k
k 比较小,所以确定的树高
k
k
k 只有 100。
于是我们可以在相同优先级的合并上做文章,当合并两颗优先级相同的树时,按照子树大小加权随机哪个在上面即可。可以这么理解,假设我们想维护的是个小根堆,那么最小值在左子树的概率就是
s
i
z
l
s
i
z
l
+
s
i
z
r
\frac {siz_l}{siz_l+siz_r}
sizl+sizrsizl。
于是树高期望就是
k
+
log
n
k+\log n
k+logn
(也许优先级相同的时候可以比较第二关键字?感觉应该可以过)
CF702F T-Shirts
又是一道福利题。。。。
直接对每个人考虑的话因为
c
i
c_i
ci 没有单调性所以不好做。
把物品排序之后按顺序考虑哪些人会选这个物品即可,人用非旋treap维护,打标记分裂合并。
详细题解
好像跟可持久化没什么关系??。。
LOJ#6203. 可持久化队列
带log的做法:
维护版本树,发现pop_front就是去掉树上最前面的祖先,于是记录每个点对应队列的长度,每次删除时倍增找到当前队列长度对应的祖先的值就可以了。
线性做法:
见LOJ讨论版
LOJ#3328. 「SNOI2020」水池
联考第二场T3,不过好像没人补题?,其实并不难
PS:原数据的答案文件是错的,LOJ上的数据是换过了的。
0操作:添加水位到 h h h,找到左右第一个挡板高度 ≥ h \ge h ≥h 的位置,中间的水位全部改为 h h h,线段树二分+懒标记。
1操作:把
x
x
x 位置的水放空,找到左右第一个挡板高度
≥
d
x
\ge d_x
≥dx 的位置,中间的部分,在
x
x
x 左边的,水位会更新为到
x
x
x 的挡板高度的后缀最大值;在
x
x
x 右边的,水位会更新为到
x
x
x 的挡板高度的前缀最大值。
因为只需要单点查询,所以只用维护一个标记组
{
t
y
p
e
,
m
x
}
\{type,mx\}
{type,mx},表示当前是哪种赋值操作,以及该操作对这个区间贡献的前缀/后缀最大值,下传的时候根据区间最高的挡板高度更新
m
x
mx
mx 即可。
(要是区间查询水位之和是不是还要李超线段树啊…)
新标记会覆盖以前的标记,所以优先级什么的也不用考虑。
2操作:升高单点的挡板,简单修改。
3操作:查询水位,一路下方标记,返回底部标记维护的
m
x
mx
mx 即可。
可持久化注意下放懒标记的时候新建(复制)节点,在外面把根复制一下。
HDU6087 Rikka with Sequence
维护一个序列 a a a,初始值为 { b } \{b\} {b},要求支持:
- 区间求和
- for(int i=l;i<=r;i++) a[i]=a[i-k] \texttt{for(int i=l;i<=r;i++)~a[i]=a[i-k]} for(int i=l;i<=r;i++) a[i]=a[i-k]
- for(int i=l;i<=r;i++) a[i]=b[i] \texttt{for(int i=l;i<=r;i++) a[i]=b[i]} for(int i=l;i<=r;i++) a[i]=b[i]
要分裂,区间复制,采用可持久化Treap。
对于操作3,分裂出初始版本的
l
,
r
l,r
l,r 区间,当前版本的
1
,
l
−
1
1,l-1
1,l−1 以及
r
+
1
,
n
r+1,n
r+1,n,合并即可,因为合并之后当前版本里面会连接初始版本的值,所以要采用可持久化。
对于操作2,可以发现是将
a
[
l
−
k
,
l
−
1
]
a[l-k,l-1]
a[l−k,l−1] 复制多次到
a
[
l
+
t
k
,
l
+
(
t
+
1
)
k
)
a[l+tk,l+(t+1)k)
a[l+tk,l+(t+1)k),以及剩余一点余数。直接暴力复制不行,可以倍增
m
e
r
g
e
merge
merge,类似于快速幂的操作,复杂度是
O
(
log
2
n
)
O(\log^2n)
O(log2n) 的。
因为涉及到可能不停地复制一个串以及可持久化,如果Treap仍然采用随机权值做键值的话会出现很多重复权值,这样平衡性就无法保证了。应该在 m e r g e merge merge 时按照子树大小加权随机哪个在上面。
由于这个题卡空间,而又只用维护当前版本和初始版本,容易发现中间有很多新建的节点是不需要再保留的(实际上只需要 2 n 2n 2n 个点),定期重构即可。如果多余点数达到 x x x 重构,时间复杂度是 O ( n 2 l o g 2 x ) O(\frac {n^2log^2}x) O(xn2log2)。重构写法有几种,可以参考这两份代码:blog1,blog2
洛谷P2483 【模板】k短路 / [SDOI2010]魔法猪学院(可持久化可并堆)
UOJ#46. 【清华集训2014】玄学
虽然是假的可持久化,但是是真的nb
序列
{
x
n
}
\{x_n\}
{xn},
q
q
q 次操作,给定模数
m
m
m,每个操作是把
i
∈
[
l
,
r
]
i\in[l,r]
i∈[l,r] 的
x
i
x_i
xi 变为
(
a
x
i
+
b
)
%
m
(ax_i+b)\%m
(axi+b)%m
操作中间穿插一些询问,假设执行第
l
l
l 到第
r
r
r 个操作,第
k
k
k 个位置的值会变成多少。
强制在线。
显然对同一区间的两个操作是可以合并的,
(
a
,
b
)
+
(
c
,
d
)
→
(
a
c
,
b
c
+
d
)
(a,b)+(c,d)\to (ac,bc+d)
(a,b)+(c,d)→(ac,bc+d)。
因为只需要查询一个位置的值,并且没有实际上的修改,我们可以把一个区间的操作对原序列的贡献写作
[
1
,
p
1
)
[
p
1
,
p
2
)
[
p
2
,
p
3
)
.
.
.
[1,p_1)[p_1,p_2)[p_2,p_3)...
[1,p1)[p1,p2)[p2,p3)... 的形式,那么这个区间的操作对
x
x
x 的变化贡献就是
x
x
x 对应位置的标记,只需要二分出那个位置即可。
考虑合并两个子区间的操作对原序列的贡献,只需要类似归并的方法,假设两个子区间的贡献区间个数分别为
x
,
y
x,y
x,y,那么复杂度为
O
(
x
+
y
)
O(x+y)
O(x+y)。最底部单个操作的贡献由
[
l
,
r
]
[l,r]
[l,r] 改为
[
1
,
l
−
1
]
[
l
,
r
]
[
r
+
1
,
n
]
[1,l-1][l,r][r+1,n]
[1,l−1][l,r][r+1,n]
假设可以离线,我们既可以对操作的线段树,并维护出每个区间对原序列的贡献区间,建树的复杂度为 O ( q log q ) O(q\log q) O(qlogq),查询的时候就找到对应的 l o g log log 个操作区间,在其中二分 k k k 的位置找到标记即可,复杂度 O ( q log 2 q ) O(q\log^2q) O(qlog2q)
在线的话,可以考虑二进制分组,实际上相当于每次在末尾插入一个操作,然后维护对应的线段树,只需要在插入的位置对应当前区间的结尾时求出合并两个子区间的贡献即可。复杂度不变。
代码参见这里
Codechef Persistent oak (OAK)
我实在是找不到什么可持久化的非模板题了
树链剖分之后,维护承重还剩下多少,区间减,然后找到第一个小于0的,然后上面的承重整体加上断掉的重量。
橡果全部归0就接上初始版本的对应区间。打恢复标记好像也没什么问题。
blog1,blog2
总结:4道水题,2道板题,3道trivial题,十分无脑。(相信大家再遇到可持久化题一定会自信满满了)