线段树

普通线段树

简介

线段树是一种时间复杂度为 \(O(log_N)\) 的维护区间信息的高级数据结构。

主要功能有:

  1. 区间修改
  2. 区间查询

下面是区间 \([1,9]\) 的线段树(未加点权)

主要操作是使用 build 建树,将叶子结点的信息通过 push_up 向上维护到更大的区间,加入了 lazytag 快速区间修改,再用 push_down 向下维护小区间,最后使用 query 进行查询。

下面以Luogu P3372 【模板】线段树 1为例题讲解

build

此函数用于建图,从更节点开始建图,最开始范围为 \([1,n]\),建图使用二分分治,每次分别递归维护左子树与右子树,知道遍历到叶子结点,把已知值附到叶子节点上,向上维护每个节点。

build 函数代码

void build(int p,int l,int r)//递归建树 
{
	tag[p]=0;//懒标记清空
	if(l==r)//叶子节点
	{
		tr[p]=a[l];//赋值
		return;//结束递归
	}
	int mid=l+r>>1;
	build(ls(p),l,mid);//二分分治左子树
	build(rs(p),mid+1,r);//二分分治右子树
	push_up(p);//向上维护
	return;//结束递归
}

push_up

向上维护节点,也就是当前节点的值为左儿子值加右儿子值。

push_up 函数代码

void push_up(int p){tr[p]=tr[ls(p)]+tr[rs(p)];}//向上维护,当前节点的和等于左节点的和加右节点的和

push_down

向下维护,因为使用了懒标记,所以需要把标记的值传递下去,有多少节点就有多少个单位的懒标记。

push_down 函数代码

void push_down(int p,int l,int r)//向下维护
{
	int mid=l+r>>1;
	tag[ls(p)]+=tag[p];//左子树懒标记的传递
	tag[rs(p)]+=tag[p];//右子树懒标记的传递
	tr[ls(p)]+=tag[p]*(mid-l+1);//左儿子的值加上一个数,mid-l+1表示在左子树内,当前要修改的个数,与tag[p]相乘即为当前增加的值
	tr[rs(p)]+=tag[p]*(r-mid);//右儿子的值加上一个数,r-mid表示在右子树内,当前要修改的个数,与tag[p]相乘即为当前增加的值
	tag[p]=0;//当前节点懒标记清空
	return;
}

lazytag

标记懒标记,输入的修改区间需要分配到每个节点上,如果此节点代表区间被完全覆盖,则说明此区间被选取,赋予其懒标记,在进行向下维护,最后向上维护确保正确。

lazytag 函数代码

void lazytag(int pl,int pr,int l,int r,int p,int k)//懒标记
{
	if(pl<=l&&pr>=r)//被包含
	{
		tr[p]+=k*(r-l+1);//r-l+1为区间包含的节点个数,总的增加量即修改量乘节点个数
		tag[p]+=k;//懒标记的叠加
		return;
	}
	push_down(p,l,r);//向下维护
	int mid=l+r>>1;
	if(pl<=mid) lazytag(pl,pr,l,mid,ls(p),k);//在左边
	if(pr>mid) lazytag(pl,pr,mid+1,r,rs(p),k);//在右边
	push_up(p);//向下维护
	return;
}

query

询问操作,与 lazytag 基本相同,也是将询问区间分配到每个节点上。

query 函数代码

int query(int pl,int pr,int l,int r,int p)
{
	int ans=0;
	if(pl<=l&&pr>=r) return tr[p];//完全被包含,直接返回此段长度
	push_down(p,l,r);//向下维护
	int mid=l+r>>1;
	if(pl<=mid) ans+=query(pl,pr,l,mid,ls(p));//剩下的在左边
	if(pr>mid) ans+=query(pl,pr,mid+1,r,rs(p));//剩下的在右边
	return ans;//返回答案
}

完整代码

AC Code of P3372 【模板】线段树 1

#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define rep1(i,l,r) for(int i=l;i<=r;i++)
#define rep2(i,l,r) for(int i=l;i>=r;i--)
const int N=1e6+10;
using namespace std;
int n,m,opt,x,y,a[N],tr[N],tag[N];
inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return f*x;
}
int ls(int x){return x<<1;}//左儿子
int rs(int x){return x<<1|1;}//右儿子
void push_up(int p){tr[p]=tr[ls(p)]+tr[rs(p)];}//向上维护,当前节点的和等于左节点的和加右节点的和 
void build(int p,int l,int r)//递归建树 
{
	tag[p]=0;//懒标记清空
	if(l==r)//叶子节点
	{
		tr[p]=a[l];//赋值
		return;//结束递归
	}
	int mid=l+r>>1;
	build(ls(p),l,mid);//二分分治左子树
	build(rs(p),mid+1,r);//二分分治右子树
	push_up(p);//向上维护
	return;//结束递归
}
void push_down(int p,int l,int r)//向下维护
{
	int mid=l+r>>1;
	tag[ls(p)]+=tag[p];//左子树懒标记的传递
	tag[rs(p)]+=tag[p];//右子树懒标记的传递
	tr[ls(p)]+=tag[p]*(mid-l+1);//左儿子的值加上一个数,mid-l+1表示在左子树内,当前要修改的个数,与tag[p]相乘即为当前增加的值
	tr[rs(p)]+=tag[p]*(r-mid);//右儿子的值加上一个数,r-mid表示在右子树内,当前要修改的个数,与tag[p]相乘即为当前增加的值
	tag[p]=0;//当前节点懒标记清空
	return;
}
void lazytag(int pl,int pr,int l,int r,int p,int k)//懒标记
{
	if(pl<=l&&pr>=r)//被包含
	{
		tr[p]+=k*(r-l+1);//r-l+1为区间包含的节点个数,总的增加量即修改量乘节点个数
		tag[p]+=k;//懒标记的叠加
		return;
	}
	push_down(p,l,r);//向下维护
	int mid=l+r>>1;
	if(pl<=mid) lazytag(pl,pr,l,mid,ls(p),k);//在左边
	if(pr>mid) lazytag(pl,pr,mid+1,r,rs(p),k);//在右边
	push_up(p);//向下维护
	return;
}
int query(int pl,int pr,int l,int r,int p)
{
	int ans=0;
	if(pl<=l&&pr>=r) return tr[p];//完全被包含,直接返回此段长度
	push_down(p,l,r);//向下维护
	int mid=l+r>>1;
	if(pl<=mid) ans+=query(pl,pr,l,mid,ls(p));//剩下的在左边
	if(pr>mid) ans+=query(pl,pr,mid+1,r,rs(p));//剩下的在右边
	return ans;//返回答案
}
signed main()
{
	n=read();
	m=read();
	rep1(i,1,n) a[i]=read();
	build(1,1,n);//建图
	while(m--)
	{
		opt=read();
		x=read();
		y=read();
		if(opt==1) lazytag(x,y,1,n,1,read());//标记
		else cout<<query(x,y,1,n,1)<<endl;//输出
	}
	return 0;
}


可持久化权值线段树

简介

给线段树增加历史点来维护历史数据,使得我们能在较短时间内查询历史数据。

完整代码

AC Code of Luogu P3834 【模板】可持久化线段树 2

#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define rep1(i,l,r) for(int i=l;i<=r;i++)
#define rep2(i,l,r) for(int i=l;i>=r;i--)
const int N=1e5+10;
using namespace std;
int n,m,a[N],root[N],idx;
vector<int> v;
struct node
{
	int l,r;
	int cnt;
}tr[N*21];
inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return f*x;
}
int build(int l,int r)
{
	int p=++idx;//新状态
	if(l==r) return p;//遍历到叶子结点了
	int mid=l+r>>1;
	tr[p]={build(l,mid),build(mid+1,r),0};//递归建树并存值
	return p;
}
int insert(int p,int id,int l,int r)//插入
{
	int q=++idx;//新状态
	tr[q]=tr[p];//继承上一状态
	if(l==r)//叶子结点
	{
		++tr[q].cnt;//个数加一
		return q;
	}
	int mid=l+r>>1;
	if(id<=mid) tr[q].l=insert(tr[p].l,id,l,mid);//在左子树
	else tr[q].r=insert(tr[p].r,id,mid+1,r);//在右子树
	tr[q].cnt=tr[tr[q].l].cnt+tr[tr[q].r].cnt;//向上维护
	return q;
}
int query(int p,int q,int k,int l,int r)//询问
{
	if(l==r) return r;//叶子结点
	int mid=l+r>>1;
	int cnt=tr[tr[q].l].cnt-tr[tr[p].l].cnt;//方便计算
	if(k<=cnt) return query(tr[p].l,tr[q].l,k,l,mid);//第k小在左子树
	else return query(tr[p].r,tr[q].r,k-cnt,mid+1,r);//第k小在右子树
}
signed main()
{
	n=read();
	m=read();
	rep1(i,1,n) v.push_back(a[i]=read());
	sort(v.begin(),v.end());
	v.erase(unique(v.begin(),v.end()),v.end());//离散化
	root[0]=build(0,v.size()-1);//最初始状态
	rep1(i,1,n) root[i]=insert(root[i-1],lower_bound(v.begin(),v.end(),a[i])-v.begin(),0,v.size()-1);//插入,得到新的状态
	while(m--)
	{
		int l=read();
		int r=read();
		int k=read();
		cout<<v[query(root[l-1],root[r],k,0,v.size()-1)]<<endl;//询问
	}
	return 0;
}
posted @ 2023-10-26 18:19  Symbolize  阅读(49)  评论(0)    收藏  举报