线段树及其应用

Nick zheng blog

# 线段树及其应用

## **特别声明: 以下仅为个人直观理解, 并不都正确严谨**


线段树($Segment Tree$), 顾名思义就是一颗节点由一条条"线段"构成的树. 这是一种十分高效的数据结构, 可以在$O(nlogn)$的时间复杂度内解决很多复杂的区间(单点)操作qaq.

考虑把一个区间分为两个长度相等的区间, 记为当前节点$u$的左孩子$u«1$和右孩子$u«1|1$($2n$ & $2n+1$). 那么我们可以分割需要修改/查询的区间为很多部分, 然后直到分割到叶子结点, 然后汇总/修改即可.

但是这样的时间复杂度并不优, 我们发现如果当前区间中有被完全包含的节点, 那么就可以直接使用其中提前汇总好的数据(或者使用懒标记$lazy$, 等到再次查询/修改的时候再下放给子节点), 从而优化时间复杂度为$O(nlogn)$.

那么我们需要一下几个函数来实现整个线段树的功能. 详见代码([P3372 【模板】线段树 1](https://www.luogu.com.cn/problem/P3372))qaq.


\#include<bits/stdc++.h>

\#define int long long

\#define md tr\[u].l+((tr\[u].r-tr\[u].l)>>1)//注意是当前节点的中点作为当前区间的分割线

using namespace std;

const int N=1e5+10;

struct node//节点

{

 	int l,r,data,lazy;

}tr\[N<<2];//一定要开四倍空间!!!

int a\[N];

void pushup(int u)//由子节点更新父节点的数据

{

 	tr\[u].data=tr\[u<<1].data+tr\[u<<1|1].data;

}

void pushdown(int u)//下放懒标记

{

 	tr\[u<<1].lazy+=tr\[u].lazy;

 	tr\[u<<1|1].lazy+=tr\[u].lazy;//懒标记直接传给下一代

 	tr\[u<<1].data+=tr\[u].lazy\*(tr\[u<<1].r-tr\[u<<1].l+1);

 	tr\[u<<1|1].data+=tr\[u].lazy\*(tr\[u<<1|1].r-tr\[u<<1|1].l+1);//孩子数据更新

 	tr\[u].lazy=0;//清空下放过的懒标记

}

void build(int u,int l,int r)//建树

{

 	if(l==r)//不能再分割了

 	{

 		tr\[u]={l,r,a\[l],0};//直接赋值

 		return;

 	}

 	tr\[u]={l,r,0,0};

 	int mid=md;

 	build(u<<1,l,mid);//递归左右孩子

 	build(u<<1|1,mid+1,r);

 	pushup(u);//一定不要忘了用子节点更新父节点(u)

}

void modify(int u,int l,int r,int x)//区间修改(单点修改可以看做l=r时的特殊情况)

{

 	if(r<tr\[u].l||l>tr\[u].r)//修改的区间不在当前节点范围内

 	{

 		return;

 	}

 	if(l<=tr\[u].l\&\&r>=tr\[u].r)//当前节点被完全包含于查询区间

 	{

 		tr\[u].lazy+=x;//打懒标记, 避免时间浪费

 		tr\[u].data+=x\*(tr\[u].r-tr\[u].l+1);//更新区间的汇总数据

 		return;

 	}

 	pushdown(u);//修改的时候一定要先下放懒标记qaq(有事有可能对修改有影响)!

 	modify(u<<1,l,r,x);//修改范围不用折半, 因为上面用if特判过了

 	modify(u<<1|1,l,r,x);

 	pushup(u);//修改完了一定要更新一下父节点

}

int query(int u,int l,int r)

{

 	if(r<tr\[u].l||l>tr\[u].r)//同理, 没被包含舍弃

 	{

 		return 0;

 	}

 	if(l<=tr\[u].l\&\&r>=tr\[u].r)//同理, 完全包含直接返回节点汇总值

 	{

 		return tr\[u].data;

 	}

 	pushdown(u);//查询时一定要下方懒标记才能查到真实值

 	return query(u<<1,l,r)+query(u<<1|1,l,r);//合并左右子树的数据, 注意查询区间不折半(同修改, 上面特判过了)

}

signed main()

{

 	int n,m;

 	scanf("%lld %lld",\&n,\&m);

 	for(int i=1;i<=n;i++)

 	{

 		scanf("%lld",\&a\[i]);

 	}

 	build(1,1,n);

 	for(int i=1;i<=m;i++)

 	{

 		int op,x,y;

 		scanf("%lld %lld %lld",\&op,\&x,\&y);

 		if(op==1)

 		{

 			int k;

 			scanf("%lld",\&k);

 			modify(1,x,y,k);//第一个参数是当前节点编号

 		}

 		else

 		{

 			printf("%lld\\n",query(1,x,y));

 		}

 	}

 	return 0;

}

以上是区间和的一个线段树, 其实线段树可以做更多操作, 只要这个操作符合以下对比表格:

|数据结构(nlogn)|维护的操作要求 |举例(并不全面) |

|:——–:|:——–:|:——–:|

|ST表 |1. 结合律, 交换律 2. 幂等性(f(a,a)=a)|最大最小, GCD(LCM特例也可), 与或(看做对每一位求最值)|

|树状数组 |1. 结合律, 交换律 2. 有逆运算|加, 乘, 异或 |

|线段树 |1. 结合律, 交换律|赋值, 加, 乘, 与或非异或, 最小最大, GCD/LCM, 矩阵乘法|

可见线段树除了比较难写, 常数比较大以外, 还是比较全能的(最后有常用操作的模板). 但是, 如果要进行多种操作, 我们就必须考虑维护的优先级了qaq.

一般需要考虑的有:

1. 加和乘并存: 先乘后加

2. 先赋值后加

(可参考[P3373 【模板】线段树 2](https://www.luogu.com.cn/problem/P3373))代码:


然后是动态开点. 动态开点, 就是解决当节点个数过多, 但很多节点都是无意义的(为$0$, 一般是多次询问但有些节点因数据规模不会被改变), 我们一般采用记录左右孩子的编号来替换$u«1$和$u«1|1$. 如果当前节点指向的孩子编号为$0$, 那么可以认为此子树内没有重要的数据.(当然, 动态开点就不需要建树函数了) 具体实现如下:


下面是线段树的几种变体

1. 值域线段树(权值线段树)

2. zkw线段树

3. 李超线段树

4. 猫树

5. 吉司机线段树

6. 树套树

7. 可持久化

这里只给出前两个的介绍, 其余的请学习其他大佬的博客(逃, 以后填坑


线段树中有几个tricks:

1. 扫描线算法

2. 标记永久化

3. 线段树优化建图

4. 线段树上跑dfs