线段树

1 - 引入  

  在我们看到   修改、查询某些区间里的值 等   的题目时,最简单、最暴力的方法一定会浮现在我们的脑海当中:开数组,O(n)暴力查询修改。这样做也并不是不可以,但对于数据规模较大的题目来说,可能会收获满满De TLE。   而线段树这样一个数据结构,将每一段段的区间分散到了树上的每个节点上去了。总之,这个数据结构包含了分治和二叉树,将普通数组的O1单点、On区间的时间复杂度改为了Ologn单点区间的复杂度。

  同时,线段树的父节点与子节点的关系:既然树上De每一个节点都代表着一个区间的话,父节点区间最左端就是左儿子区间的最左端,同理父节点区间最右端就是右儿子区间的最右端(如图所示),即 [FA.L, FA.R] = [LC.L, RC.R]。

【1】
[L,R] : [1, 7]
【2】
[L,R] : [1, 3]
【3】
[L,R] : [4, 7]
【4】
[L,R] : [1, 2]
【5】
[L,R] : [3, 3]
【6】
[L,R] : [4, 5]
【7】
[L,R] : [6, 7]
【8】
[L,R] : [1, 1]
【9】
[L,R] : [2, 2]

【10】
- 空-

【11】
- 空- 
【12】
[L,R] : [4, 4]
【13】
[L,R] : [5, 5]
【14】
[L,R] : [6, 6]
【15】
[L,R] : [7, 7]

————————————————————

2 - 原理及操作方法  

————————————

维护线段树

————————————

  【建树过程】原理嘞其实很简单,在引入中有说到“分治”的部分,那就用二分来做。 首先,我们拿一个节点【1】当根节点,区间大小就是[1, n];然后将这个区间分为两部分:节点【2】与节点【3】,区间大小分别为 [1, (1+n)/2] 和 [(1+n)/2+1, n]。以此类推…………。 这个时候我们可以发现节点的关系:【1】->【2】【3】,【2】->【4】【5】,【3】-> 【6】【7】……,即【n】->【n*2】【n*2+1】,同时这也是二叉树的节点性质。

  由于树型结构和二分的想法,我们采用递归建树,在到达叶子节点的时候将单个点的值赋值上去。对于ta的父亲节点维护的是 某些区间的值 ,所以回溯的时候记得update一下父亲的值。

struct TREE {
    int l, r;
    int sum, max, min; // …………
} sm[KI];

void update(int now)
{
	sm[now].sum = sm[now<<1].sum + sm[now<<1|1].sum;
	sm[now].max = max(sm[now<<1].max, sm[now<<1|1].max);
	sm[now].min = min(sm[now<<1].min, sm[now<<1|1].min);
}

  可能一个题目不会让你只算区间和之类的,线段树除了可以维护和,你也可以对这棵树进行“改造”,比如用一下结构体,update的话可以简简单单的重载一下运算符:

struct TREE {
    int l, r;
    int sum, max, min; // …………
    TREE(){
        l = r = sum = max = min = 0;
    }
};

TREE operator+(const TREE &l, const TREE &r){
    TREE ans;
    ans.l =  l.l, ans.r = r.r;
    ans.sum = l.sum + r.sum;
    ans.max = max(l.max, r.max);
    ans.min = min(l.min, r.min);
    return ans;
}

 

————————————

单点查询、单点修改

————————————

让我们来举一个例子好好的研究一下线段树的操作。

请你将区间[L, R]里的某一个元素u加上x,或查询某一个元素u的值,共m次询问。

“用数组O1就可以完成的啦,用什么线段树啊” ,这时线段树与数组不同的是,Ta无法O1的找出指定元素在哪里,那么在二分查找中我们容易的想到 u 是否大于 mid,或是小于等于 mid,将复杂度压为logn,然后就可以O1愉快地修改和询问了。(code

#define ababababababa IdontKown

————————————

区间查询

————————————

虽然说在上面的例子中,由于是对单点的查询和修改,导致线段树的优势还没有发挥到完全,那就让我们再看一道题目。

请你查询区间[L, R]里的某一个区间[L',R']的值,共m次询问。

“教练,我会枚举”,往往这种题目的数据范围会比较大,至少不会让你的Onm卡过去,所以暴力枚举是不行滴,让我们拿出刚刚讲的线段树过来看看。和单点查询差不多的是,我们可以通过二分查找去查询answer。但是区间并不是一个点,除非你要找到区间就是某一个节点的范围,可以直接返回值,否则就需要分开继续向下去找,直到叶子节点,即只有一个点的时候。

``` 有一个[L --- R]的区间,你想要去查询[l --- r]区间的值。 First. 当两个区间形成这样的情况时,即要查询的区间被大的区间包含着 || ,此时我们需要继续的向下去查询。 || [L - [l -- r] - R] Second. 当两个区间形成这样的情况时,即要查询的区间包含着大的区间,此时需要先返回[L,R]的值。 || 因为我们只需对[L,R]区间进行操作,那其他的部分, || 在查询到这段区间前分配好了。 [l - [L -- R] - r] or [L/l --- R/r] Third. 当两个区间形成这样的情况时,即要查询的区间与大的区间相交织着 || ,我们的任务也只是要对[L,R]进行操作,所以去查询[l,R]和[L,r]就可以了吗? || 这是要判断[L - l - (L+R)/2 - R]的情况,也就是[l,mid]的情况,2.同理。 || [L - [l -- R] - r] or [l - [L -- r] - R] ```

(code  

#define ababababababa IdontKown

————————————

区间修改、区间查询

————————————

当然一个题目或是一个实际问题,怎么可能会考你单点查询、修改,区间查询这种简单的问题呢。它既然让你改数, 肯定 不会让你只去改一个数滴,那对于一个区间该怎么修改嘞?

请你将区间[L, R]里的所有的元素u加上x,或查询[L, R]所有的元素的值,共m次询问。

  "既然单点修改可以一个改,那不就是改一个区间里的所有的数吗,在选择区间查询,不就可以A了吗",
   虽然说这样可以吧,有些题也确实可以通过这样暴力的修改去拿到正解(例如GSS4,虽然是暴力修改,但是也有优化),
   但是你不觉得这样做太慢了吗,而且还是如此暴力的暴力。

So,当你遇到这种题目的时候,你所要在线段树中维护的区间sum,在你对区间[l,r]中的所有的数都+/-val的时候,也随之把区间[l,r]维护的sum改变了,即$sum+/-(r-l+1)\times val$。等等,既然只改了这整个区间,那ta下面的小的区间你该怎么办呢?继续往下改?那这样就损失了一开始的价值了;在改的时候记录一下?对吗?OK,我们使用LAZY懒惰标记记录一下这个区间改了多少,当ta询问的时候先一步往下传LAZY标记。

#define ababababababa IdontKown
//代码是zhx讲课时的代码
//代码是zhx讲课时的代码 
#define root 1,n,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1

struct node//线段树上的节点信息 
{
	int l,r;//这个节点左端点和右端点 所维护的区间 
	int sum;//当前这段区间的和
	int maxv;//当前区间的最大值
	int minv;//当前区间的最小值 
	int sum2;//|al-al+1| + |al+1 - al+2| + ... + |ar-1 - ar| 
	int lv,rv;//lv代表最左边的数 rv代表最右边的数 
	int ans;
	node()
	{
		l=r=sum=maxv=0;
	} 
}z[];

node operator+(const node &l,const node &r)//l左儿子 r右儿子
{
	node ans;
	ans.l = l.l;
	ans.r = r.r;
	ans.sum = l.sum + r.sum;
	ans.maxv = max(l.maxv,r.maxv);
	ans.minv = min(l.minv,r.minv);
	ans.sum2 = l.sum2 + r.sum2 + abs(l.rv - r.lv);
	ans.lv = l.lv;
	ans.rv = r.rv;
	ans.ans = max( l.ans , r.ans , l.maxv - r.minv);
	return ans;
} 

void update(int rt)
{
	z[rt] = z[rt<<1] + z[rt<<1|1];
} 

void build(int l,int r,int rt)//建树 当前线段树节点编号为rt 并且当前区间为l~r 
{
	if (l==r)//到最底层
	{
		z[rt].l = z[rt].r = l;
		z[rt].sum = y[l];//这段区间的和就等于第l个数 
		return;
	}
	int m=(l+r)>>1;//(l+r)/2
	//左儿子所对应的区间 [l,m] 右儿子所对应的区间 [m+1,r]
	build(lson);//build(l,m,rt*2)
	build(rson);//build(m+1,r,rt*2+1)
	update(rt);//更新rt这个节点的值 
} 

node query(int l,int r,int rt,int nowl,int nowr)
//当前线段树节点所对应的区间是l~r
//当前线段树节点编号是rt
//询问的区间是nowl~nowr
{ 
	if (nowl <= l && r <= nowr) return z[rt];
	int m=(l+r)>>1;
	if (nowl <= m)//询问区间和左儿子有交集 
	{
		if (m < nowr) return query(lson,nowl,nowr) + query(rson,nowl,nowr);//和右儿子有交集 
		else return query(lson,nowl,nowr);//只和左儿子有交集 
	}
	else return query(rson,nowl,nowr);//只和右儿子有交集 
}

build(root);
int l,r;
node ans = query(root,l,r); 

————————————————————

3 - 技巧及应用、其他

3/1 - 动态开点线段树

3/2 - LAZY标记固化(永久化

  【OI WIKI】如果确定懒惰标记不会在中途被加到溢出(即超过了该类型数据所能表示的最大范围),那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。具体如何处理与题目特性相关,需结合题目来写。这也是树套树和可持久化数据结构中会用到的一种技巧。

3/3 - 猫树

3/4 - 权值线段树

3/5 - 可持久化权值线段树(主席树

3/6 - 李超线段树

3/6 - zkw线段树

posted @ 2022-10-15 21:31  Ciaxin  阅读(34)  评论(0编辑  收藏  举报