线段树扩展应用(基础)

前言

码量越来越大,bug 越来越难找。

区间最值线段树

这种数据结构用于解决动态区间的最值问题,如果问题给出的是静态区间,建议使用常数更小的 ST 表。

其实很简单:在原来线段树的基础上再维护最大值和最小值即可,理论上可以附着在任何一种求值的线段树上。
下文以加法线段树来演示。
由于这样的线段树在建树和区间修改时要更新三个值,因此一般把这个操作封装在函数里。

inline void pushup(int p){
	segtree[p].v=segtree[p*2].v+segtree[p*2+1].v;//左右子区间的和
	segtree[p].maxx=max(segtree[p*2].maxx,segtree[p*2+1].maxx);//左右子区间的最大值
	segtree[p].minn=min(segtree[p*2].minn,segtree[p*2+1].minn);//左右子区间的最小值
}

同样,在懒标记下传时也要更新:

inline void pushdown(int p){
	if(segtree[p].lzt!=0){
		int k=segtree[p].lzt;
        //下传懒标记
		segtree[p*2].lzt+=k;
		segtree[p*2+1].lzt+=k;
        //更新子区间最大最小值
		segtree[p*2].maxx+=k;
		segtree[p*2+1].maxx+=k;
		segtree[p*2].minn+=k;
		segtree[p*2+1].minn+=k;
		//更新子区间的值
		int mid=segtree[p].r+segtree[p].l>>1;
		segtree[p*2].v+=(mid-segtree[p*2].l+1)*k;
		segtree[p*2+1].v+=(segtree[p*2+1].r-mid)*k;

		segtree[p].lzt=0;
	}
}

区间求最大最小值的函数和区间加法的大同小异,只是返回值略有不同。
区间最大值:

inline ll maxquery(int p,int l,int r){
	if(l<=segtree[p].l&&segtree[p].r<=r){//区间被答案区间完全覆盖,直接返回区间最大值
		return segtree[p].maxx;
	}
	if(l>segtree[p].r||segtree[p].l>r)return LMIN;//无穷小,使用 #define
	pushdown(p);//依然要下传懒标记
	ll sub=LMIN;//初始化为无穷小
	if(l<=segtree[p*2].r){
		sub=max(sub,maxquery(p*2,l,r));//左子区间的最大值
	}
	if(segtree[p*2+1].l<=r){
		sub=max(sub,maxquery(p*2+1,l,r));//右子区间的最大值
	}
	return sub;//返回
}

区间最小值:

inline ll minquery(int p,int l,int r){
	if(l<=segtree[p].l&&segtree[p].r<=r){
		return segtree[p].minn;
	}
	if(l>segtree[p].r||segtree[p].l>r)return LMAX;//无穷大,使用 #define
	pushdown(p);//下传懒标记
	ll sub=LMAX;//初始化为无穷大
	if(l<=segtree[p*2].r){
		sub=min(sub,minquery(p*2,l,r));
	}
	if(segtree[p*2+1].l<=r){
		sub=min(sub,minquery(p*2+1,l,r));
	}
	return sub;//获得两子区间最小值后返回
}

完整代码,经过 \(10000\)\(1\le n,q \le 10^6\) 量级的数据对拍后无错误。

例题:

区间和积线段树

给出一些区间,把这个区间内的值加或乘上某个数。
查询某一区间值的和。
初看到这种线段树不以为意:不就是加入了一个乘法操作吗?
但是细细想来就会发现,对于懒标记的操作有些复杂,还要考虑加法和乘法的优先级关系。
那么如何解决这个问题呢?我们考虑建两个懒标记,一个记录乘法操作,一个记录加法操作。
那么怎样记录加法操作和乘法操作呢?我们的思路是:进行了某一些操作后:

\[\text{原始值}\times \text{乘法懒标记的值}+\text{加法懒标记的值}=\text{现在的值} \]

第一个问题来了:在区间修改操作中如何更新这两个懒标记呢?
我们不妨手摸一组数据,例如如下操作列表(这里的操作按区间修改的顺序从左到右给出,不是四则运算的顺序!):

\[\times 5 \ \ +4 \ \ \times 3 \ \ +6\ \ \times 2 \]

如果初始数据是 \(2\),则容易得出修改后的值为:

\[2\times 5 \ \ +4 \ \ \times 3 \ \ +6\ \ \times 2=96 \]

把它写成我们想要的用懒标记表示的方式:

\[2\times 30+36=96 \]

也就是说乘法懒标记的值应是 \(30\),加法懒标记的值应是 \(36\)
这两个数怎么来的?

\[1\times 5\times 3\times 2=30 \]

\[\left((0\times 5+4)\times 3+6\right)\times 2=36 \]

也就是说,对于区间乘法操作,不仅要更新乘法懒标记的值,还要更新加法懒标记的值。
而加法操作只需更新加法懒标记的值就可以了。
而且我们还得出,加法懒标记的初始值应是 \(0\),乘法懒标记的初始值应是 \(1\)
那么,我们将加法和乘法操作分成两个函数分别实现。根据我们上面的讨论,可以写出建树,区间加,区间乘操作的函数。
建树:

inline void buildt(int l,int r,int p){
	segtree[p].l=l;
	segtree[p].r=r;
	segtree[p].lat=0;//加法操作懒标记的初始值为 0
	segtree[p].lmt=1;//乘法操作的初始值为 1
    //其他部分和标准加法线段树相同。
	if(l==r){
		segtree[p].v=num[l];
		return;
	}
	int mid=(l+r)>>1;
	buildt(l,mid,p*2);
	buildt(mid+1,r,p*2+1);
	segtree[p].v=segtree[p*2].v+segtree[p*2+1].v;
}

区间加:

inline void add(ll k,int l,int r,int p){
	if(l<=segtree[p].l&&segtree[p].r<=r){
		segtree[p].v+=k*(segtree[p].r-segtree[p].l+1);//更新值 (要乘上区间个数!!)
		segtree[p].lat+=k;//更新加法懒标记
		return;
	}
    //其余部分和标准加法线段树相同。
	push_down(p);
	if(l<=segtree[p*2].r)add(k,l,r,p*2);
	if(segtree[p*2+1].l<=r)add(k,l,r,p*2+1);
	segtree[p].v=(segtree[p*2].v+segtree[p*2+1].v);
	return;
}

区间乘:

inline void mult(ll k,int l,int r,int p){
	if(l<=segtree[p].l&&segtree[p].r<=r){
		segtree[p].v*=k;//根据乘法分配律,直接乘即可; 
		segtree[p].lmt*=k;//更新乘法懒标记 
		segtree[p].lat*=k;//更新加法懒标记 
		return;
	}
	//其余部分和标准加法线段树相同。
	push_down(p);
	if(l<=segtree[p*2].r)mult(k,l,r,p*2);
	if(segtree[p*2+1].l<=r)mult(k,l,r,p*2+1);
	segtree[p].v=(segtree[p*2].v%M+segtree[p*2+1].v%M)%M;
	return;
}

第二个问题来了,怎么下传懒标记?
为了描述方便,我们下面把子区间的加法懒标记和乘法懒标记分别称为 “子加法懒标记” 和 “子乘法懒标记”。
我们下传懒标记的目的,就是把子区间的现在值也用 \(\text{原始值}\times \text{乘法懒标记的值}+\text{加法懒标记的值}=\text{现在的值}\) 的方式表示出来。
那么在下传懒标记之前,子区间的值应该是:

\[\text{原始值}\times \text{子乘法懒标记}+\text{子加法懒标记}=\text{之前的值} \]

下传懒标记以后的子区间的值应是:

\[\text{之前的值}\times \text{乘法懒标记}+\text{加法懒标记}=\text{现在的值}\ \ \ \ \ (1) \]

代入 \(\text{之前的值}\) 的表示:

\[(\text{原始值}\times \text{子乘法懒标记}+\text{子加法懒标记})\times \text{乘法懒标记}+\text{加法懒标记}=\text{现在的值} \]

展开它:

\[\text{原始值}\times (\text{子乘法懒标记}\times\text{乘法懒标记}) + (\text{子加法懒标记}\times \text{乘法懒标记}+\text{加法懒标记})=\text{现在的值} \]

我们发现两个括号里的内容正好符合我们对乘法懒标记和加法懒标记的定义。
于是我们可以得出:

\[\text{新的子乘法懒标记}=\text{子乘法懒标记}\times\text{乘法懒标记}\ \ \ \ \ (2) \]

\[\text{新的子加法懒标记}=\text{子加法懒标记}\times \text{乘法懒标记}+\text{加法懒标记}\ \ \ \ \ (3) \]

于是我们可以写出下传懒标记的函数:

inline void push_down(int p){
	if(segtree[p].lat!=0||segtree[p].lmt!=1){//任何一个懒标记如果不是初始值都要进行下传
		ll k1=segtree[p].lat,k2=segtree[p].lmt;
        //k1 为下传的区间的加法懒标记,k2 为下传的区间的乘法懒标记

        //更新左区间的加法乘法懒标记,公式(2)(3)
		segtree[p*2].lat=(segtree[p*2].lat*k2+k1);
		segtree[p*2].lmt=(segtree[p*2].lmt*k2);
        //更新右区间的加法乘法懒标记
		segtree[p*2+1].lat=(segtree[p*2+1].lat*k2+k1);
		segtree[p*2+1].lmt=(segtree[p*2+1].lmt*k2);

        //更新左右区间的值,公式(1) (加法操作要乘上区间个数!!,乘法操作由于乘法分配律可以直接乘)
		int mid=(segtree[p].r+segtree[p].l)>>1;
		segtree[p*2].v=(segtree[p*2].v*k2+k1*(mid-segtree[p*2].l+1));
		segtree[p*2+1].v=(segtree[p*2+1].v*k2+k1*(segtree[p*2+1].r-mid));

        //重置区间的懒标记为初始值
		segtree[p].lat=0;segtree[p].lmt=1;
	}
}

查询区间和的操作和标准加法线段树完全相同,这里不再列出。
完整代码在这,可以通过 洛谷 P3373 【模板】线段树 2

区间覆盖线段树

给出一些区间,把这个区间里的所有值设为某值。
查询某区间值的情况,或是某个点的值。
最简单的一种线段树了,甚至比加法线段树还要简单。
直接贴代码:

struct seg{
	int l,r;//左右区间
	int v;//节点值
	int lzt;//懒标记
}segtree[10000*4];  
void buildt(int p,int l,int r){
	segtree[p].l=l;
	segtree[p].r=r;
	segtree[p].lzt=0;
	if(l==r)segtree[p].v=0;return;
	int mid=l+r>>1;
	buildt(p*2,l,mid);
	buildt(p*2+1,mid+1,r);
	return;//由于是区间覆盖,并不需要计算合并的值,直接返回即可
}
void pushdown(int p){//懒标记的下传
	if(segtree[p].lzt!=0){
		int k=segtree[p].lzt;
		segtree[p*2].lzt=k;//更新子区间懒标记
		segtree[p*2+1].lzt=k;
		segtree[p*2].v=k;//更新子区间值
		segtree[p*2+1].v=k;
		segtree[p].lzt=0;//清除自己的懒标记
	}
}
void cover(int p,int l,int r,int id){//区间覆盖函数
	if(l<=segtree[p].l&&segtree[p].r<=r){
		segtree[p].v=id;
		segtree[p].lzt=id;
		return;
	}
	pushdown(p);//记得下传
	if(segtree[p*2].r>=l)cover(p*2,l,r,id);
	if(segtree[p*2+1].l<=r)cover(p*2+1,l,r,id);
}
int ask(int p,int k){//以询问点的情况为例 
	if(segtree[p].l==k&&segtree[p].r==k){
		return segtree[p].v;//找到了点代表的线段,直接返回点的值
	}
	pushdown(p);//下传懒标记
	if(segtree[p*2].r>=k)return ask(p*2,k);//点在左子区间就往左子区间找
	if(segtree[p*2+1].l<=k)return ask(p*2+1,k);//在右子区间就往右子区间找
	return 0;
}

例题:

  • 洛谷 P3470
    注意:在本题中由于点太大直接暴力建树会导致 MLE。
    考虑离散化这些点,但是会遇到这样的情况:

    1--4/6--9
    //3---7

    很明显能看到三个海报,答案应为 \(3\)
    但是按照海报的头尾位置大小离散化后变成了:

    1-34-6
    /2--5

    排名为 \(3,4\) 的两个节点挡住了本应露出的 \(2,5\) 两个节点的海报。
    如何解决这样的问题呢?我们考虑在存储每一个海报头尾节点的基础上,再多插入一个右节点加一的位置。
    由于这样的离散化,原图中节点变成了这样:

    1---4(+5)6----9(+10)
    ///3-------7(+8)

    如此这般,在线段树中表示这些海报就成了:

    1---3 5-----8
    ///2------6

    为什么这样存储就对了呢?考虑两个海报头尾节点的关系:

    • 如果他们在输入中本来就相邻,那么加入左边海报尾节点加 \(1\) 后的位置并不影响右边海报头节点在离散化数组中的大小排名(因为 “左边海报尾节点加 \(1\) 后的位置” 就是 “右边海报头节点的位置”)。
    • 如果他们在输入中不相邻,且他们中间还有其他海报的头尾节点,则加入左边海报尾节点加 \(1\) 的位置并不影响其他海报头尾节点在离散化数组中的大小关系。
    • 如果他们在输入中不相邻,且他们中间没有其他海报的头尾节点,那么加入左边海报尾节点加 \(1\) 的位置后总会让右边海报头节点在离散化数组中与左边海报尾节点隔了一个新加入的左边海报尾节点加 \(1\) 的位置,避免了头尾节点合起来挡住下面的海报的情况。

    综上可以说明这种存储方式的正确性。


迁移自洛谷

posted @ 2025-02-04 13:21  hm2ns  阅读(17)  评论(0)    收藏  举报