线段树重温

欧耶是线段树,我们没救了。

upd.2025.7.6 小修部分冗余代码/调整部分格式。

 

定义


线段树是一种二叉搜索树。

线段树的每个结点都存储了一个区间,也可以理解成一个线段。

 

线段树就是分块思想的树化,或者说是对于信息处理的二进制化——用于达到 O(log⁡n) 级别的处理速度,log⁡ 以 2 为底。

而分块的思想,则是可以用一句话总结为:通过将整个序列分为有穷个小块,对于要查询的一段区间,总是可以整合成 k 个所分块与 m 个单个元素的信息的并 (0≤k,m≤sqrt(n) )

但普通的分块不能高效率地解决很多问题,所以作为 log⁡ 级别的数据结构,线段树应运而生。 

 

用途


线段树的适用范围很广,可以在线维护修改以及查询区间上的最值,求和。实际上线段树可以处理很多符合结合律,交换律的操作。对于线段树来说,每次更新以及查询的时间复杂度为O(logN)

1. 静态的区间询问:ST表

2. 动态的区间询问:线段树/树状数组

 

 

基础部分详解


建树


对于每个父亲管理的区间为[L, R],它左儿子管理的区间为[L, mid],右儿子管理的区间为[mid+1, R]

根据这点去递归建树即可。去遍历左子树和右子树即可。

void build(int id, int L, int R)
{
//在这里初始化懒标记,此处先不讲。 if (L==R) { tr[id].sum=a[L]; return ; } int mid=L+R>>1; build(id*2, L, mid); build(id*2+1, mid+1, R); push_up(id); }

  

但是这里有几个细节经常会错:

1.在L==R时,修改的是tr[id]的值而并非tr[L]的值!!注意线段树数组的下标都是以id标号的!!而L, R是对于初始化数组标号的!!!

2.只要是修改了线段树的值,都要push_up,把沿路的父亲值改掉,不要漏了!!!!!

3.线段树空间开4倍!!!

线段树开4倍空间原因:

有个公式:度为2的结点个数=度为0的结点个数-1,而线段树有个有意思的性质,没有度为1的点。所以对于线段树满足:非叶子结点个数=叶子结点个数-1

而叶子结点的个数就是数组长度,也就是n个叶子结点,n-1个非叶子结点。

把两个叶子调到最后一层,剩下n-2个叶子结点在倒数第二层。

(n-2+n-2)就是倒数第二层最右边的结点的编号,它分别有左右两个叶子。也就是最下面一层最右边的叶子可以表示为2*(2n-4)+1,编号为4n-5

所以开4n够了。

 

线段树构建出来具体如下图(以维护最大值为例):

 

单点修改+区间查询


单点修改:

跟建树差不多,不断递归定位一下这个点在哪里就好。

找到了再改。记得 push_up

 

区间查询:

具体看下文的区间查询,都类似就略过了,不过没有push_down操作。

 

板子(维护的区间加):

P3374 【模板】树状数组 1 - 洛谷 | 计算机科学教育新生态

void add(int id, int L, int R, int qL, long long k)
{
	if (L==R)
	{
		tr[id].sum+=k;
		return ;
	}
	
	int mid=L+R>>1;
	
	if (qL<=mid) add(id*2, L, mid, qL, k);
	if (qL>=mid+1) add(id*2+1, mid+1, R, qL, k);
	
	push_up(id);
}
long long query(int id, int L, int R, int qL, int qR)
{
	if (qL<=L && R<=qR) return tr[id].sum;
	
	int mid=L+R>>1;
	long long res=0;
	
	if (qL<=mid) res+=query(id*2, L, mid, qL, qR);
	if (qR>=mid+1) res+=query(id*2+1, mid+1, R, qL, qR);
	
	return res;
}

  

 

 

区间修改+区间查询


这里要用懒标记了。所以在建树的时候记得初始化懒标记

 

区间修改:

我们规定,要修改区间为[qL, qR],线段树每个结点对应的区间为[L, R]

 

 

我们不断缩小大区间,并递归找到若干个小区间[L, R]能完全被[qL, qR]包含,并修改[L, R]的值,打上懒标记(可能你会疑惑为什么在这里打懒标记,下面有解释)就好了。

这里举个例

比如线段树维护区间[1, 6],我们要修改[2, 5]这个大区间,可以分别修改[2, 2],[3, 3],[4, 5]这几个小区间。

考虑如何算小区间[L, R]能完全被[qL, qR]包含:

 画这个图理解,也就是qL<=L,且R<=qR,判断用这两个条件够了。因为只要满足这两个条件,其余条件一定满足(L<=qR,qL<=R等等)。

 

 

 

考虑如何去递归的找小区间:

假设当前递归到了管理区间[L, R]的结点标号id,此时需要修改[qL, qR]

分情况讨论往左子树[L, mid]或右子树[mid+1, R]再遍历。

1.往左子树[L, mid]遍历,证明[qL, qR]包含了[L, mid]的一部分,也可以说两区间有重叠部分

此时只要满足 qL<=mid 即可。这样一定包含而且不会漏其余情况。

可以这样感性理解:想要考虑两区间有一部分重叠,只需要考虑最端点的情况。分别考虑两区间的最左边和最右边端点位置情况即可。

qL,qR中最小的qL,在 L,mid中最大的mid,的左边,那不就一定重叠一部分吗。

 

2.往右子树遍历同理。满足qR>=mid+1即可。

 

 

 

管理区间[L, R]的结点标号id,要修改id的值时,无非两种情况

1.如果id是非叶子结点,修改完此时就不用继续往下找儿子了,先存懒标记,到了下次必须要利用它的儿子时候,再下放懒标记。

2.如果id是叶节点就没什么影响。因为这点没影响,所以在代码里面直接省略,没有体现。(就算被打上了懒标记并下放了也没影响)

 

考虑如何下放懒标记:

给当前结点的懒标记继承给它的孩子,并改变孩子的值,情况当前结点的懒标记。

 

在遍历的时候记得push_down,push_up(只要改了值就要push_up,只要遍历了就要push_down)

 

 

区间查询:

我们不断缩小大区间,并找到若干个小区间[L, R]能完全被[qL, qR]包含,并查询[L, R]的值,就好了。

考虑如何算小区间[L, R]能完全被[qL, qR]包含,还要递归的去找小区间,和区间修改一样。

记得push_down

 

 

板子(维护区间和):

P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态

void push_down(int id, int L, int R)
{
//有的题目注意,没有懒标记不能下放,此题是个例外,有没有懒标记下放都ok int mid=L+R>>1; tr[id*2].lazy+=tr[id].lazy; tr[id*2+1].lazy+=tr[id].lazy; tr[id*2].sum+=1ll*(mid-L+1)*tr[id].lazy; tr[id*2+1].sum+=1ll*(R-(mid+1)+1)*tr[id].lazy; tr[id].lazy=0; } void add(int id, int L, int R, int qL, int qR, long long k) { if (L>=qL && R<=qR) { tr[id].sum+=1ll*(R-L+1)*k; tr[id].lazy+=k; return ; } push_down(id, L, R); int mid=L+R>>1; if (qL<=mid) add(id*2, L, mid, qL, qR, k); if (qR>=mid+1) add(id*2+1, mid+1, R, qL, qR, k); push_up(id); } long long query(int id, int L, int R, int qL, int qR) { if (L>=qL && R<=qR) return tr[id].sum; push_down(id, L, R); int mid=L+R>>1; long long res=0; if (qL<=mid) res+=query(id*2, L, mid, qL, qR); if (qR>=mid+1) res+=query(id*2+1, mid+1, R, qL, qR); return res; }

  

 

 

 

两个懒标记


当有多个lazy_tag时,可以考虑合并在一起,把维护多种信息看成一个整体试试。

核心思想就是把单独维护多种信息看成维护一种信息的思想

 

具体的,可以考虑不同信息的所有结合的顺序,然后捏出来一种大一统信息,保证这个信息在结合下是封闭的

一种常见的情况是你可以给信息钦定一个作用顺序,就比如先乘后加,再比如先推平后加之类的,然后定义反序信息作用的交换子,不过合成大信息的思考方式比较一般化

(说句闲话,这都是是粉兔大佬说的)

 

 

两个懒标记间的互相影响-结合律


P3373

考虑又要加又要乘,一个懒标记不够只能用两个。

但是乘法和加法之间运算有先后顺序(在传递懒标记的时候会有顺序),我们可以考虑消除先后顺序的影响(用结合律消除,可能吧),但是得看哪种方法更好。

 

记住懒标记是直接对数字操作的。

比如+ a  *b  +c,初值是v。

 

1.先加后乘

正常来说是这样:(v+a)*b+c

懒标记是这样:(v+a+c)*b

考虑消除影响变成这样:(v+a+c/b)*b

但是c/b可能会出现小数,不考虑这种方法。

 

2.先乘后加

正常来说是这样:(v+a)*b+c

懒标记是这样:v*b+a+c

考虑消除影响变成这样:v*b+a*b+c

这种方法一点问题没有,很好维护。

也就是说在出现一个乘法时,得把之前的加法标记先乘上这个乘法标记(因为后面传递懒标记是先乘后加),取消这个影响。

所以在区间乘操作的时候要改加法标记,取消影响。

 

上面只是特例分析,会特例之后考虑一般如何分析。

分析括号外面是加法还是乘法就行了,且括号里面加法乘法都要包含,这样可以探究一下对两种符号的影响。

就像这样:

 

反正这玩意多分析分析就出来了,并不是多么难。

 

区间乘法,具体如下:

void upd_mul(int id, int L, int R, int qL, int qR, long long k)
{
	if (qL<=L && R<=qR)
	{
		tr[id].sum=(tr[id].sum*k)%Mod;
		tr[id].lazy_add=(tr[id].lazy_add*k)%Mod;
		tr[id].lazy_mul=(tr[id].lazy_mul*k)%Mod;
		
		return ;
	}
	
	.....
}

  

传递懒标记:

还是一样,先乘后加,把父亲的乘法标记先传递过来了,再传递加法标记。

void push_down(int id, int L, int R)
{
	int mid=L+R>>1;

	tr[id*2].sum=(tr[id*2].sum*tr[id].lazy_mul+(mid-L+1)*tr[id].lazy_add)%Mod; 
	tr[id*2+1].sum=(tr[id*2+1].sum*tr[id].lazy_mul+(R-(mid+1)+1)*tr[id].lazy_add)%Mod; 
	
	tr[id*2].lazy_add=(tr[id*2].lazy_add*tr[id].lazy_mul+tr[id].lazy_add)%Mod;
	tr[id*2+1].lazy_add=(tr[id*2+1].lazy_add*tr[id].lazy_mul+tr[id].lazy_add)%Mod;
		
	tr[id*2].lazy_mul=(tr[id*2].lazy_mul*tr[id].lazy_mul)%Mod;
	tr[id*2+1].lazy_mul=(tr[id*2+1].lazy_mul*tr[id].lazy_mul)%Mod;
		
	tr[id].lazy_add=0, tr[id].lazy_mul=1;
}

  

区间加法,就没影响了。

 

AC代码:

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;

struct node
{
	long long sum, lazy_add, lazy_mul;
}tr[N<<2];
int op, n, q, x, y;
long long a[N], Mod, k;
void push_up(int id)
{
	tr[id].sum=(tr[id*2].sum+tr[id*2+1].sum)%Mod;
}
void push_down(int id, int L, int R)
{
	int mid=L+R>>1;

	tr[id*2].sum=(tr[id*2].sum*tr[id].lazy_mul+(mid-L+1)*tr[id].lazy_add)%Mod; 
	tr[id*2+1].sum=(tr[id*2+1].sum*tr[id].lazy_mul+(R-(mid+1)+1)*tr[id].lazy_add)%Mod; 
	
//	printf("%d %lld %lld %lld\n", id, tr[id].lazy_mul, tr[id].lazy_add, tr[id*2+1].sum);
	
	tr[id*2].lazy_add=(tr[id*2].lazy_add*tr[id].lazy_mul+tr[id].lazy_add)%Mod;
	tr[id*2+1].lazy_add=(tr[id*2+1].lazy_add*tr[id].lazy_mul+tr[id].lazy_add)%Mod;
		
	tr[id*2].lazy_mul=(tr[id*2].lazy_mul*tr[id].lazy_mul)%Mod;
	tr[id*2+1].lazy_mul=(tr[id*2+1].lazy_mul*tr[id].lazy_mul)%Mod;
		
	tr[id].lazy_add=0, tr[id].lazy_mul=1;
}
void build(int id, int L, int R)
{
	tr[id].lazy_add=0;
	tr[id].lazy_mul=1;
	
	if (L==R)
	{
		tr[id].sum=a[L];
		return ;
	}
	int mid=L+R>>1;
	
	build(id*2, L, mid);
	build(id*2+1, mid+1, R);
	
	push_up(id);
}
void upd_mul(int id, int L, int R, int qL, int qR, long long k)
{
	if (qL<=L && R<=qR)
	{
		tr[id].sum=(tr[id].sum*k)%Mod;
		tr[id].lazy_add=(tr[id].lazy_add*k)%Mod;
		tr[id].lazy_mul=(tr[id].lazy_mul*k)%Mod;
		
		return ;
	}
	push_down(id, L, R);
	
	int mid=L+R>>1;
	if (qL<=mid) upd_mul(id*2, L, mid, qL, qR, k);
	if (qR>=mid+1) upd_mul(id*2+1, mid+1, R, qL, qR, k);
	
	push_up(id);
}
void upd_add(int id, int L, int R, int qL, int qR, long long k)
{
	if (qL<=L && R<=qR)
	{
		tr[id].sum=(tr[id].sum+k*(R-L+1))%Mod;
		tr[id].lazy_add=(tr[id].lazy_add+k)%Mod;
		return ; 
	}
	push_down(id, L, R);
	
	int mid=L+R>>1;
	if (qL<=mid) upd_add(id*2, L, mid, qL, qR, k);
	if (qR>=mid+1) upd_add(id*2+1, mid+1, R, qL, qR, k);
	
	push_up(id);
}
long long query(int id, int L, int R, int qL, int qR)
{
	if (qL<=L && R<=qR) return tr[id].sum; 
	push_down(id, L, R);
	
	int mid=L+R>>1;
	long long res=0;
	
	if (qL<=mid) res=(res+query(id*2, L, mid, qL, qR))%Mod;
	if (qR>=mid+1) res=(res+query(id*2+1, mid+1, R, qL, qR))%Mod;
	
	return res;
}
int main()
{
	scanf("%d%d%lld", &n, &q, &Mod);
	for (int i=1; i<=n; i++) scanf("%lld", &a[i]);
	build(1, 1, n);
	
	while (q--)
	{
		scanf("%d", &op);
		if (op==1)
		{
			scanf("%d%d%lld", &x, &y, &k);
			upd_mul(1, 1, n, x, y, k);
		}
		else if (op==2)
		{
			scanf("%d%d%lld", &x, &y, &k);
			upd_add(1, 1, n, x, y, k);
		}
		else
		{
			scanf("%d%d", &x, &y);
			printf("%lld\n", query(1, 1, n, x, y));
		}
	}
    return 0;
}
/*
5 2 38
1 5 4 2 3
1 2 3 2
3 1 4


19
18
*/

 

懒标记间先后顺序的影响-交换律


P1253

区间覆盖和区间加,要考虑一个先后顺序。

两种思考方向,一种是最常见的“结合律”,两个操作也结合在一起一起下放,推平看成*0然后加一个数,就好维护了。就是维护区间乘,区间加。

 

 

还有一种,可以考虑只下放一个操作。即“交换律”

区间覆盖了,那么先前的区间加标记得清空,这样当前点只有区间覆盖标记。

区间加了,那么先前的区间覆盖操作得先下放,这样当前点只有区间加标记。

相当于交换了下执行顺序,确保每次下放标记只需要考虑下放一个标记。上面这两点是对于 下放标记,和修改,两个地方都要改的。

 

对于区间覆盖:

void push_down_cg(int id)
{
	if (tr[id].lazy_cg!=INF)
	{
		tr[id*2].lazy_cg=tr[id].lazy_cg;
		tr[id*2+1].lazy_cg=tr[id].lazy_cg;
		
		tr[id*2].Max=tr[id].lazy_cg;
		tr[id*2+1].Max=tr[id].lazy_cg;
		
		tr[id*2].lazy_ad=0; //清空加法标记 
		tr[id*2+1].lazy_ad=0; //清空加法标记 
		
		tr[id].lazy_cg=INF;
	}
}
....
void change(int id, int L, int R, int qL, int qR, int x)
{
	if (qL<=L && R<=qR)
	{
		tr[id].Max=x;
		tr[id].lazy_cg=x;
		tr[id].lazy_ad=0; //清空加法标记 
		return ;
	}
	push_down(id);
	int mid=L+R>>1;
	
	if (qL<=mid) change(id*2, L, mid, qL, qR, x);
	if (qR>=mid+1) change(id*2+1, mid+1, R, qL, qR, x);
	
	push_up(id);
}

  

对于区间加:

void push_down_ad(int id)
{
	if (tr[id].lazy_ad!=0)
	{
		push_down_cg(id); //提前下放覆盖标记 
		
		tr[id*2].lazy_ad+=tr[id].lazy_ad;
		tr[id*2+1].lazy_ad+=tr[id].lazy_ad;
		
		tr[id*2].Max+=tr[id].lazy_ad;
		tr[id*2+1].Max+=tr[id].lazy_ad;
		
		tr[id].lazy_ad=0;
	}
}
...
void Add(int id, int L, int R, int qL, int qR, int x)
{
	if (qL<=L && R<=qR)
	{
		push_down_cg(id); //提前下放覆盖标记 
		
		tr[id].Max+=x;
		tr[id].lazy_ad+=x;
		return ;
	}
	push_down(id);
	int mid=L+R>>1;
	
	if (qL<=mid) Add(id*2, L, mid, qL, qR, x);
	if (qR>=mid+1) Add(id*2+1, mid+1, R, qL, qR, x);
	
	push_up(id);
}

  

 

左右前缀区间维护段树


大概是维护一段连续区间的某些值要用到吧。

主要就分情况讨论,如下:

 

板子[无区查]


P6492 [COCI 2010/2011 #6] STEP - 洛谷 | 计算机科学教育新生态

记录 tr[i].ans,表示当前区间的最优答案,这个答案,可以考虑由两个孩子的 ans 得来,也可以考虑是两个孩子拼接后的中间那一部分。

所以再维护 tr[i].ansL,表示当前区间的前缀最优答案,tr[i].ansR,就是后缀。

就可以通过 tr[i].ansL,tr[i].ansR,转移到 tr[i].ans 了。

再如何维护tr[i].ansL,tr[i].ansR?

直接继承儿子的就行。注意分类讨论一组特殊情况。

如果儿子的 tr[i].ansL,即前缀最优答案,是这个儿子的整个区间,那要再加上这个另外一个儿子的 ans[i].ansL,因为可以拼接而成。

tr[i].ansR 维护同理

 

注意,每题的 push_up 维护方法不一定固定。但大致思路一样。

比如下面那道板题,维护 tr[i].ansL/R时,必须不能加判断当前前缀/后缀的最优答案是否为整个区间。但是这题必须要判断。

 

具体一点,就看题解P6492 [COCI2010-2011#6] STEP - 洛谷专栏。很详细了。

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;

struct node
{
	int ans, ansL, ansR;
}tr[N<<2];
int n, q, a[N], x;
void push_up(int id, int L, int R)
{
	int mid=L+R>>1;
	
	tr[id].ans=max(tr[id*2].ans, tr[id*2+1].ans);
	if (a[mid]!=a[mid+1]) tr[id].ans=max(tr[id].ans, tr[id*2].ansR+tr[id*2+1].ansL);
	
	tr[id].ansL=tr[id*2].ansL;
	if (tr[id*2].ans==mid-L+1 && a[mid]!=a[mid+1]) tr[id].ansL=max(tr[id].ansL, mid-L+1+tr[id*2+1].ansL);
	
	tr[id].ansR=tr[id*2+1].ansR;
	if (tr[id*2+1].ans==R-(mid+1)+1 && a[mid]!=a[mid+1]) tr[id].ansR=max(tr[id].ansR, R-(mid+1)+1+tr[id*2].ansR);
}
void build(int id, int L, int R)
{
	tr[id].ans=tr[id].ansL=tr[id].ansR=1;
	if (L==R)
	{
		return ;
	}
	int mid=L+R>>1;
	
	build(id*2, L, mid);
	build(id*2+1, mid+1, R);
	
	push_up(id, L, R);
}
void change(int id, int L, int R, int x)
{
	if (L==R)
	{
		a[L]^=1;
		return ;
	}
	int mid=L+R>>1;
	
	if (x<=mid) change(id*2, L, mid, x);
	if (x>=mid+1) change(id*2+1, mid+1, R, x);
	
	push_up(id, L, R);
}
int query()
{
	return tr[1].ans;
}
int main()
{
	scanf("%d%d", &n, &q);
	for (int i=1; i<=n; i++) a[i]=1;
	build(1, 1, n);
	
	while (q--)
	{
		scanf("%d", &x);
		change(1, 1, n, x);
		
		printf("%d\n", query());
	}
	return 0;
}
/*
3 1
2

*/

  

板子[带区查]


P4513 小白逛公园 - 洛谷 | 计算机科学教育新生态

其实带区查差不多,就是细节多了点。

区间查询的时候跟push_up一样,分情况讨论,合并区间答案即可。可以直接套用push_up的代码。

在不断递归下去的时候,具体这样分情况(可以看代码理解。有点抽象)

1)查询区间只在左区间,直接查询左区间

2)查询区间只在右区间,直接查询右区间

3)查询区间左右区间都有,考虑如何合并

因为有合并区间的情况,所以 query 返回的应该是一个结构体,且维护方式和 push_up 一样。

 

对于这题,维护区间连续段和的最大值即可。然后再开区间前缀和最大值,后缀和最大值辅助更新。

维护 tr[i].ans 时,注意跨越两段的情况,这个时候两段可能有负数,所以可以只选1段,就直接考虑最大值。或者选2段,直接相加。

维护 tr[i].ansL/R 时,注意不能判断当前前缀/后缀的最优答案是否为整个区间,因为全选当前区间可能不优,但是另一半边的区间选上后可能更优。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5, INF=-1e18;
 
struct node
{
    int sum;
    int Max, MaxL, MaxR;
}tr[N<<2];
int n, m, a[N], op, x, y;
void push_up(int id, int L, int R)
{
    int mid=L+R>>1;
    tr[id].sum=tr[id*2].sum+tr[id*2+1].sum;
     
    tr[id].Max=max(tr[id*2].Max, tr[id*2+1].Max);
    tr[id].Max=max(tr[id].Max, tr[id*2+1].MaxL+tr[id*2].MaxR);
     
    tr[id].MaxL=tr[id*2].MaxL;
    tr[id].MaxL=max(tr[id].MaxL, tr[id*2].sum+tr[id*2+1].MaxL);
     
    tr[id].MaxR=tr[id*2+1].MaxR;
    tr[id].MaxR=max(tr[id].MaxR, tr[id*2+1].sum+tr[id*2].MaxR);
}
void build(int id, int L, int R)
{
    tr[id].sum=0;
    tr[id].Max=tr[id].MaxL=tr[id].MaxR=INF;
    if (L==R)
    {
        tr[id].sum=tr[id].Max=tr[id].MaxL=tr[id].MaxR=a[L];
        return ;
    }
    int mid=L+R>>1;
     
    build(id*2, L, mid);
    build(id*2+1, mid+1, R);
     
    push_up(id, L, R);
}
node query(int id, int L, int R, int qL, int qR)
{
    if (qL<=L && R<=qR) return tr[id];
    int mid=L+R>>1;
     
    if (qR<=mid) return query(id*2, L, mid, qL, qR); //left
    else if (qL>=mid+1) return query(id*2+1, mid+1, R, qL, qR); //right
    else //all
    {
        node res, resL, resR;
        resL=query(id*2, L, mid, qL, qR), resR=query(id*2+1, mid+1, R, qL, qR);
         
        res.sum=resL.sum+resR.sum;
         
        res.Max=max(resL.Max, resR.Max);
        res.Max=max(res.Max, max(max(resL.MaxR, resR.MaxL), resL.MaxR+resR.MaxL));
         
        res.MaxL=resL.MaxL;
        res.MaxL=max(res.MaxL, resL.sum+resR.MaxL);
         
        res.MaxR=resR.MaxR;
        res.MaxR=max(res.MaxR, resR.sum+resL.MaxR);
         
        return res;
    }
}
void change(int id, int L, int R, int p, int s)
{
    if (L==R)
    {
        tr[id].Max=tr[id].MaxL=tr[id].MaxR=s;
        tr[id].sum=s;
        return ;
    }
    int mid=L+R>>1;
     
    if (p<=mid) change(id*2, L, mid, p, s);
    if (p>=mid+1) change(id*2+1, mid+1, R, p, s);
     
    push_up(id, L, R);
}
signed main()
{
    scanf("%d%d", &n, &m);
    for (int i=1; i<=n; i++) scanf("%lld", &a[i]);
    build(1, 1, n);
     
    while (m--)
    {
        scanf("%d%d%lld", &op, &x, &y);
         
        if (op==1)
        {
            if (x>y) swap(x, y);
            printf("%lld\n", query(1, 1, n, x, y).Max);
        }
        else change(1, 1, n, x, y);
    }
    return 0;
}

  

  

 

 

 

势能线段树


板子[单点势能]


P4145

作为一个好用的黑科技,还是多讲下吧。

 

势能线段树的介绍:

本质上是一种区间暴力修改技术。

考虑普通的区修线段树,我们使用 lazytag 来记录区间修改信息,在访问到当前节点之后再将当前节点的 tag 下传。

然而,有很多信息是不好下传的(不满足交换或结合律),于是我们就无法使用 tag 来记录区修信息。

但有的题目每一个操作都有潜在的操作次数上限,我们可以先判断该区间有没有需要操作的节点,有的话遍历到叶子节点进行修改,没有的话返回。

 

相对应的,和势能线段树配合着用的一个trict:

如果要看某个区间的每个值是否>x,只需要找区间的最小值。最大值同理

比如这题就需要,常用于次数限制。

 

此题同理,每次都sqrt,从1e12变成1最多只需7次。变成1之后,再sqrt没有意义,怎么样都是1。

所以可以维护一个区间最大值,看看最大值是否>1,>1才进行递归并修改。

修改操作可以暴力一点,不用懒标记了,直接递归到叶子结点进行修改(因为不符合结合律交换律)

 

AC代码:记录详情 - 洛谷 | 计算机科学教育新生态

 

区间势能


2.9.数据结构PPT

咕咕咕。

 

例题


数据结构(势能线段树) - cn是大帅哥886 - 博客园

 

 

其他技巧


P1558-线段树+状压


关注到数据范围较小,且正常维护不好维护。考虑维护一个二进制数,表状态压缩,就好了。

 

P1438-线段树维护差分数组


区间加等差数列,因为是等差,所以中间一串的差相等。考虑维护中间一段,两边特殊处理就好。所以可以想到维护差分数组。

具体的:

原序列:0 0 0 0 0 0
差分序列:0 0 0 0 0 0
等差序列:1 3 5 7 9
加上等差数列后的序列:1 3 5 7 9 0
然后差分:1 2 2 2 2 -9

 

当前也可以考虑另一种思路,因为是单点查询,所以有这种思路:题解 P1438 【无聊的数列】 - 洛谷专栏

当我们对区间 $[l,r]$ 加上一个首项为 $k$ 公差为 $d$ 的等差数列时,我们给这个区间加上 $k - d \times l$,再给每个位置加上 下标$\times d$

P3870-区间01串取反trict


维护区间1的个数,每次会对区间取反。因为只有0/1,所以取反后,1的个数=区间长度-原来1的个数。

 

根据值域建树,解决一类偏序问题


可以解决 I<j, a[i]<a[j] 的数对个数,直接按照值域建线段树就好。

1.P1908 逆序对 - 洛谷,离散化后,容斥一下,就是板子了。

记录详情 - 洛谷 | 计算机科学教育新生态

 

2.P1637 三元上升子序列 - 洛谷

此题同理,三元组只需以中间一元为中心考虑问题即可。即枚举中间一元,快速维护左右两元非法数量。

记录详情 - 洛谷 | 计算机科学教育新生态

 

posted @ 2025-01-24 19:44  cn是大帅哥886  阅读(9)  评论(0)    收藏  举报