吉司机线段树
Segment Tree Beats
吉斯机线段树,一种特化的线段树,可以用来维护区间历史最值,以及进行对区间内每个数将其和给定参数取最值的操作
区间最值操作
区间最值操作,给定一个数\(k\),给出区间\([l,r]\),令区间内的每个数\(x_{i}\)变为\(\min(x_{i},k)\)或\(\max(x_{i},k)\)
考虑怎么实现这件事
以区间最小值操作为例。暴力枚举显然是不可取的。我们考虑到,对于区间最小值操作,实际上只需要关注\(>k\)的数就行。我们还注意到,如果区间内的最大值比\(k\)小,这个区间可以直接忽略。所以考虑维护一个区间最大值,记为\(maxn\)。由于需要维护区间和,所以还要维护最大值的个数\(cnt\)。这时候我们考虑,我们可以每次只修改区间最大值,然后通过不断二分缩小区间覆盖所有的值。所以,还需要再维护一个区间严格次大值\(secn\)。根据\(secn\)和\(k\)的大小关系,可以判断要向那一棵子树递归
具体评判标准如下。对于给定的值\(k\)
1、如果\(maxn\le k\),显然这个\(k\)是没有意义的,直接返回
2、如果\(secn<k < maxn\),那么这个\(k\)就能更新当前区间中的最大值。于是我们让区间和加上\(cnt*(k-maxn)\),然后更新\(maxn\)为\(k\),并打一个标记
3、如果\(k\le secn\),那么这时你发现你不知道有多少个数涉及到更新的问题。于是我们的策略就是,暴力递归向下操作。然后上传信息
使用势能分析法可以得到复杂度是\(O(m\log n)\)
例题
HDU5306 Gorgeous Sequence
维护一个序列 a,执行以下操作:
0 l r t \(\forall l\le i\le r,~ a_i=\min(a_i,t)\)
1 l r 输出\(\max\limits_{i=l}^r a_i\)
2 l r 输出\(\sum\limits_{i=l}^r a_i\)
多组测试数据,保证\(T\le 100,~\sum n,\sum m\le 10^6\)
比较简单的区间最值操作,代码如下(转载自oiwiki)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1000006;
int t,n,m;
int a[N];
int mx[N<<2], se[N << 2], cn[N << 2], tag[N << 2];
long long sum[N << 2];
void pushup(int u) { // 向上更新标记
const int ls = u << 1, rs = u << 1 | 1;
sum[u] = sum[ls] + sum[rs];
if (mx[ls] == mx[rs]) {
mx[u] = mx[rs];
se[u] = max(se[ls], se[rs]);
cn[u] = cn[ls] + cn[rs];
} else if (mx[ls] > mx[rs]) {
mx[u] = mx[ls];
se[u] = max(se[ls], mx[rs]);
cn[u] = cn[ls];
} else {
mx[u] = mx[rs];
se[u] = max(mx[ls], se[rs]);
cn[u] = cn[rs];
}
}
void pushtag(int u, int tg) { // 单纯地打标记,不暴搜
if (mx[u] <= tg) return;
sum[u] += (1ll * tg - mx[u]) * cn[u];
mx[u] = tag[u] = tg;
}
void pushdown(int u) { // 下传标记
if (tag[u] == -1) return;
pushtag(u << 1, tag[u]), pushtag(u << 1 | 1, tag[u]);
tag[u] = -1;
}
void build(int u = 1, int l = 1, int r = n) { // 建树
tag[u] = -1;
if (l == r) {
sum[u] = mx[u] = a[l], se[u] = -1, cn[u] = 1;
return;
}
int mid = (l + r) >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
void modify_min(int L, int R, int v, int u = 1, int l = 1, int r = n) {
if (mx[u] <= v) return;
if (L <= l && r <= R && se[u] < v) return pushtag(u, v);
int mid = (l + r) >> 1;
pushdown(u);
if (L <= mid) modify_min(L, R, v, u << 1, l, mid);
if (mid < R) modify_min(L, R, v, u << 1 | 1, mid + 1, r);
pushup(u);
}
int query_max(int L, int R, int u = 1, int l = 1, int r = n) { // 查询最值
if (L <= l && r <= R) return mx[u];
int mid = (l + r) >> 1, r1 = -1, r2 = -1;
pushdown(u);
if (L <= mid) r1 = query_max(L, R, u << 1, l, mid);
if (mid < R) r2 = query_max(L, R, u << 1 | 1, mid + 1, r);
return max(r1, r2);
}
long long query_sum(int L, int R, int u = 1, int l = 1, int r = n) { // 数值
if (L <= l && r <= R) return sum[u];
int mid = (l + r) >> 1;
long long res = 0;
pushdown(u);
if (L <= mid) res += query_sum(L, R, u << 1, l, mid);
if (mid < R) res += query_sum(L, R, u << 1 | 1, mid + 1, r);
return res;
}
void go() { // 根据题意
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
build();
for (int i = 1; i <= m; i++) {
int op, x, y, z;
cin >> op >> x >> y;
if (op == 0)
cin >> z, modify_min(x, y, z);
else if (op == 1)
cout << query_max(x, y) << '\n';
else
cout << query_sum(x, y) << '\n';
}
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> t;
while (t--) go();
return 0;
}
区间历史最值
虽然带有“历史”二字,但实际上和可持久化没有半毛钱关系。所谓历史最值,实际上是在原数组\(A\)的基础上另外维护一个数组\(B\),初始时令\(B=A\),随后的每一次操作以后都令所有的\(B_{i}=max(B_{i},A_{i})\)
我们考虑把\(A\)和\(B\)都放到线段树中维护。在原有的\(maxn\)之外,加入一个变量\(maxb\)维护区间内\(B\)的最大值。每次\(modify\)更新\(A\)的最值时,同步更新对应位置的\(B\)的值就可以了
详细内容看一道例题:
P6242 【模板】线段树 3(区间最值操作、区间历史最值)
这道题中要求同时支持区间最值、区间加两种操作,同时支持区间最大值、区间历史最大值、区间和三种询问。考虑使用lazytag维护两种操作。由于历史最大值的参与,需要设置4个lazytag,其中\(tgmn\)、\(pret\)对应区间最值操作和历史区间操作的最大值;\(tgpl\)、\(pres\)对应区间加和历史区间加法的最大值
我们可以发现,实际上所有的操作都被分为了两类:对当前区间最大值的操作和对非最大值的操作。不难发现,区间最值操作的标记只会打在区间最大值上
这个时候,所谓的区间最值标记实际上可以转化为对区间最大值这一类数的加法标记。所以刚才所说的四类标记被赋予了新的含义:
最大值加减标记\(tgmn\)
最大值历史最大的加减标记\(pret\)
非最大值加减标记\(tgpl\)
非最大值历史最大的加减标记\(pres\)
剩下的部分就按照正常的流程写就可以了,直接看代码
如果有一些疑问,可以参考灵梦の题解
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define N 500010
#define ls(p) p<<1
#define rs(p) p<<1|1
#define inf 2e9
int n,m;
int op,x,y,k;
int a[N];
struct segTree{
int maxn,secn;//区间最大值和严格次大值
int maxb;//历史最大值
int sum;//区间和
int cnt;//区间内最大值的个数
int tgmn;//标记(区间取最小)
int tgpl;//区间加标记
int pret;//区间内所有最大值的懒标记的最大值
int pres;//区间内非最大值的懒标记的最大值
int l,r;//当前线段树分管的区间
}t[N*4];
void push_up(int p){
int pl=ls(p),pr=rs(p);
t[p].sum=t[pl].sum+t[pr].sum;
t[p].maxb=max(t[pl].maxb,t[pr].maxb);
//继承次大值
if(t[pl].maxn==t[pr].maxn){
t[p].maxn=t[pl].maxn;
t[p].secn=max(t[pl].secn,t[pr].secn);
t[p].cnt=t[pl].cnt+t[pr].cnt;
}else if(t[pl].maxn>t[pr].maxn){
t[p].maxn=t[pl].maxn;
t[p].secn=max(t[pl].secn,t[pr].maxn);
t[p].cnt=t[pl].cnt;
}else{
t[p].maxn=t[pr].maxn;
t[p].secn=max(t[pl].maxn,t[pr].secn);
t[p].cnt=t[pr].cnt;
}
return;
}
/*打标记+更新信息*/
void push_tg(int p,int tg1,int tg2,int pr1,int pr2){
int l=t[p].l,r=t[p].r;
t[p].sum+=tg1*t[p].cnt+tg2*(r-l+1-t[p].cnt);
//更新历史最值
t[p].maxb=max(t[p].maxb,t[p].maxn+pr1);
t[p].maxn+=tg1;
if(t[p].secn!=-inf) t[p].secn+=tg2;
//注意优先级,先更新历史最大标记,然后更新当前标记
t[p].pret=max(t[p].pret,t[p].tgmn+pr1);
t[p].pres=max(t[p].pres,t[p].tgpl+pr2);
t[p].tgmn+=tg1;
t[p].tgpl+=tg2;
}
/*下传*/
void push_down(int p){
//当前最大值取左右儿子最大值的较大值
int mx=max(t[ls(p)].maxn,t[rs(p)].maxn);
if(t[ls(p)].maxn==mx){//最大值在左区间
//传入最大值相关的tag,表示区间内同时维护最大值和非最大值
push_tg(ls(p),t[p].tgmn,t[p].tgpl,t[p].pret,t[p].pres);
}else{//只传入非最大值相关的tag,因为不用维护最大值
push_tg(ls(p),t[p].tgpl,t[p].tgpl,t[p].pres,t[p].pres);
}
if(t[rs(p)].maxn==mx){//最大值在右区间
//传入最大值相关的tag,表示区间内同时维护最大值和非最大值
push_tg(rs(p),t[p].tgmn,t[p].tgpl,t[p].pret,t[p].pres);
}else{//只传入非最大值相关的tag,因为不用维护最大值
push_tg(rs(p),t[p].tgpl,t[p].tgpl,t[p].pres,t[p].pres);
}
//清空标记
t[p].tgmn=t[p].tgpl=t[p].pret=t[p].pres=0;
}
/*建树*/
void build(int p,int l,int r) {
t[p].tgmn=t[p].tgpl=t[p].pret=t[p].pres=0;//初始化标记
t[p].l=l,t[p].r=r;
if(l==r){
t[p].sum=t[p].maxn=t[p].maxb=a[l];
t[p].secn=-inf;
t[p].cnt=1;
return;
}
int mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
return;
}
/*区间取最小值*/
//把L到R区间内的所有数和k取较小值
void modify_min(int p,int L,int R,int k){
if(t[p].maxn<=k) return;//没有改的必要,回溯
int l=t[p].l,r=t[p].r;
if(R<l||L>r) return;//无关,回溯
//如果区间次小值比k小,单次操作不足以解决,给区间打标记等待下传
if(L<=l&&r<=R&&t[p].secn<k){
int v=k-t[p].maxn;
push_tg(p,v,0,v,0);
return;
}
push_down(p);
modify_min(ls(p),L,R,k);
modify_min(rs(p),L,R,k);
push_up(p);
return;
}
/*区间加*/
void modify_pls(int p,int L,int R,int k){
//如果赋值区间完全包含遍历到的点的区间,直接下放标记
int l=t[p].l,r=t[p].r;
if(L>r||R<l) return;
if(L<=l&&r<=R){
t[p].sum+=k*(r-l+1);//更新区间和
t[p].maxn+=k;
t[p].maxb=max(t[p].maxb,t[p].maxn);
if(t[p].secn!=-inf) t[p].secn+=k;
t[p].tgmn+=k;
t[p].tgpl+=k;
//更新标记本身的最值
t[p].pret=max(t[p].pret,t[p].tgmn);
t[p].pres=max(t[p].pres,t[p].tgpl);
return ;
}
//如果毫不相关,直接下传
push_down(p);
//搜左右子树
modify_pls(ls(p),L,R,k);
modify_pls(rs(p),L,R,k);
//回溯时更新父亲的值
push_up(p);
}
/*查询区间最大值*/
int query_max(int p,int L,int R){
int l=t[p].l,r=t[p].r;
if(R<l||L>r) return -inf;
if(L<=l&&r<=R) return t[p].maxn;
push_down(p);
int res1=-inf,res2=-inf;
//分别找左右子树最大值取较大
res1=query_max(ls(p),L,R);
res2=query_max(rs(p),L,R);
return max(res1,res2);
}
/*查询区间历史最大值值*/
int query_mxb(int p,int L,int R){
int l=t[p].l,r=t[p].r;
if(R<l||L>r) return -inf;
if(L<=l&&r<=R) return t[p].maxb;
push_down(p);
int res1=-inf,res2=-inf;
//分别找左右子树最大值取较大
res1=query_mxb(ls(p),L,R);
res2=query_mxb(rs(p),L,R);
return max(res1,res2);
}
/*查询区间和*/
int query_sum(int p,int L,int R){
int l=t[p].l,r=t[p].r;
if(R<l||L>r) return 0;
if(L<=l&&r<=R) return t[p].sum;
push_down(p);
int res=0;
res+=query_sum(ls(p),L,R);
res+=query_sum(rs(p),L,R);
return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,1,n);
for(int i=1;i<=m;i++){
cin>>op>>x>>y;
if(op==1){
cin>>k;
modify_pls(1,x,y,k);
}else if(op==2){
cin>>k;
modify_min(1,x,y,k);
}else if(op==3){
cout<<query_sum(1,x,y)<<'\n';
}else if(op==4){
cout<<query_max(1,x,y)<<'\n';
}else if(op==5){
cout<<query_mxb(1,x,y)<<'\n';
}
}
return 0;
}
练习题
P10639 BZOJ4695 最佳女选手
要求同时支持区间加、区间最大、区间最小三种操作,支持区间和、区间最大、区间最小三种询问。情况比较多,逻辑稍显复杂,但都是上文已经提到过的套路,是一道很好的板子题