特别浅的浅谈线段树

Segment_Tree

线段树好题大赏

定义

线段树是一种二叉搜索树,线段树的每个结点都存储了一个区间,也可以理解成一个线段。

用处

维护区间信息。线段树可以在 \(O(\log n)\) 的时间复杂度内实现单点修改,区间修改,区间查询等操作。

最典型的,也是最简单的就是 区间加区间求和

此题 为例,就代表最简单的线段树操作了。

树的储存

线段树比较直观的理解方式是看图:

(比较懒,直接从网上找了一个,我也不知道来自哪个 dalao 的博客)

这个图中每个节点有三个数:上边是点的编号,左边是区间的左端点,右边是区间的右端点。

个人习惯用结构体:

struct Seg_Tree{
	int le;//区间左端点
	int ri;//区间右端点
	int val;//区间维护的值,图中未展示
	int tag;//懒标记,之后会提到
}T[inf*4];

虽然图中的线段树不是每个节点都画了出来,但那些节点确确实实存在,因此线段树大概需要所维护的数组大小的 4 倍左右。

建树

对于线段树来说,每个节点 \(i\) 的左儿子的编号都是 \(2\times i\),右儿子的编号都是 \(2\times i+1\),同时区间大小对半分(见上图)。

通俗的理解一下,1 号节点代表整个区间,2 号节点代表左半个区间,3 号节点代表右半个区间。

这个代表可以是区间最值,区间和,区间 GCD 等题目要求维护的东西。

那以此题来说,就是区间和了。

建树的时候,先递归到叶子,将原数组的数分别赋到对应的叶节点,然后回溯时再将左右两个子树的权值加回来。

代码实现:

void build(int i,int l,int r)
{
	T[i].le=l;T[i].ri=r;
	if(l==r)
	{
		T[i].val=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(i*2,l,mid);
	build(i*2+1,mid+1,r);
	T[i].val=T[i*2].val+T[i*2+1].val;
}

区间求和

基本思路就是若覆盖则返回,否则递归到下一层合适的区间再返回。

举个例子,还是由上图,手模一下查询区间 \([3,5]\) 的和。

首先进入 1 号节点。

1 号节点太大,\([3,5]\) 无法将其覆盖,那么就找一号节点的两个子节点:2 号节点和 3 号节点。

2 号节点与 \([3,5]\) 有交集,3 号节点与 \([3,5]\) 并无交集,就递归到 2 号节点。同理,再递归到 4 号节点和 5 号节点。

这时注意 5 号节点。5 号节点不仅与 \([3,5]\) 有交集,而且能被其覆盖,那么这时就可以直接返回 5 号节点的权值,并在回溯时累加到答案中。

Code:

int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].val;
	int mid=(T[i].le+T[i].ri)>>1,ans=0;
	if(l<=mid)ans+=ask(i<<1,l,r);
	if(mid<r)ans+=ask(i<<1|1,l,r);
	return ans;
}

部分初学者可能会在递归传参的时候将区间写错(别问我怎么知道的),记住这句话:查询的区间不能变。

区间加

基本的思路和区间求和相似,同样是找交集。

但是每次都将所覆盖的区间全部更新的话复杂度是 \(O(n\log n)\),而暴力的时间复杂度是 \(O(n)\) 的,多个 \(\log\)

如果想要我们的线段树比暴力快的话,我们需要引入一个 懒惰标记 ,顾名思义,就是偷懒。此时的单次修改时间复杂度就成了 \(O(\log n)\)

因为每次修改不一定会马上被查询。比如上图中我更新 \([6,9]\),查询 \([3,5]\),那么这次的更新对下次的询问没有任何影响。我们就用懒标将这次更新储存下来,来表示 这个区间的每个元素都有 tag 还没加上

然后查询或者再次更新的时候,就将这个懒标下放到两个儿子上。

代码:

void update(int i,int l,int r,int k)
{
	if(l<=T[i].le&&T[i].ri<=r)
	{
		T[i].val+=k*(T[i].ri-T[i].le+1);
		T[i].tag+=k;
		return;
	}
	if(T[i].tag)pushdown(i);
	int mid=(T[i].l+T[i].r)>>1;
	if(l<=mid)update(i<<1,l,r,k);
	if(mid<r)update(i<<1|1,l,r,k);
	T[i].val=T[i<<1].val+T[i<<1|1].val;
}

pushdown 即为:

void pushdown(int i)
{
	T[i<<1].val+=T[i].tag*(T[i<<1].ri-T[i<<1].le+1);
	T[i<<1|1].val+=T[i].tag*(T[i<<1|1].ri-T[i<<1|1].le+1);
	T[i<<1].tag+=T[i].tag;
	T[i<<1|1].tag+=T[i].tag;
	T[i].tag=0;
}

那么区间求和也需要加上这个 pushdown

int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].val;
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1,ans=0;
	if(l<=mid)ans+=ask(i<<1,l,r);
	if(mid<r)ans+=ask(i<<1|1,l,r);
	return ans;
}

这里也有一个易混淆的点:懒标表示的是当前区间已经维护但其子区间仍未维护。注意区分。

完整代码

由于不是同一历史时期写的,可能码风会有不同,等有空了在维护吧。

const int inf=1e5+7;
int n,m,a[inf];
struct Seg_Tree{
	int le,ri;
	int val,tag;
}T[inf<<2];
void build(int i,int l,int r)
{
	T[i].le=l;T[i].ri=r;
	if(l==r)
	{
		T[i].val=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	T[i].val=T[i<<1].val+T[i<<1|1].val;
}
void pushdown(int i)
{
	T[i<<1].val+=(T[i<<1].ri-T[i<<1].le+1)*T[i].tag;
	T[i<<1|1].val+=(T[i<<1|1].ri-T[i<<1|1].le+1)*T[i].tag;
	T[i<<1].tag+=T[i].tag;
	T[i<<1|1].tag+=T[i].tag;
	T[i].tag=0;
}
void update(int i,int l,int r,int k)
{
	if(l<=T[i].le&&T[i].ri<=r)
	{
		T[i].val+=(T[i].ri-T[i].le+1)*k;
		T[i].tag+=k;
		return;
	}
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1;
	if(l<=mid)update(i<<1,l,r,k);
	if(mid<r)update(i<<1|1,l,r,k);
	T[i].val=T[i<<1].val+T[i<<1|1].val;
}
int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].val;
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1,ans=0;
	if(l<=mid)ans+=ask(i<<1,l,r);
	if(mid<r)ans+=ask(i<<1|1,l,r);
	return ans;
}
signed main()
{
	n=re();m=re();
	for(int i=1;i<=n;i++)
		a[i]=re();
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		int op=re(),l=re(),r=re();
		if(op==1)update(1,l,r,re());
		else wr(ask(1,l,r)),putchar('\n');
	}
	return 0;
}

习题

P1816

除了区间求和,动态区间 RMQ 问题也经常用线段树求解。

因为求和与 RMQ 都具有区间可加性,即知道两个子区间的和/最值就可以直接求出整个区间的和/最值。

不过对于静态区间 RMQ 问题,一个更快(指常数更小)的算法是 ST 表。

Code

const int inf=1e5+7;
int n,m,a[inf];
struct Seg_Tree{
	int le,ri,minn;
}T[inf<<2];
void build(int i,int l,int r)
{
	T[i].le=l,T[i].ri=r;
	if(l==r)
	{
		T[i].minn=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	T[i].minn=min(T[i<<1].minn,T[i<<1|1].minn);
}
int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].minn;
	int mid=(T[i].le+T[i].ri)>>1,ans=2147483647;
	if(l<=mid)ans=min(ans,ask(i<<1,l,r));
	if(mid<r)ans=min(ans,ask(i<<1|1,l,r));
	return ans;
}
int main()
{
	n=re();m=re();
	for(int i=1;i<=n;i++)
		a[i]=re();
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		int l=re(),r=re();
		wr(ask(1,l,r)),putchar(' ');
	}
	return 0;
}

P2574

区间取反操作,只是在 tag 上有些小动作,其他部分基本没有什么区别。

tag 表示这个区间有没有被翻转,显然翻转两次的区间和未翻转的区间相同,那么每次异或 1 就可以了。

Code

const int inf=2e5+7;
int n,m,a[inf];
struct Seg_Tree{
	int le,ri,siz;
	int val,tag;
}T[inf<<2];
void build(int i,int l,int r)
{
	T[i].le=l,T[i].ri=r;
	T[i].siz=r-l+1;
	if(l==r)
	{
		T[i].val=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	T[i].val=T[i<<1].val+T[i<<1|1].val;
}
void pushdown(int i)
{
	T[i<<1].val=T[i<<1].siz-T[i<<1].val;
	T[i<<1|1].val=T[i<<1|1].siz-T[i<<1|1].val;
	T[i<<1].tag^=1,T[i<<1|1].tag^=1;
	T[i].tag=0;
}
void update(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
	{
		T[i].val=T[i].siz-T[i].val;
		T[i].tag^=1;
		return;
	}
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1;
	if(l<=mid)update(i<<1,l,r);
	if(mid<r)update(i<<1|1,l,r);
	T[i].val=T[i<<1].val+T[i<<1|1].val;
}
int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].val;
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1,ans=0;
	if(l<=mid)ans+=ask(i<<1,l,r);
	if(mid<r)ans+=ask(i<<1|1,l,r);
	return ans;
}
int main()
{
	n=re();m=re();
	for(int i=1;i<=n;i++)
		scanf("%1d",&a[i]);
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		int op=re(),l=re(),r=re();
		if(op)wr(ask(1,l,r)),putchar('\n');
		else update(1,l,r);
	}
	return 0;
}

P1558

状压线段树,建议先了解部分位运算知识。

通过观察可以发现,颜色数并不多,可以用一个 int 存下来。那么区间的颜色数就是两个子区间的颜色数之并集,就是两个数取或即可。

Code

const int inf=1e5+7;
int n,m,k;
struct Seg_Tree{
	int le,ri;
	int val,tag;
}T[inf<<2];
void build(int i,int l,int r)
{
	T[i].le=l,T[i].ri=r;
	T[i].val=2;
	if(l==r)return;
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
}
void pushdown(int i)
{
	T[i<<1].val=T[i<<1|1].val=1<<T[i].tag;
	T[i<<1].tag=T[i<<1|1].tag=T[i].tag;
	T[i].tag=0;
}
void assign(int i,int l,int r,int k)
{
	if(l<=T[i].le&&T[i].ri<=r)
	{
		T[i].val=1<<k;
		T[i].tag=k;
		return;
	}
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1;
	if(l<=mid)assign(i<<1,l,r,k);
	if(mid<r)assign(i<<1|1,l,r,k);
	T[i].val=T[i<<1].val|T[i<<1|1].val;
}
int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].val;
	if(T[i].tag)pushdown(i);
	int mid=(T[i].le+T[i].ri)>>1,ans=0;
	if(l<=mid)ans|=ask(i<<1,l,r);
	if(mid<r)ans|=ask(i<<1|1,l,r);
	return ans;
}
int main()
{
	n=re();k=re();m=re();
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		char op[10]="";scanf("%s",op);
		int l=re(),r=re();
		if(l>r)l^=r^=l^=r;
		if(op[0]=='C')assign(1,l,r,re());
		else
		{
			int ls=ask(1,l,r),ans=0;
			while(ls)
			{
				if(ls&1)ans++;
				ls>>=1;
			}
			wr(ans),putchar('\n');
		}
	}
	return 0;
}

P4145

小清新线段树,就是在线段树上加剪枝。

可以发现,就算是 \(10^{12}\),在开 \(6\) 次平方之后也变成了 \(1\)

而且 \(\sqrt1=1\),那么如果这个区间已经全是 \(1\) 了,就可以不操作,否则暴力单点修改整个区间。

const int inf=1e5+7;
int n,m,a[inf];
struct Seg_Tree{
	int le,ri;
	int val,siz;
}T[inf<<2];
void build(int i,int l,int r)
{
	T[i].le=l,T[i].ri=r;
	T[i].siz=r-l+1;
	if(l==r)
	{
		T[i].val=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(i<<1,l,mid);
	build(i<<1|1,mid+1,r);
	T[i].val=T[i<<1].val+T[i<<1|1].val;
}
void update(int i,int l,int r)
{
	if(T[i].le==T[i].ri)
	{
		T[i].val=sqrt(T[i].val);
		return;
	}
	if(T[i].val==T[i].siz)return;
	int mid=(T[i].le+T[i].ri)>>1;
	if(l<=mid)update(i<<1,l,r);
	if(mid<r)update(i<<1|1,l,r);
	T[i].val=T[i<<1].val+T[i<<1|1].val;
}
int ask(int i,int l,int r)
{
	if(l<=T[i].le&&T[i].ri<=r)
		return T[i].val;
	int mid=(T[i].le+T[i].ri)>>1,ans=0;
	if(l<=mid)ans+=ask(i<<1,l,r);
	if(mid<r)ans+=ask(i<<1|1,l,r);
	return ans;
}
signed main()
{
	n=re();
	for(int i=1;i<=n;i++)
		a[i]=re();
	build(1,1,n);
	m=re();
	for(int i=1;i<=m;i++)
	{
		int op=re(),l=re(),r=re();
		if(l>r)l^=r^=l^=r;
		if(op)wr(ask(1,l,r)),putchar('\n');
		else update(1,l,r);
	}
	return 0;
}

线段树进阶

posted @ 2022-09-07 15:56  Zvelig1205  阅读(184)  评论(0编辑  收藏  举报