吉老师线段树学习笔记(内含吉老师ppt)
Segment tree beats
吉老师线段树
Segment tree Beats!.pdf_免费高速下载|百度网盘-分享无限制 (baidu.com)
为广大oier们提供学习ppt(笑)
历史最大值未完工
作用
用于维护区间最值和区间历史最值的线段树
区间最值
引入
问题
给定一个长度为n的数列A,有m次操作:
1.将区间\([l,r]\)里面的数\(a[i]=min(a[i],x)\)
2.询问区间\([l,r]\)中所有数的和
思路
case1:\(n,m\leq 5e4\)直接分块搞!
case2:\(n,m\leq 5e5\)线段树
我们发现最基本的线段树是没办法做到这个东西的修改操作的
在吉老师的ppt中写到,类比某道区间取模的题目,我们每一次找出这个区间的最大值mx,如果mx>x,就暴力修改这个位置的值,否则修改完毕,直接退出,发现时间复杂度是\(O(n^2logn)\),我们不能接受
继续试着深入一下,于是就有了吉老师线段树
正文
对于线段树,我们维护以下几个东西:
- 区间最大值mx
- 最大值在区间内出现次数cnt
- 区间次大值se
- 区间和sum
现在开始考虑打标记x(区间取min):
- case1:mx<=x,结合初步思想,对于整体没有影响
- case2:se<x<mx,那么sum=sum-(mx-x)*t
- case3:x<=se,不能直接计算出答案,我们分别dfs这个rt(当前)节点的两个孩子,如果当前dfs的过程中出现了前两种情况,我们打上标记后退出,否则继续
这个就是吉老师线段树的整体思想
时间复杂度是O(nlogn),证明在论文里面(ppt也有),我没有找到qwq
代码
struct JiSirSegTree{
#define ls rt<<1
#define rs rt<<1|1
struct Node{
int sum,mx,cnt,se;
int lazy;
}t[M<<2];
void push_up(int rt){
if(t[ls].mx<t[rs].mx){
t[rt].mx=t[rs].mx;
t[rt].cnt=t[rs].cnt;
t[rt].se=max(t[rs].se,t[ls].mx);
}else{
if(t[ls].mx>t[rs].mx){
t[rt].mx=t[ls].mx;
t[rt].cnt=t[ls].cnt;
t[rt].se=max(t[ls].se,t[rs].mx);
}else{
t[rt].mx=t[rs].mx;
t[rt].cnt=t[rs].cnt+t[ls].cnt;
t[rt].se=max(t[rs].se,t[ls].se);
}
}
t[rt].sum=t[rs].sum+t[ls].sum;
}
void modify(int rt,int x){
if(t[rt].mx<=x)return;
t[rt].sum-=t[rt].cnt*(t[rt].mx-x);
t[rt].se=t[rt].mx;//sss
t[rt].mx=t[rt].lazy=x;
}
void push_down(int rt){
if(t[rt].lazy!=-1){
modify(ls,t[rt].lazy);
modify(rs,t[rt].lazy);
t[rt].lazy=-1;
}
}
void build(int rt,int l,int r){
t[rt].lazy=-1;
if(l==r){
t[rt].sum=t[rt].mx=a[l];
t[rt].cnt=1;
t[rt].se=-1;
return;
}
int mid=(l+r)>>1;
build(ls,l,mid);
build(rs,mid+1,r);
push_up(rt);
}
void update(int rt,int l,int r,int L,int R,int val){
if(t[rt].mx<=val)return;
if(L<=l&&r<=R&&t[rt].se<val){
modify(rt,val);
return;
}
push_up(rt);
int mid=(l+r)>>1;
if(L<=mid)update(ls,l,mid,L,R,val);
if(R>mid)update(rs,mid+1,r,L,R,val);
push_up(rt);
}
int query_mx(int rt,int l,int r,int L,int R){
if(L<=l&&r<=R){
return t[rt].mx;
}
push_up(rt);
int mid=(l+r)>>1;
int maxn=0x3f3f3f3f;
if(L<=mid)maxn=max(query_mx(ls,l,mid,L,R),maxn);
if(R>mid)maxn=max(query_mx(rs,mid+1,r,L,R),maxn);
return maxn;
}
int query_sum(int rt,int l,int r,int L,int R){
if(L<=l&&r<=R){
return t[rt].sum;
}
push_up(rt);
int mid=(l+r)>>1;
int sum=0;
if(L<=mid)sum+=query_sum(ls,l,mid,L,R);
if(R>mid)sum+=query_sum(rs,mid+1,r,L,R);
return sum;
}
}tree;
new problem1
给定一个长度为n的数列A,有m次操作:
1.将区间\([l,r]\)里面的数\(a[i]=min(a[i],x)\)
2.询问区间\([l,r]\)中所有数的和
3.区间[l,r]的所有数加上x(x可能为负数)
我们直捣黄龙,看一下线段树怎么搞
对于线段树,我们依旧维护那几个值用于解决操作1和操作3,但是现在多了一个操作2好像没什么大问题
时间复杂度
哇去,我还没有学势能分析!我不会!(真诚恳的说)
咕咕咕(我要是高一还在学oi的话说不定可以补一下)
new problem2
给定一个长度为n的数列A,有m次操作:
1.将区间\([l,r]\)里面的数\(a[i]=min(a[i],x)\)
2.将区间\([l,r]\)里面的数\(a[i]=max(a[i],x)\)
3.查询区间和
可以发现和最初的问题很相似,所以类比一下:
- 对于线段树,我们维护:最大值mx,最小值mn,出现次数分别为cnt1和cnt2,次大值分别是se1和se2
- 我们的lazy标记是可以合并的,所以接下来的就和问题1相似了!
- 参照初始问题
历史最值
虽然这里已经和我的训练列表没有任何关系了,但是我还是要写完
先咕咕咕一天吧
引入
问题
给定一个长度为n的数列A,有m次操作:
1.区间\([l,r]\)中的所有数加上x
2.区间\([l,r]\)中的所有数变成\(max(a[i]-x,0)\)
3.区间\([l,r]\)中所有数变成x
4.查询\(a_i\)的值
5.查询\(a_i\)的历史最大值
思路
我们要支持的操作只有一个:给区间中所有数加上x然后和y取max,我们记作(x,y)
那么这三个操作分别为: \((x,inf),(-x,0),(-inf,x)\)
考虑合并两个标记\((a,b),(c,d)\),结果是\((a+c,max(b+c,d))\)
然后标记合并,操作1到操作4全部都over了
对于操作5:我们对于线段树的每一个节点,额外记录一个历史最大标记(即区间中元素在使用这个标记后是上一次标记传到这里的最大值),参考cpu监控
正文
问题在于维护这个历史最大标记:
- 用每一时刻的标记来更新历史最大标记(在push_down时,令父亲节点的历史最大标记为p,当前节点为q,历史最大标记为w,那么我们要用p+q取更新w)