前言
本文主要讲解一种叫做 S e g m e n t T r e e B e a t s SegmentTree~Beats SegmentTree Beats 的维护区间取最值操作的问题,以及维护区间历史最值的方法。本文参考自许多博客,以及吉老师 2016 2016 2016 年的集训队论文,会加上很多例题进行讲解QAQ。
区间最值操作
例题一 [HDU5306] Gorgeous Sequence
给出长度为 n ( n ≤ 1 e 6 ) n (n\le 1e6) n(n≤1e6) 的序列 { A n } \{A_n\} { An} 和 m ( m ≤ 1 e 6 ) m(m\le1e6) m(m≤1e6) 次操作,每次操作为以下三种类型之一:
1.给出 l , r , k l,r,k l,r,k,对所有 i ∈ [ l , r ] i\in[l,r] i∈[l,r],将 A i A_i Ai 变成 m i n ( A i , k ) min(A_i,k) min(Ai,k)
2.给出 l , r l,r l,r,询问序列 A A A 在区间 [ l , r ] [l,r] [l,r] 的最大值
3.给出 l , r l,r l,r,询问序列 A A A 在区间 [ l , r ] [l,r] [l,r] 的和
在这道题中,第一种操作就叫区间最值操作。
我们把这种操作看作是一种标记,那怎么快速更新区间的信息呢?
可以发现,由于存在不同的值,所以区间的信息是无法更新的。但是如果只有一种值,区间取 m i n min min 就变得轻而易举了。
因此,可以想到一个看起来像暴力的做法。
我们用线段树维护每个区间的最大值 m x mx mx 和严格次大值 s e se se,以及 m x mx mx 的个数 c n t cnt cnt。
考虑 1 1 1 操作,对于一个线段树上的区间,我们分类讨论一下:
- 如果 m x ≤ k : mx\leq k: mx≤k:
那么我们可以直接 r e t u r n return return - 如果 m x > k , s e < k : mx>k,se < k: mx>k,se<k:
我们在区间打一个 t a g tag tag 标记,并更新最大值为 k k k,同时更新 s u m = s u m − c n t ∗ ( m x − k ) sum = sum - cnt * (mx - k) sum=sum−cnt∗(mx−k) - 如果 s e ≥ k : se\ge k: se≥k:
因为此时我们并不知道哪些数大于 t t t,于是我们暴力递归子区间
这样子的操作看起来非常滴玄学,但是跑起来却非常滴快,轻松地就把这题 A A A 了。
吉老师通过势能分析得出这样子操作总的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn) 的。我们也可以换一种角度思考,假设会进行递归,那么一定存在一个节点,满足 m x > t , s e > t mx>t,se>t mx>t,se>t,那么通过这次取 m i n min min 之后, s e se se 和 m x mx mx 都会变成 k k k。也就是说,值域会减少一。而线段树有 l o g n logn logn 层,每层的值域加起来是 O ( n ) O(n) O(n) 的,因此值域最多只有 O ( n l o g n ) O(nlogn) O(nlogn),所以总的复杂度是 O ( ( n + m ) l o g n ) O((n+m)logn) O((n+m)logn) 的(虽然看起来还是很玄学。
为了方便理解,下面给出了这题区间打标记的代码:
void addtag(int rt, int c){
if(mx[rt] <= c) return;
sum[rt] -= cnt[rt] * (mx[rt] - c);
mx[rt] = tag[rt] = c;//更新最大值并打标记
}
void pushdown(int rt){
if(tag[rt]){
addtag(lc, tag[rt]);
addtag(rc, tag[rt]);
tag[rt] = 0;
}
}
void update(int l, int r, int rt, int a, int b, int c){
if(mx[rt] <= c) return;
if(l >= a && r <= b && se[rt] < c){
//次大值小于 c,最大值大于 c,此时只修改最大值
addtag(rt, c);
return;
}
pushdown(rt);
int m = l + r >> 1;
if(a <= m) update(lson, a, b, c);
if(b > m) update(rson, a, b