吉老师线段树学习笔记(内含吉老师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)\),我们不能接受

继续试着深入一下,于是就有了吉老师线段树

正文

对于线段树,我们维护以下几个东西:

  1. 区间最大值mx
  2. 最大值在区间内出现次数cnt
  3. 区间次大值se
  4. 区间和sum

现在开始考虑打标记x(区间取min):

  1. case1:mx<=x,结合初步思想,对于整体没有影响
  2. case2:se<x<mx,那么sum=sum-(mx-x)*t
  3. 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.查询区间和

可以发现和最初的问题很相似,所以类比一下:

  1. 对于线段树,我们维护:最大值mx,最小值mn,出现次数分别为cnt1和cnt2,次大值分别是se1和se2
  2. 我们的lazy标记是可以合并的,所以接下来的就和问题1相似了!
  3. 参照初始问题

历史最值

虽然这里已经和我的训练列表没有任何关系了,但是我还是要写完

先咕咕咕一天吧

引入

问题

给定一个长度为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监控

正文

问题在于维护这个历史最大标记:

  1. 用每一时刻的标记来更新历史最大标记(在push_down时,令父亲节点的历史最大标记为p,当前节点为q,历史最大标记为w,那么我们要用p+q取更新w)
posted @ 2023-05-02 20:28  6penny  阅读(78)  评论(0编辑  收藏  举报