前言
本文主要讲解一种叫做 SegmentTree BeatsSegmentTree~BeatsSegmentTree Beats 的维护区间取最值操作的问题,以及维护区间历史最值的方法。本文参考自许多博客,以及吉老师 201620162016 年的集训队论文,会加上很多例题进行讲解QAQ。
区间最值操作
例题一 [HDU5306] Gorgeous Sequence
给出长度为 n(n≤1e6)n (n\le 1e6)n(n≤1e6) 的序列 { An}\{A_n\}{ An} 和 m(m≤1e6)m(m\le1e6)m(m≤1e6) 次操作,每次操作为以下三种类型之一:
1.给出 l,r,kl,r,kl,r,k,对所有 i∈[l,r]i\in[l,r]i∈[l,r],将 AiA_iAi 变成 min(Ai,k)min(A_i,k)min(Ai,k)
2.给出 l,rl,rl,r,询问序列 AAA 在区间 [l,r][l,r][l,r] 的最大值
3.给出 l,rl,rl,r,询问序列 AAA 在区间 [l,r][l,r][l,r] 的和
在这道题中,第一种操作就叫区间最值操作。
我们把这种操作看作是一种标记,那怎么快速更新区间的信息呢?
可以发现,由于存在不同的值,所以区间的信息是无法更新的。但是如果只有一种值,区间取 minminmin 就变得轻而易举了。
因此,可以想到一个看起来像暴力的做法。
我们用线段树维护每个区间的最大值 mxmxmx 和严格次大值 sesese,以及 mxmxmx 的个数 cntcntcnt。
考虑 111 操作,对于一个线段树上的区间,我们分类讨论一下:
- 如果 mx≤k:mx\leq k:mx≤k:
那么我们可以直接 returnreturnreturn - 如果 mx>k,se<k:mx>k,se < k:mx>k,se<k:
我们在区间打一个 tagtagtag 标记,并更新最大值为 kkk,同时更新 sum=sum−cnt∗(mx−k)sum = sum - cnt * (mx - k)sum=sum−cnt∗(mx−k) - 如果 se≥k:se\ge k:se≥k:
因为此时我们并不知道哪些数大于 ttt,于是我们暴力递归子区间
这样子的操作看起来非常滴玄学,但是跑起来却非常滴快,轻松地就把这题 AAA 了。
吉老师通过势能分析得出这样子操作总的时间复杂度是 O(nlogn)O(nlogn)O(nlogn) 的。我们也可以换一种角度思考,假设会进行递归,那么一定存在一个节点,满足 mx>t,se>tmx>t,se>tmx>t,se>t,那么通过这次取 minminmin 之后,sesese 和 mxmxmx 都会变成 kkk。也就是说,值域会减少一。而线段树有 lognlognlogn 层,每层的值域加起来是 O(n)O(n)O(n) 的,因此值域最多只有 O(nlogn)O(nlogn)O(nlogn),所以总的复杂度是 O((n+m)logn)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, c);
pushup(rt);
}
例题二 [2020ICPC小米邀请赛网络赛一] E Phone Network
给出长度为 nnn 的序列 { An}\{A_n\}{ An} 和 mmm,满足 Ai∈[1,m]A_i\in [1,m]Ai∈[1,m]。(m≤n≤2e5)(m\le n\le 2e5)(m≤n≤2e5)
对于每个 k∈[1,m]k\in [1,m]k<