线段树学习笔记

很久之前的码风了,函数名不是很好看。

定义

线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。-oiwiki

用途

对于区间的维护(包括查询,修改)操作,且要求时间复杂度在 \(O(\log n)\) 以内时,可以考虑线段树做法。

做法

线段树分为建树,区间修改,区间查询,单点查询,单点修改等操作。其中每个操作又分为一些小操作,我们逐一讲解。
本博客线段树不为结构体线段树。

建树操作:结构体线段树的基础

需要注意,为了维护每个结点的信息,我们需要一个数组来记录每个结点的值,这里,我定义这个数组为 \(tr\),需要注意,\(tr\) 的大小一般要是题目中给的序列的元素个数的 \(4\) 倍,其原因是我们每一次的编号至少都是 \(k \times 2\)\(k\) 的含义见下)。

  • 定义:我们一般称建树操作为 \(build\) ,对于每一次建树操作,我们都将区间 \([l, r]\) 分为 \([l, mid]\)\([mid + 1, r]\)。这两个区间分别为区间 \([l,r]\) 的子区间, \([l,r]\) 即为两个区间的父区间,在线段树上也就是它的左右儿子,在以下我们为了好表述,使用父结点和子结点来代替父区间和儿子区间。若结点 \([l,r]\) 编号为 \(k\) 其子结点编号就分别为 \(k \times 2,k \times 2 + 1\)
  • “递”:之后我们从区间 \([1,n]\) 开始,依次向下进行“递”,直到遍历到叶子结点,即 \(l==r\) 的情况时,我们用线段树来记录这个结点的值,并进行“归”操作。
  • “归”:在“归”的时候我们也不能闲着,对于一个父区间,其值可以为两个子区间合并起来的值。
    例如对于每一个区间,我们去维护其每个区间的和,那么父结点的值就是左右儿子的值的和,即 \(tr[k] = tr[k \times 2] + tr[k \times 2 + 1]\),这个“归”的操作也就是我们说的 \(pushup\) 操作。

下面我们假设用线段树维护区间和。

代码:

inline void pushup(int k){tr[k] = tr[k * 2] + tr[k * 2 + 1];}
inline void build(int l,int r,int k)
{
    //因为 [l,r] 表示的是序列 a 的一个区间,所以区间 [l,l] 即为 a 第 l 个数 
	if(l == r){tr[k] = a[l];return ;}//如果是叶子结点,就赋值返回
	int mid = l + r >> 1; 
	build(l, mid, k * 2);
	build(mid + 1, r, k * 2 + 1);//建树
	pushup(k);//pushup操作
}

单点操作

我们先讲单点修改及单点查询,这个平常用树状数组就可以实现,所以这里不细讲。

对于一个单点,我们在每一次寻找它的时候去判断其在左儿子结点还是右儿子结点(就是和 \(mid\) 判断)如果在左就只进入左儿子结点,否则就进入右儿子结点。当左右区间相等也就是到叶子结点时,表示的就是在线段树上这个点,更改其值。注意在“归”时修改父结点的值就行。

代码实现:

//x表示要更改或查找的点 l,r 表示这个结点所维护的区间 k 表示这个结点的编号 val表示值
inline void point_change(int k, int l, int r, int x, int val)
{
	if(l == r && l == x){tr[k] += val;return ;}//修改
	int mid = l + r >> 1;
	if(x <= mid) point_change(k * 2, l, mid, x, val);
	else point_change(k * 2 + 1, mid + 1, r, x, val);//单点查询不是左就是右
	pushup(k);//修改父亲,即“归”
}//单点修改
inline int point_query(int k, int l, int r, int x)
{
	if(l == r && l == x){return tr[k];}//如果找到了就返回这个节点的值
	int mid = l + r >> 1;
	if(x <= mid) return point_query(k * 2, l, mid, x);
	else return point_query(k * 2 + 1, mid + 1, r, x);//基本和单点修改一样
}//单点查询

区间修改及查询

区间修改的情况比较复杂,和单点修改的情况基本不同,并且引入了线段树优化的一个技巧:懒标记。

首先先讨论区间查询,不讨论使用懒标记。

20190214231602648网上随便找的图片。

我们以图片所示,当我们查找 \([0,3]\) 这个区间的时候,我们发现可以直接找到 \([0,3]\)。同时我们还需要更新 \([0, 3]\) 的子节点的值。
那如果查找 \([0,4]\) 这个区间时,我们发现 \([0,3]\) 这个结点似乎不能完全包裹 \([0,4]\) 这个区间。那么我们就需要查找右结点 \([4,7]\)
可见,区间查找并不像单点查询不是左结点就是右结点。而判断进入左右结点的条件就是是否比区间的中间值(即 \(mid\))大。

所以我们就可以思考,那就输入一个区间,如果要查找区间的左端点小于等于中间值,就进入左结点,如果要查找区间的右端点大于等于中间值+1,即若查找区间为 \([x,y]\) 那么对应的进入子节点的条件分别是 \(x \le mid\), \(mid + 1\le y\)

那么我们就可以写出如下代码:

inline void change(int x,int y,int l,int r,int k,int v)
{
    if(l == r){tr[k] += v;return ;} //更改操作,如果是叶子节点,直接更改
	if(x <= l && r <= y){tr[k] += v;}//如果 [x,y](查询区间) 完全包裹 [l,r](结点表示的区间) 就直接更改这个区间的值
	int mid = l + r >> 1;
	if(x <= mid) change(x, y, l, mid ,k * 2, v);
	if(y >= mid + 1) change(x, y, mid + 1, r, k * 2 + 1, v);//向下更改
	pushup(k);//注意返回父亲
}

随手写的,不一定能过(毕竟不咋用)。

但是我们发现这样每一次都得遍历到叶子结点,复杂度很大。
那么就有一种方法可以优化时间复杂度:懒标记

懒标记(代码中用 \(tag\) 数组表示)就是一个标记。
对于一个结点,如果完全被包裹于查询区间,那么就标记一下节点所表示区间更改的值,并且更改这个节点的值,然后就不再管了。
如果在查询和更改操作中发现需要向下找到一个其子节点或孙子结点,就将标记下放(即 \(pushdown\) 操作),这样就可以做到减少遍历次数的优化。

需要注意,因为标记已经下放给子节点了那么本结点的标记就无用了,为了不影响值,我们需要赋值为 \(0\)

代码:

inline void noc(int l,int r,int k,int v)//即进行懒标记
{
	tr[k] += (r - l + 1) * v;//区间 l,r 所增加的值也就是 (r-l+1) 这个区间长度和所修改值的乘积
	tag[k] += v;//tag数组就是懒标记数组
}
inline void pushdown(int l,int r,int k)//标记下放
{
	if(tag[k] != 0)//如果没有标记就不需要多操作
	{
		int mid = l + r >> 1;
	 	noc(l, mid, k * 2,tag[k]);
		noc(mid + 1, r, k * 2 + 1,tag[k]);
		tag[k] = 0;//赋值为0
	}
}
inline void change(int x,int y,int l,int r,int k,int v)//区间修改 [x,y]为修改区间 [l,r] 为结点维护区间 k 为编号 v 为更改的值
{
	if(x <= l && r <= y){noc(l, r, k, v);return ;}//如果完全包裹,就进行标记就可以
	pushdown(l, r, k);//否则就标记下放
	int mid = l + r >> 1;
	if(x <= mid) change(x, y, l, mid ,k * 2, v);
	if(y >= mid + 1) change(x, y, mid + 1, r, k * 2 + 1, v);//重新向下修改
	pushup(k);//修改本结点的值
}
inline int query(int x,int y,int l,int r,int k)
{
	if(x <= l && r <= y) return tr[k];//如果完全包括,就返回值
	pushdown(l, r, k);//否则就标记下放
	int res = 0,mid = l + r >> 1;
	if(x <= mid) res += query(x, y, l, mid, k * 2);
	if(y >= mid + 1) res += query(x, y, mid + 1, r, k * 2 + 1);
	return res;//记录所有值以后返回
}

例题

luogu线段树1

板子中的板子,代码讲解都在上面,直接粘代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename T> inline void rd(T &x)
{
	x = 0;int f = 1;char c;
	do{c = getchar();if(c == '-') f = -1;}while(!isdigit(c));
	do{x = x * 10 + c - '0';c = getchar();}while(isdigit(c));
	x *= f;
}
const int N = 1e5 + 66;
int a[N],tr[N << 2],tag[N << 2],ac[N],n,q,m;
inline void pushup(int k){tr[k] = tr[k * 2] + tr[k * 2 + 1];}
inline void build(int l,int r,int k)
{
	if(l == r){tr[k] = a[l];return ;}
	int mid = l + r >> 1; 
	build(l, mid, k * 2);
	build(mid + 1, r, k * 2 + 1);
	pushup(k);
}
inline void noc(int l,int r,int k,int v)
{
	tr[k] += (r - l + 1) * v;
	tag[k] += v;
}
inline void pushdown(int l,int r,int k)
{
	if(tag[k] != 0)
	{
		int mid = l + r >> 1;
	 	noc(l, mid, k * 2,tag[k]);
		noc(mid + 1, r, k * 2 + 1,tag[k]);
		tag[k] = 0;
	}
}
inline void change(int x,int y,int l,int r,int k,int v)
{
	if(x <= l && r <= y){noc(l, r, k, v);return ;}
	pushdown(l, r, k);
	int mid = l + r >> 1;
	if(x <= mid) change(x, y, l, mid ,k * 2, v);
	if(y >= mid + 1) change(x, y, mid + 1, r, k * 2 + 1, v);
	pushup(k);
}
inline int query(int x,int y,int l,int r,int k)
{
	if(x <= l && r <= y) return tr[k];
	pushdown(l, r, k);
	int res = 0,mid = l + r >> 1;
	if(x <= mid) res += query(x, y, l, mid, k * 2);
	if(y >= mid + 1) res += query(x, y, mid + 1, r, k * 2 + 1);
	return res;
}
signed main()
{
	rd(n),rd(q);
	for(int i = 1;i <= n;i ++) rd(a[i]);
	build(1, n, 1);
	for(int i = 1;i <= q;i ++)
	{
		int od,x,y,v;
		rd(od),rd(x),rd(y);
		if(od == 1){rd(v),change(x, y, 1, n, 1, v);}
		else cout << query(x, y, 1, n, 1) << endl;
	}
	return 0;
}

luogu线段树2

多加个懒标记表示乘的数,再通过乘法分配律进行 \(O(1)\) 维护即可。其实也就是在 \(pushdown\)\(noc\) 里多加几行而已。
需要注意,因为另一个标记维护的是乘法,所以无论有没有修改都需要进行 \(noc\) 操作,并且赋值应该变为 \(1\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
template<typename T> inline void rd(T &x)
{
	x = 0;int f = 1;char c;
	do{c = getchar();if(c == '-') f = -1;}while(!isdigit(c));
	do{x = x * 10 + c - '0';c = getchar();}while(isdigit(c));
	x *= f;
}
const int N = 1e5 + 66;
int a[N],tr[N << 2],tag[N << 2],mul[N << 2],n,q,m;
inline void pushup(int k){tr[k] = tr[k * 2] + tr[k * 2 + 1];}
inline void build(int l,int r,int k)
{
	mul[k] = 1;
	if(l == r){tr[k] = a[l];return ;}
	int mid = l + r >> 1; 
	build(l, mid, k * 2);
	build(mid + 1, r, k * 2 + 1);
	pushup(k);
}
inline void noc(int l,int r,int k,int v,int ac)
{
	tag[k] = (tag[k] * ac + v) % m;
	mul[k] = (mul[k] * ac) % m;
	tr[k] = (tr[k] * ac + (r - l + 1) * v) % m;
}
inline void pushdown(int l,int r,int k)
{
	int mid = l + r >> 1;
	noc(l, mid, k * 2, tag[k], mul[k]);
	noc(mid + 1, r, k * 2 + 1, tag[k], mul[k]);
	tag[k] = 0;mul[k] = 1;
}
inline void change(int x,int y,int l,int r,int k,int v,int ac)
{
	if(x <= l && r <= y){noc(l, r, k, v, ac);return ;}
	pushdown(l, r, k);
	int mid = l + r >> 1;
	if(x <= mid) change(x, y, l, mid ,k * 2, v, ac);
	if(y >= mid + 1) change(x, y, mid + 1, r, k * 2 + 1, v, ac);
	pushup(k);
}
inline int query(int x,int y,int l,int r,int k)
{
	if(x <= l && r <= y) return tr[k];
	pushdown(l, r, k);
	int res = 0,mid = l + r >> 1;
	if(x <= mid) res = (res + query(x, y, l, mid, k * 2)) % m;
	if(y >= mid + 1) res = (res + query(x, y, mid + 1, r, k * 2 + 1)) % m;
	return res;
}
signed main()
{
	rd(n),rd(q),rd(m);
	for(int i = 1;i <= n;i ++) rd(a[i]);
	build(1, n, 1);
	for(int i = 1;i <= q;i ++)
	{
		int od,x,y,v;
		rd(od),rd(x),rd(y);
		if(od == 1){rd(v),change(x, y, 1, n, 1, 0, v);}
		else if(od == 2){rd(v),change(x, y, 1, n, 1, v, 1);}
		else cout << query(x, y, 1, n, 1) << endl;
	}
	return 0;
}
posted @ 2025-09-28 19:50  medal_dreams  阅读(44)  评论(0)    收藏  举报