线段树学习笔记

什么是线段树

线段树是一种基于分治思想的二叉树结构,用于在区间上进行信息统计,比树状数组更为通用、直观,支持单点修改、区间修改、区间查询。

线段树维护的数据具有可并性,比如区间和、区间积、区间最值等等。

模板

建树

void build(int l,int r,int p)
{
	tre[p].l=l;tre[p].r=r;
	if(l==r)
    {
      //magic
      return;
    }
	int mid=(l+r)/2;
	build(l,mid,p<<1);
	build(mid+1,r,(p<<1)+1);
  psu(p);
}

修改

void upd1(int k,int x,int p)//单点修改
{
	if(tre[p].l==tre[p].r)
	{
		tre[p].v=k;return;
	}
	int mid=(tre[p].l+tre[p].r)/2;
	if(x<=mid)upd(k,x,p<<1);
	else upd(k,x,(p<<1)+1);
	tre[p].v=max(tre[p<<1].v,tre[(p<<1)+1].v);
}
void upd2(int l,int r,int k,int p)//区间修改
{
	int nl=lf[p],nr=rt[p];
	if(l<=nl&&nr<=r)
	{
		vl[p]+=1ll*k*(nr-nl+1);
		tg[p]+=k;
		return;
	}
	spr(p);
	int mid=(nl+nr)/2;
	if(l<=mid)upd(l,r,k,p<<1);
	if(r>mid)upd(l,r,k,(p<<1)+1);
	vl[p]=vl[p<<1]+vl[(p<<1)+1];
}

向下传递

void psd(int p)
{
	if(tg[p])
	{
		vl[p<<1]+=tg[p]*(rt[p<<1]-lf[p<<1]+1);
		tg[p<<1]+=tg[p];
		vl[(p<<1)+1]+=tg[p]*(rt[(p<<1)+1]-lf[(p<<1)+1]+1);
		tg[(p<<1)+1]+=tg[p];
		tg[p]=0;
	}
}

区间查询

long long query(int l,int r,int p)
{
	int nl=lf[p],nr=rt[p];
	if(l<=nl&&nr<=r)return vl[p];
	spr(p);
	int mid=(nl+nr)/2;
	long long val=0;
	if(l<=mid)val+=query(l,r,p<<1);
	if(r>mid)val+=query(l,r,(p<<1)+1);
	return val;
}

写题的时候要想清楚需要维护哪些信息,数据如何在节点之间传递等等。

Second Largest Query
单点修改,每次查询区间内次大值的出现次数。
不难看出,区间的最大值在合并前的区间的最大值中产生,次大值在次大的最大值和次大的最大值中产生(当两个最大值相等时,在两个次大值中产生)。每个节点储存区间最大值、次大值及其出现次数。

Can you answer on these queries III
单点修改,每次查询区间内的最大连续子段和。
分类讨论,最大连续子段来自于区间的左半部分或者右半部分或者左边靠右的部分和右边靠左的部分拼起来。每个节点储存区间从最左开始的最大子段和,最右开始的最大子段和以及整个区间内的最大子段和。

Vacation Query
和上面那题很像,但是区间修改需要把区间内所有的0/1反过来。每个节点内储存紧靠左边/右边的最大连续0/1的个数,区间内最大连续个数,向上传递时如果左子树全是0/1的话,lmaxp=lenlson+lmaxrson,否则lmaxp=lmaxlson。更新时交换0/1对应的连续值。

【模板】线段树 2
模板plus,区间修改为整体×c或者+c,区间查询。
需要两个延迟标记,一个记录倍数,一个记录加数,传递的时候要注意先乘后加。


势能线段树

有些情况下,线段树的区间修改会比较麻烦,比如区间开根、求商、取余等等。但是不难发现,这些修改的次数是有上限的,比方说0/1开根之后还是它本身,0不管除以多少还是0,那么到达这些区间时,修改的传递就可以停止了。又不难发现达到修改上限需要的次数也非常少,整体的复杂度是O(cnlogn),其中c为修改次数上限。

void upd(int l,int r,int p)
{
	if(lf(p)==rt(p))//暴力修改
	{
		mx(p)=sm(p)=sqrt(sm(p));
		return;
	}
	int mid=(lf(p)+rt(p))>>1;
	if(l<=mid&&mx(p<<1)>1)upd(l,r,p<<1);//判断边界
	if(r>mid&&mx(p<<1|1)>1)upd(l,r,p<<1|1);
	psu(p);
}

花神游历各国
就是这样。线段树每个节点维护区间和and最大值,如果最大值1就停止传递修改,其余递归到单点。


权值线段树

权值线段树的区间端点并非下标含义,而是值域。

逆序对
每个节点维护[l,r]中数的个数。每次更新ai对应节点,查询[ai+1,1e5]中的个数。而不要用线段树做归并排序,==。


动态开点

有时我们会遇到值域很小但是实际用到的点不多的权值线段树,很显然是没有必要把所有节点都build出来的。所以选择在访问到发现没有这个节点的时候再造一个。

struct treee{
	int lc,rc,nm;
	#define lc(p) tr[p].lc//左儿子下标
	#define rc(p) tr[p].rc//右儿子下标
	#define nm(p) tr[p].nm
}tr[N*30];
int tot=0;
void upd(int x,int p,int lf,int rt)//单点更新 插入x
{
	if(lf==rt)
	{
		nm(p)++;return;
	}
	if(x<=mid)
	{
		if(!lc(p))lc(p)=++tot;//动态开点
		upd(x,lc(p),lf,mid);
	}
	else
	{
		if(!rc(p))rc(p)=++tot;//动态开点
		upd(x,rc(p),mid+1,rt);
	}
	nm(p)=nm(lc(p))+nm(rc(p));
}

魔道研究 JZOJ 4270
动态开点一堆线段树,每棵树上二分找前 i 大,插入元素时把新增可以选择的元素加到 0 号树上,然后每次在这棵树上二分找前 n 大的元素和。


线段树二分

二分,但是在线段树上。

int query(int k,int p,int lf,int rt)//查询第k大的数
{
	if(lf==rt)return lf;
	if(k<=nm(lc(p)))return query(k,lc(p),lf,mid);
	else return query(k-nm(lc(p)),rc(p),mid+1,rt);
}

中位数
思路一对顶堆,思路二权值线段树动态开点+二分。

Siano
不难发现每次割掉的草都是长得最快的,所以排序。我的做法是两个懒标记记录上一次推平的高度和推平之后生长的天数。重点:每次推平都要把生长天数清空,向下传递推平标记的时候也要清空。


线段树合并

如果有若干棵线段树,它们都维护相同的值域 [1,n] ,那么它们对各个子区间的划分显然是一致的。假设有 m 次操作,每次修改在某一棵线段树上执行。所有操作完成后我们希望把这些线段树上对应位置的值相加,这就可以通过线段树合并实现。

Promotion Counting
需要统计每棵子树中比根节点大的节点数,dfs + 权值线段树 + 合并节点。

void merge(int &a,int b,int lf,int rt)//合并
{
	if(!a)
	{
		a=b;return;
	}
	if(!b)return;
	if(lf==rt)
	{
		num[a]+=num[b];return;
	}
	merge(lc[a],lc[b],lf,mid);
	merge(rc[a],rc[b],mid+1,rt);
	num[a]=num[lc[a]]+num[rc[a]];
}

可持久化线段树(主席树)

一般的线段树维护的只是数据集的最新状态,若想知道数据集在任意时间的状态,可以在每次操作结束后创建有改动部分的副本。可持久化线段树的空间复杂度为 O(N+MlogN)M 为修改次数)。

区间第 k
如果每次查询区间 [1,N] 的第 k 小数,我们可以在权值线段树上进行二分。那么如果查询任意区间 [l,r] 的第 k 小,看做从加入第 l 个元素到 r 个元素中间的第 k 小数。

int add(int p,int x,int lf,int rt)//加入新元素
{
	int t=++tot;
	lc[t]=lc[p];rc[t]=rc[p];num[t]=num[p]+1;//拿来不需要改动的部分
	if(lf==rt)return t;
	if(x<=mid)lc[t]=add(lc[t],x,lf,mid);
	else rc[t]=add(rc[t],x,mid+1,rt);
	return t;
}
int query(int p1,int p2,int k,int lf,int rt)//p1为时间是l-1的节点,p2为时间是r的节点
{
	if(lf==rt)return lf;
	if(num[lc[p2]]-num[lc[p1]]>=k)return query(lc[p1],lc[p2],k,lf,mid);
	else return query(rc[p1],rc[p2],k-num[lc[p2]]+num[lc[p1]],mid+1,rt);
}

扫描线

把平面上的若干个矩形看做若干 ×2 条线段,用四元组 (x,y1,y2,k) 记录,处理这些重叠的矩形的问题时可以看做用一条线从左到右扫过去,问题的答案在矩形的左右边界发生变化,这些变化就可以用线段树维护。

Atlantis

给出若干个矩形,求它们的面积并。顶点坐标不一定是整数。

int main()
{
	scanf("%d",&n);
	while(n)
	{
		for(int i=1;i<=n;i++)
		{
			scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
			g[i]={a,b,d,1};g[i+n]={c,b,d,-1};//将矩形转化为线段
			y.push_back(b);y.push_back(d);
		}
		sort(y.begin(),y.end());y.erase(unique(y.begin(),y.end()),y.end());
		siz=y.size();
		build(1,1,siz);
		sort(g+1,g+n*2+1);//按照x递增的顺序排序
		int b1,c1,d1;
		for(int i=1;i<=n*2;i++)
		{
			a=g[i].x;
			b1=lower_bound(y.begin(),y.end(),g[i].y)-y.begin()+1;//离散化
			c1=lower_bound(y.begin(),y.end(),g[i].y2)-y.begin()+1;
			d1=g[i].f;
			ans+=(g[i].x-g[i-1].x)*sum[1];//修改答案
			upd(b1,c1-1,d1,1,1,siz);
		}
		printf("Test case #%d\n",++t);
		printf("Total explored area: %.2f\n\n",ans);
		reset();
		scanf("%d",&n);
	}
	return 0;
}

易错

  1. 线段树空间必须×4

  2. 如果push up的时候使用了结构体,像这样:

treee psu(treee l,treee r)
{
  treee res;
  //magic
  return res;
}

建议所有变量都要初始化。

  1. #define max(a,b) ((a)>(b))?a:b

  2. 为了防止莫名其妙的TLE,建议减少使用minmax的次数。

  3. 注意空间

  4. 要注意排序运算符的定义,否则可能会RE。


只能说发明线段树的人真是个天才!

posted @   baiguifan  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示