luqiuyu

导航

数据结构2------线段树

线段树

上次我们讲解了夹在数据结构和树论中的树链剖分现在我们将树链剖分的序列版本,实际上树链剖分是线段树的树版本俗话说,我们要想理解一个庞大的数据结构,那么我们需要将他的1.思想,2.实现,3.理解

第一步 思想

先给出例题

给你一个序列,有q次查询/修改,1.单点加 2.区间查询和,若n为序列长度,\(1 \le n,q \le 10^5\)

首先考虑暴力,时间为\(O(nq)\)现在思考,如何优化唉,我们发现,区间可以分成许多小区间,然后解决(实际上就是分治)那么我们直接造一个树,每个节点存的是一个区间!那么我们可以将区间操作改为在这棵树上的操作,但并不是树链剖分

这棵树长这样:

![ea2c99844c364c8183fc698862679334](file:///C:/Users/27681/Pictures/Typedown/ea2c9984-4c36-4c81-83fc-698862679334.png?msec=1767525599562)

d就是这个区间维护的东东,可以是个NODE

第二部 实现

1.建树

首先,我们采用递归实现

我们观察上面的树,我们可以发现叶子结点是\(l==r\)的点

那么\([l,l]\)的区间和为多少呢?

不就是\(a_l\)么?

除了叶子结点,我们还有其他节点

这些节点我们将它的左右儿子给递归

建议先递归左,再递归右

左儿子就是\([l,mid]\),右儿子就是\((mid,r]\)

最终递归回来,d[左儿子]+(node结构体的定义加)d[右儿子],结束!

对于例题,我们就直接d[左儿子]+d[右儿子]

由于我们经常使用d数组来存,所以它的左儿子为\(x<<1\),右儿子为\(x<<1|1\)

所以我们放出建树代码

void build(int x,int l, int r){
  //建树
    if(l == r){
        t[x] = a[l];//赋值
        return ;
    }
    ll mid = (l + r) >> 1;//可能超int,需要看着办,然后ll为long long
    build(x<<1,l,mid);//递归左儿子
    build(x<<1|1,mid+1,r);//递归右儿子
    t[x] = t[x<<1]+t[x<<1|1];//t数组处理,t=d
}

既然我们说了,用数组存,那空间开多大,是多少\(n\)?

证明不在这里展开,可以上OI-wiki寻找,有详细证明,最后证明出节点个数最大为\(4n-5\)

所以我们直接开\(4n\)大小

易错!!!

2.查询

查询也很简单

我们先看我们到达的区间是否是查询的区间的子集,若是,直接返回这个区间的区间和,也就是t[x]

若不是,那么我们将这个区间分成左儿子和右儿子

然后看,若左儿子在查询区间内,那么递归

若右儿子在查询区间内,那么递归

最后将递归的左右儿子的和加起来,就是答案

劝大家分三种讨论,这样以后查询模版就基本上不用改了

给出代码

ll ask(ll x,ll l, ll r, ll L, ll R){
	if(L <= l && r <= R) return t[x]; 
	//push_down(x,l,r); 后面有这个操作,在单点+区间中不用加
	ll mid = (l + r) >> 1;
	if(mid >= L && mid < R) return ask(x<<1,l,mid,L, R) + ask(x<<1|1, mid + 1, r, L, R);
	if(mid >= L) return ask(x << 1, l, mid, L, R);
	return ask(x << 1|1, mid + 1, r, L,R);
}

3.修改

和查询没多大改变,这里不细讲

现在我们改成区间加,区间和

怎么求?

我们可以做一个懒标记,可以优化时间

首先,若我们现在所在区间\([l,r]\)在修改区间\([L,R]\)中,那么我们直接改,懒标记加上这次所加上的钱,但儿子就不要递归,相当于将它的工资独吞

然后没有包含,哎呀,要把以前贪污的钱给儿子,在修改前将旧账发下去,自己贪污的钱就归0

然后根据情况做change,注意一点,我们单点和区间查询后面都要把自己的区间和修改成儿子的区间和相加

区间查询基本上就是在查询中下放,就是上文代码中的push_down

给出代码:

void add(ll x,ll l,ll r, ll w){
	t[x] += (r-l+1)*w;
	tag[x] += w;
}
void push_down(ll x,ll l,ll r){
	if(tag[x]){
		ll mid = (l + r) >> 1;
		add(x<<1,l,mid,tag[x]);
		add(x<<1|1,mid+1,r,tag[x]);
		tag[x] = 0;
	}
} 
void change(ll x,ll l, ll r, ll L, ll R, ll w){
	if(L <= l && r <= R){
		add(x,l,r, w);
		return ;
	}
	push_down(x,l,r);
	ll mid = (l + r) >> 1;
	if(mid >= L) change(x<<1,l,mid,L,R,w);
	if(mid < R) change(x<<1|1, mid + 1, r, L,R,w);
	t[x] = t[x<<1] +t[x<<1|1];
}

然后这道例题我们就可以颗秒掉了

对了,如果遇到区间修改+单点查询,怎么办呢?留作思考题,不要使用懒标记

3.理解

以下内容涉及一道名副其实的黑题,可以回绝

我们知道,有些序列问题我们线段树有可能会炸掉,需要用到分块,莫队等操作

现在抛出一个疑问:线段树可以维护什么信息?

我们先看一道黑题

(对数据结构的爱) 说给你一个函数,他的模很奇怪,题目有写这个模函数,用这个模去写一个区间和查询

这道题目强制在线,杀掉了离线算法

分块思想没有,只有一个线段树,怎么办?

区间和,就是将答案减去(减p的次数乘上p)

考虑一个函数f(x)最初是x时,会减去多少个p

然后我们就需要证明一个定理:这个问题不同种类的数量只有\(r-l+2\)

炸裂,不会证,老师的证明也忘了

彻底炸裂

反正都知道这是对的,继续做吧

那么这道题我们并不是要想这个定理,而是这个线段树维护的信息

我们注意到,我们写过的线段树题目,他维护的信息是一个半群结构

而这个函数,他满足半群结构!

那么题解只有一句话:维护一个函数f(x):最初是x时,会减去多少个p,满足半群结构,上线段树维护

\(Good!\)


这道题为啥说这事名副其实的黑题呢?因为他并不是线段树板子题,他让人明白了线段树可以维护那些信息,他并不在是我们前面所说的区间,也不是什么乱七八糟的东西

而是一个半群!!!

习题:洛谷:P5609,P1253,P6327

作者:LQY

参考:yixiuge777

posted on 2026-01-04 19:55  秋天的宇宙  阅读(0)  评论(0)    收藏  举报