线段树综合

线段树综合

线段树 Pro Max。

前置知识:权值线段树动态开点线段树。这两个东西是相辅相成的,到现在应该很熟练。其中权值线段树可以维护数字的出现次数相关信息,动态开点线段树则是解决值域过大带来的问题,或者是要开多个线段树之用。

线段树分裂

前置知识:线段树合并。那么有合并就有分裂,线段树分裂一般用在权值线段树上,它可以分裂出权值线段树的一部分方便我们进行操作。

具体实现方面,常用的有两种。第一种用于分裂出前 \(k\) 小,就是类似 FHQ Treap 的方式,定义函数 \(\operatorname{split}(p,q,k)\) 表示将 \(p\) 除了前 \(\boldsymbol k\) 小以外的部分分裂到 \(q\) 上去。此时我们针对 \(p\) 的左儿子的权值 \(c\) 进行分讨:

  • \(c<k\) 时,左儿子会被完全保留,此时递归右儿子,同时 \(k\) 变为 \(k-c\)
  • \(c=k\) 时,左儿子恰好被完全保留,此时直接把右儿子拷给 \(q\)
  • \(c>k\) 时,需要递归左儿子,此时仍然把右儿子拷给 \(q\),然后递归左儿子。

最后更新 \(p\)\(q\) 的权值即可。

void split(int p,int&q,int k){
    if(!p) return;
    q=newnode();
    int res=st[lp].c;
    if(res<k) split(rp,rq,k-res);
    else swap(rp,rq);
    if(res>k) split(lp,lq,k);
    st[q].c=st[p].c-k;
    st[p].c=k;
}

第二种方法可以一次性分裂出一个区间 \([l,r]\)。采用一般的线段树遍历方式,当当前区间被完全包含在分裂区间时,就直接把当前区间拷过去,同时删除原区间。显然,这种做法相比于调用两次第一种 \(\operatorname{split}\) 函数有着 \(1/2\) 的常数优势。

void split(int&p,int&q,int l,int r,int s,int t){
	if(!p||l>t||s>r) return;
	if(l<=s&&t<=r) return q=p,p=0,void();
	if(!q) q=newnode();
	int mid=(s+t)>>1;
	if(l<=mid) split(lp,lq,l,r,s,mid);
	if(mid<r) split(rp,rq,l,r,mid+1,t);
	pushup(p);
	pushup(q);
}

例题

P5494 【模板】线段树分裂

板子题。

  • 对于操作 0,直接调用上面的第二个 \(\operatorname{split}\) 函数;
  • 对于操作 1,线段树合并;
  • 对于操作 23,就是在权值线段树上单点加、区间求和;
  • 对于操作 4,线段树二分即可。

P2824 [HEOI2016/TJOI2016] 排序

发现可以把排序看作一种区间推平,即把区间推成一段递增或递减的连续段,于是考虑珂朵莉树维护颜色段均摊,用权值线段树维护每个连续段中出现的数字。在珂朵莉树分裂的时候同时进行线段树分裂,在其合并的时候同时进行线段树合并即可。分讨一下颜色段种类。

CF558E A Simple Task

和上一道题是类似的,只不过因为这题中字符可能有重复,所以上一道题的代码不能直接通过。实际上有一个取巧的方法,因为长度还是 \(10^5\),所以可以把相同的英文字母离散化成不同的值,这样就能套用上道题的代码了,代价是常数比较大。

CF911G Mass Change Queries

考虑到值域只有 \(100\),所以可以开 \(100\) 棵权值线段树,每次就相当于把一棵树的一段区间分裂再合并到另一棵树上。注意特判 \(x=y\) 的情况。

线段树优化建图

用于图论问题,可以在 \(\log\) 的复杂度内解决一点 \(v\) 连接区间 \([l,r]\) 所有点,或区间 \([l,r]\) 所有点连接 \(v\)。它的原理是预先把线段树上父亲和儿子之间连边,然后在一连多或者多连一的时候只需连接 \(\log\) 条边就可以了。代价是点的个数变成 \(4N\) 级别,所以空间需要整体开 4 倍,如果需要同时解决一连多和多连一就需要开 8 倍

代码(一连多)

#define lp (p<<1)
#define rp (p<<1|1)
void build(int s,int t,int p){
    if(s==t) return id[s]=p,void(); // 记录每个点的位置
    int mid=(s+t)>>1;
    addedge(p,lp),addedge(p,rp);
    build(s,mid,lp),build(mid+1,t,rp);
}
void add(int l,int r,int v,int s=1,int t=n,int p=1){
    if(l<=s&&t<=r) return addedge(v,p),void();
    int mid=(s+t)>>1;
    if(l<=mid) add(l,r,v,s,mid,lp);
    if(mid<r) add(l,r,v,mid+1,t,rp);
}

代码(多连一)

这种情况需要把儿子向父亲连边。之所以加 \(S\) 是需要将点整体上抬。

#define lp (p<<1)
#define rp (p<<1|1)
void build(int s,int t,int p){
    if(s==t) return id[s]=p,addedge(s,p+S); 
    int mid=(s+t)>>1;
    addedge(lp+S,p+S),addedge(lp+S,p+S);
    build(s,mid,lp),build(mid+1,t,rp);
}
void add(int l,int r,int v,int s=1,int t=n,int p=1){
    if(l<=s&&t<=r) return addedge(p,v),void();
    int mid=(s+t)>>1;
    if(l<=mid) add(l,r,v,s,mid,lp);
    if(mid<r) add(l,r,v,mid+1,t,rp);
}

代码(二者都有)

实现有细微差别,需要注意。

#define lp (p<<1)
#define rp (p<<1|1)
void build(int s,int t,int p){
    if(s==t) return id[s]=p,void();
    int mid=(s+t)>>1;
    addedge(p,lp,0),addedge(p,rp,0);
    addedge(lp+S,p+S,0),addedge(rp+S,p+S,0);
    build(s,mid,lp),build(mid+1,t,rp);
}
void add(int l,int r,int op,int v,ll w,int s=1,int t=n,int p=1){
    if(l<=s&&t<=r){
        if(op&1) addedge(p+S,v,w);
        else addedge(v+S,p,w);
        return; 
    }
    int mid=(s+t)>>1;
    if(l<=mid) add(l,r,op,v,w,s,mid,lp);
    if(mid<r) add(l,r,op,v,w,mid+1,t,rp);
}

线段树分治

线段树分治用于解决一类图论问题:每条边有一个出现时间和一个消失时间,即它的存在周期是一段时间时,需要我们求出每个时间点图的形态或计算一些信息。

线段树分治是离线算法。对于上述的问题,线段树的每个节点维护当前节点所代表的区间有多少条边,对于每条边的存在时间 \([l,r]\) 进行区间修改。最后再对整棵树进行一次 DFS,每到一个节点就连上该节点的边,离开时再断开这些边即可。

线段树分治常和扩展域并查集可撤销并查集连用。其中扩展域并查集用于解决有关二分图的判定问题,可撤销并查集则是用来方便地维护连边和断边操作。

代码(P5787 二分图 /【模板】线段树分治

constexpr int MAXN=1e5+5;
int n,m,k;
struct{
	int f[MAXN<<1],siz[MAXN<<1],stk[MAXN<<1],top;
	void init(int n){
		for(int i=1;i<=n;i++) f[i]=i,siz[i]=1;
	}
	int fnd(int x){
		return f[x]==x?x:fnd(f[x]);
	}
	void mge(int x,int y){
		x=fnd(x),y=fnd(y);
		if(x==y) return;
		if(siz[x]<siz[y]) swap(x,y);
		f[y]=x,siz[x]+=siz[y];
		stk[++top]=y;
	}
	void del(int x){
		while(top>x){
			int t=stk[top--];
			siz[f[t]]-=siz[t];
			f[t]=t;
		}
	}
}D;
struct{
	#define lp p<<1
	#define rp p<<1|1
	vector<pair<int,int>>st[MAXN<<2];
	void mdf(int l,int r,int u,int v,int s=1,int t=k,int p=1){
		if(l<=s&&t<=r) return st[p].emplace_back(u,v),void();
		int mid=(s+t)>>1;
		if(l<=mid) mdf(l,r,u,v,s,mid,lp);
		if(mid<r) mdf(l,r,u,v,mid+1,t,rp);
	}
	bool fl=1;
	void dfs(int s,int t,int p){
		int tp=D.top;
		for(auto x:st[p]){
			int u=x.first,v=x.second;
			if(D.fnd(u)==D.fnd(v)){
				for(int i=s;i<=t;i++) No();
				return D.del(tp);
			}
			D.mge(u,v+n);
			D.mge(v,u+n);
		}
		if(s==t) Yes();
		else{
			int mid=(s+t)>>1;
			dfs(s,mid,lp),dfs(mid+1,t,rp);	
		}
		D.del(tp);
	}
}T;

int main(){
	n=read(),m=read(),k=read();
	for(int i=1,x,y,l,r;i<=m;i++){
		x=read(),y=read(),l=read(),r=read();
		T.mdf(l+1,r,x,y);
	}
	D.init(n<<1);
	T.dfs(1,k,1);
	return fw,0;
}

例题

CF576E Painting Edges

\(50\) 种颜色显然可以开 \(50\) 个并查集暴力维护。

与模板题的不同之处在于,这道题的操作只有合法才能被执行。考虑对于每一次修改,我们不直接插到线段树上,而是等到遍历到对应的叶子之后再考虑贡献。预处理每条边被修改的位置,当遍历到叶子节点 \(s\) 时,我们知道它下一次被修改是在位置 \(\text{nxt}(s)\),那么这之间的颜色就取决于当前是不是一个二分图。如果是,则这个操作可以被执行,就在线段树上区间修改为新颜色;否则仍然修改为旧颜色。

这种技巧被 UKE_Automation 称作 “半在线线段树分治”。

CF938G Shortest Path Queries

需要知道线性基是维护异或和问题的有力工具。本题恰好是图上线性基的经典应用。

先考虑如果不带修改怎么处理,也就是只剩下异或最短路。这是个经典问题,即 P4151 [WC2011] 最大XOR和路径。把最短路可以拆成一条链加许多环,假设我们找出了一条 \(1\leadsto n\) 的链,现在要用一些环来增广这条链。发现从这条链走到那个环再走回来的这个过程中,连接环和链的中间这一段并不提供贡献。于是我们只需找出所有环,扔进线性基里,最后求所有环和这条链的最大异或和即可。而这条链容易发现是随便选的,因为最优的链和任意一条链都构成环,想要得到最优链只需和这个大环异或一下即可。

然后回到本题。每条边有了出现时间,考虑线段树分治,在可撤销并查集上维护线性基。同时需要维护每个并查集中点到根的距离的异或和即可。查询时找出任意一条从 \(x\)\(y\) 的链在线性基上查询即可。

线段树二分

就是在线段树上进行二分,一般用来求 \(k\) 大值,或是维护了一些具有单调性的信息后固定一个端点、需要找出另一个端点的情况。

线段树二分的需要注意一些边界的细节问题。

吉司机线段树

用于解决区间最值 / 区间历史最值的问题。

区间最值问题

题意

需要实现以下操作:

  • 将区间 \([l,r]\) 的所有 \(a_i\) 变为 \(\min(a_i,x)\)
  • 将区间 \([l,r]\) 的所有 \(a_i\) 变为 \(\max(a_i,x)\)
  • 区间加、区间求和。

思路

对于无区间加减,线段树上每个节点维护区间最大值 \(\mathit{mx}\)严格次大值 \(\mathit{cmx}\),最大值出现次数 \(\mathit{tmx}\) 和区间和 \(c\)。然后对于每个区间:

  • \(\mathit{mx}\le x\),则无需操作;
  • \(\mathit{cmx}<x<\mathit{mx}\) 时,这次修改只影响最大值,更新 \(c=c-\mathit{tmx}\times(\mathit{mx}-x)\) 以及 \(\mathit{mx}=x\),打上标记后退出。
  • 否则,\(\mathit{cmx}\ge x\),无法直接更新这个节点,递归它的左右儿子。

容易发现,在这个过程中关键时严格次大值 \(\mathit{cmx}\),它起到了剪枝的作用,保证了均摊复杂度 \(O(m\log n)\)


对于有区间加减,有两种办法:

  • 多维护一个区间加标记;
  • 发现上文的方法实际上是把值域按照 \([\mathit{cmx},\mathit{mx}\,]\) 分成了两部分,考虑将最大值 / 最小值的部分拆出来另开标记维护。由此,取区间 \(\min/\max\) 的同样可以使用加减标记维护。

第二种方法更为重要,是之后很多更为复杂的情况的基础。这样一来,我们就需要维护区间和、区间最大值、最大值出现次数、严格次大值、区间最小值、最小值出现次数、严格次小值、最大值加法标记、最小值加法标记、其他值加法标记。下传标记的时候需要针对区间内有无最大值进行判断,到底是下传其他值标记还是最值标记。同时需要注意如果区间很小,可能会出现值域重叠的情况,需要特判。

代码(P10639 BZOJ4695 最佳女选手

using ll=long long;
constexpr int MAXN=5e5+5,INF=0x3f3f3f3f;
int n,m,a[MAXN];
struct{
	#define lp p<<1
	#define rp p<<1|1
	struct SegTree{
		int mx,cmx,tmx,mn,cmn,tmn,lmx,lmn,lad;
		ll sm;
		SegTree(){
			lmx=lmn=lad=sm=0;
			mx=cmx=-INF;
			mn=cmn=INF;
		}
		friend SegTree operator+(const SegTree&x,const SegTree&y){
            // 分讨,维护最大值、次大值、最大值出现次数
            // 最小值同理
			SegTree res;
			res.sm=x.sm+y.sm;
			if(x.mx==y.mx){
				res.mx=x.mx;
				res.cmx=max(x.cmx,y.cmx);
				res.tmx=x.tmx+y.tmx;
			}else if(x.mx>y.mx){
				res.mx=x.mx;
				res.cmx=max(x.cmx,y.mx);
				res.tmx=x.tmx;
			}else{
				res.mx=y.mx;
				res.cmx=max(x.mx,y.cmx);
				res.tmx=y.tmx;
			}
			if(x.mn==y.mn){
				res.mn=x.mn;
				res.cmn=min(x.cmn,y.cmn);
				res.tmn=x.tmn+y.tmn;
			}else if(x.mn<y.mn){
				res.mn=x.mn;
				res.cmn=min(x.cmn,y.mn);
				res.tmn=x.tmn;
			}else{
				res.mn=y.mn;
				res.cmn=min(x.mn,y.cmn);
				res.tmn=y.tmn;
			}
			return res;
		}
	}st[MAXN<<2];
	void pushtag(int lmx,int lmn,int lad,int s,int t,int p){
		if(st[p].mx==st[p].mn){ // 若区间只有一个数
			if(lmx==lad) lmx=lmn; // 则最大值和最小值加法标记应当一致
			else lmn=lmx;
			st[p].sm+=1ll*lmx*st[p].tmx;
		}else st[p].sm+=1ll*lmx*st[p].tmx+1ll*lmn*st[p].tmn+1ll*lad*(t-s+1-st[p].tmx-st[p].tmn);
        // 判断次大值是否和最小值发生重叠
		if(st[p].cmx==st[p].mn) st[p].cmx+=lmn;
		else if(st[p].cmx>-INF) st[p].cmx+=lad;
        // 判断次小值是否和最大值发生重叠
		if(st[p].cmn==st[p].mx) st[p].cmn+=lmx;
		else if(st[p].cmn<INF) st[p].cmn+=lad;
		st[p].mx+=lmx,st[p].mn+=lmn;
		st[p].lmx+=lmx,st[p].lmn+=lmn,st[p].lad+=lad;
	}
	void pushdown(int s,int t,int p){
		int mid=(s+t)>>1;
		int mx=max(st[lp].mx,st[rp].mx),mn=min(st[lp].mn,st[rp].mn);
        // 判断左右区间是否包含最大值、最小值
		pushtag(st[lp].mx==mx?st[p].lmx:st[p].lad,st[lp].mn==mn?st[p].lmn:st[p].lad,st[p].lad,s,mid,lp);
		pushtag(st[rp].mx==mx?st[p].lmx:st[p].lad,st[rp].mn==mn?st[p].lmn:st[p].lad,st[p].lad,mid+1,t,rp);
		st[p].lmx=st[p].lmn=st[p].lad=0;
	}
	void build(int s,int t,int p){
		if(s==t){
			st[p].sm=st[p].mx=st[p].mn=a[s];
			st[p].cmx=-INF,st[p].cmn=INF;
			st[p].tmx=st[p].tmn=1;
			return;
		}
		int mid=(s+t)>>1;
		build(s,mid,lp),build(mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	void add(int l,int r,int k,int s=1,int t=n,int p=1){
		if(l<=s&&t<=r) return pushtag(k,k,k,s,t,p);
		pushdown(s,t,p);
		int mid=(s+t)>>1;
		if(l<=mid) add(l,r,k,s,mid,lp);
		if(mid<r) add(l,r,k,mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	void amx(int l,int r,int k,int s=1,int t=n,int p=1){
        // 结合一开始讲的将区间最值操作转化为一定部分的加减
		if(st[p].mn>=k) return;
		if(l<=s&&t<=r&&st[p].cmn>k) return pushtag(0,k-st[p].mn,0,s,t,p);
		pushdown(s,t,p);
		int mid=(s+t)>>1;
		if(l<=mid) amx(l,r,k,s,mid,lp);
		if(mid<r) amx(l,r,k,mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	void amn(int l,int r,int k,int s=1,int t=n,int p=1){
		if(st[p].mx<=k) return;
		if(l<=s&&t<=r&&st[p].cmx<k) return pushtag(k-st[p].mx,0,0,s,t,p);
		pushdown(s,t,p);
		int mid=(s+t)>>1;
		if(l<=mid) amn(l,r,k,s,mid,lp);
		if(mid<r) amn(l,r,k,mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	SegTree ask(int l,int r,int s=1,int t=n,int p=1){
		if(l<=s&&t<=r) return st[p];
		pushdown(s,t,p);
		int mid=(s+t)>>1;
		if(l>mid) return ask(l,r,mid+1,t,rp);
		if(mid>=r) return ask(l,r,s,mid,lp);
		return ask(l,r,s,mid,lp)+ask(l,r,mid+1,t,rp);
	}
}T;

区间历史最值

区间历史最值问题则是需要我们在维护原序列 \(a_i\) 的基础上,还要维护一个 \(b_i\),其中 \(b_i\)\(a_i\) 在所有修改操作中的最值。所以我们需要对每一个区间维护两个值 \(a,b\),表示当前值和历史最值。至于维护,一种好写的方法是使用广义矩阵乘法,把信息看作一个矩阵 \(\begin{bmatrix}a\\b\end{bmatrix}\)

P4314 CPU 监控

以下代码拷自 UKE_Automation。

对于区间加,我们的目标是 \(\begin{bmatrix}a\\b\end{bmatrix}\leftarrow \begin{bmatrix}a+k\\\max(b,a+k)\end{bmatrix}\)。那么可以轻易构造出如下广义矩阵乘法:

\[\begin{bmatrix}k&-\infty\\k& 0\end{bmatrix} \begin{bmatrix}a\\b\end{bmatrix}=\begin{bmatrix}a+k\\\max(b,a+k)\end{bmatrix} \]

对于区间覆盖,这个向量似乎难以完成任务。不过我们可以给它加上一维辅助的变量,变成 \(\begin{bmatrix}a\\b\\0\end{bmatrix}\),于是有:

\[\begin{bmatrix}-\infty &-\infty& k\\-\infty&0&k\\-\infty&-\infty&0\end{bmatrix}\begin{bmatrix}a\\b\\0\end{bmatrix}=\begin{bmatrix}k\\\max(k,b)\\0 \end{bmatrix} \]

当然你也不能一直拿着一个矩阵在那瞎乘,毕竟 \(3^3\) 的常数还是有的。我们发现,上面所构造出来的矩阵均可以写作 \(\begin{bmatrix}a &-\infty& c\\b&0&d\\-\infty&-\infty&0\end{bmatrix}\) 的形式,我们试着把两个矩阵标记相乘:

\[\begin{bmatrix}a_1 &-\infty& c_1\\b_1&0&d_1\\-\infty&-\infty&0\end{bmatrix}\begin{bmatrix}a_2 &-\infty& c_2\\b_2&0&d_2\\-\infty&-\infty&0\end{bmatrix}=\begin{bmatrix}a_1+a_2 &-\infty& \max(a_1+c_2,c_1)\\\max(b_1+a_2,b_2)&0&\max(b_1+c_2,d_1,d_2)\\-\infty&-\infty&0\end{bmatrix} \]

发现要维护的始终只有这四个值,所以直接维护即可。需要注意的是这个矩阵的初始形式是 \(\begin{bmatrix}0 &-\infty& -\infty\\-\infty&0&-\infty\\-\infty&-\infty&0\end{bmatrix}\),所以初始化的时候要将标记初始化为这个样子。以及由于标记是左乘的,所以要注意运算时的顺序。

#include<bits/stdc++.h>
using namespace std;

using ll=long long;
constexpr int MAXN=1e5+5;
constexpr ll ING=INT_MIN;
int n,a[MAXN],m;
struct SG{
	#define lp p<<1
	#define rp p<<1|1
	struct SegTree{
		ll mx,hmx;
		struct Tag{
			ll a,b,c,d;
			Tag(ll a=0,ll b=ING,ll c=ING,ll d=ING):a(a),b(b),c(c),d(d){}
			friend Tag operator+(const Tag&x,const Tag&y){
                // 简化的广义矩阵乘法
				Tag res;
				res.a=x.a+y.a;
				res.b=max(x.b+y.a,y.b);
				res.c=max(x.a+y.c,x.c);
				res.d=max({x.b+y.c,x.d,y.d});
				return res;
			}
		}tag;
		SegTree(ll mx=0,ll hmx=0):mx(mx),hmx(hmx),tag(){}
		friend SegTree operator+(const SegTree&x,const SegTree&y){
			SegTree res;
			res.mx=max(x.mx,y.mx);
			res.hmx=max(x.hmx,y.hmx);
			return res;
		}
	}st[MAXN<<2];
	void build(int s,int t,int p){
		if(s==t) return st[p]=SegTree(a[s],a[s]),void();
		int mid=(s+t)>>1;
		build(s,mid,lp),build(mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	void pushtag(SegTree::Tag k,int p){
        // 照着矩阵打
		st[p].tag=k+st[p].tag;
		st[p].hmx=max({k.b+st[p].mx,st[p].hmx,k.d});
		st[p].mx=max(k.a+st[p].mx,k.c);
	}
	void pushdown(int p){
		pushtag(st[p].tag,lp),pushtag(st[p].tag,rp);
		st[p].tag=SegTree::Tag();
	}
	void mdf(int l,int r,SegTree::Tag k,int s=1,int t=n,int p=1){
		if(l<=s&&t<=r) return pushtag(k,p);
		pushdown(p);
		int mid=(s+t)>>1;
		if(l<=mid) mdf(l,r,k,s,mid,lp);
		if(mid<r) mdf(l,r,k,mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	SegTree ask(int l,int r,int s=1,int t=n,int p=1){
		if(l<=s&&t<=r) return st[p];
		pushdown(p);
		int mid=(s+t)>>1;
		if(l>mid) return ask(l,r,mid+1,t,rp);
		if(mid>=r) return ask(l,r,s,mid,lp);
		return ask(l,r,s,mid,lp)+ask(l,r,mid+1,t,rp);
	}
}T;

int main(){
	cin.tie(nullptr)->sync_with_stdio(0);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	T.build(1,n,1);
	cin>>m;
	while(m--){
		char op;
		int x,y,z;
		cin>>op>>x>>y;
		if(op=='Q') cout<<T.ask(x,y).mx<<'\n';
		else if(op=='A') cout<<T.ask(x,y).hmx<<'\n';
		else if(op=='P') cin>>z,T.mdf(x,y,SG::SegTree::Tag(z,z,ING,ING));
		else cin>>z,T.mdf(x,y,SG::SegTree::Tag(ING,ING,z,z));
	}
	return 0;
}

P6242 【模板】线段树 3(区间最值操作、区间历史最值)

相当于把区间最值和区间历史最值结合了起来。于是对于最大值和非最大值各开一套标记维护。注意查询区间历史最大值的时候实际上是取历史最大值和历史次大值的 \(\max\)

using ll=long long;
constexpr int MAXN=5e5+5;
constexpr ll INF=0x3f3f3f3f3f3f3f3f;
int n,m,a[MAXN];
struct{
	#define lp p<<1
	#define rp p<<1|1
	struct Tag{
		ll a,b;
		Tag(ll a=0,ll b=-INF):a(a),b(b){}
		friend Tag operator+(const Tag&x,const Tag&y){
			return Tag(x.a+y.a,max(x.b+y.a,y.b));
		}
	};
	struct SegTree{
		ll sm,tmx,mx,cmx,hmx,hcmx;
		Tag tgmx,tgcmx;
		SegTree(){
			sm=tmx=mx=hmx=0;
			cmx=hcmx=-INF;
			tgmx=tgcmx=Tag();
		}
		friend SegTree operator+(const SegTree&x,const SegTree&y){
			SegTree res;
			res.sm=x.sm+y.sm;
			if(x.mx==y.mx){
				res.mx=x.mx;
				res.cmx=max(x.cmx,y.cmx);
				res.tmx=x.tmx+y.tmx;
				res.hmx=max(x.hmx,y.hmx);
				res.hcmx=max(x.hcmx,y.hcmx);
			}else if(x.mx>y.mx){
				res.mx=x.mx;
				res.cmx=max(x.cmx,y.mx);
				res.tmx=x.tmx;
				res.hmx=x.hmx;
				res.hcmx=max({x.hcmx,y.hmx,y.hcmx});
			}else{
				res.mx=y.mx;
				res.cmx=max(x.mx,y.cmx);
				res.tmx=y.tmx;
				res.hmx=y.hmx;
				res.hcmx=max({x.hcmx,x.hmx,y.hcmx});
			}
			return res;
		}
	}st[MAXN<<2];
	void build(int s,int t,int p){
		if(s==t){
			st[p].sm=st[p].mx=st[p].hmx=a[s];
			st[p].tmx=1;
			st[p].cmx=st[p].hcmx=-INF;
			return;
		}
		int mid=(s+t)>>1;
		build(s,mid,lp),build(mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	void pushtag(Tag tgmx,Tag tgcmx,int s,int t,int p){
        // 因为需要维护区间历史最大值、历史次大值,各开一套标记
		st[p].sm+=st[p].tmx*tgmx.a+(t-s+1-st[p].tmx)*tgcmx.a;
		st[p].hmx=max(st[p].mx+tgmx.b,st[p].hmx);
		st[p].mx+=tgmx.a;
        // 如果有次大值就更新它
		if(st[p].cmx>-INF){
			st[p].hcmx=max(st[p].cmx+tgcmx.b,st[p].hcmx);
			st[p].cmx+=tgcmx.a;
		}
		st[p].tgmx=tgmx+st[p].tgmx;
		st[p].tgcmx=tgcmx+st[p].tgcmx;
	}
	void pushdown(int s,int t,int p){
		int mid=(s+t)>>1;
		ll mx=max(st[lp].mx,st[rp].mx);
		pushtag(st[lp].mx==mx?st[p].tgmx:st[p].tgcmx,st[p].tgcmx,s,mid,lp);
		pushtag(st[rp].mx==mx?st[p].tgmx:st[p].tgcmx,st[p].tgcmx,mid+1,t,rp);
		st[p].tgmx=st[p].tgcmx=Tag();
	}
	void add(int l,int r,ll k,int s=1,int t=n,int p=1){
		if(l<=s&&t<=r) return pushtag(Tag(k,k),Tag(k,k),s,t,p);
		pushdown(s,t,p);
		int mid=(s+t)>>1;
		if(l<=mid) add(l,r,k,s,mid,lp);
		if(mid<r) add(l,r,k,mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	void amn(int l,int r,ll k,int s=1,int t=n,int p=1){
		if(st[p].mx<=k) return;
		if(l<=s&&t<=r&&st[p].cmx<k) return pushtag(Tag(k-st[p].mx,k-st[p].mx),Tag(),s,t,p);
		pushdown(s,t,p);
		int mid=(s+t)>>1;
		if(l<=mid) amn(l,r,k,s,mid,lp);
		if(mid<r) amn(l,r,k,mid+1,t,rp);
		st[p]=st[lp]+st[rp];
	}
	SegTree ask(int l,int r,int s=1,int t=n,int p=1){
		if(l<=s&&t<=r) return st[p];
		pushdown(s,t,p);
		int mid=(s+t)>>1;
		if(l>mid) return ask(l,r,mid+1,t,rp);
		if(mid>=r) return ask(l,r,s,mid,lp);
		return ask(l,r,s,mid,lp)+ask(l,r,mid+1,t,rp);
	}
}T;

int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	T.build(1,n,1);
	while(m--){
		int op=read(),l=read(),r=read();
		switch(op){
			case 1:{
				T.add(l,r,read());
				break;
			}case 2:{
				T.amn(l,r,read());
				break;
			}case 3:{
				write(T.ask(l,r).sm);
				break;
			}case 4:{
				write(T.ask(l,r).mx);
				break;
			}default:{
				auto res=T.ask(l,r);
				write(max(res.hmx,res.hcmx));
				break;
			}
		}
	}
	return fw,0;
}

前缀最值线段树

典型例题:P4198 楼房重建

这种线段树需要我们维护出前缀最大值的个数。考虑线段树上维护两个标记:\(\mathit{mx}\) 表示区间最大值,\(c\) 表示前缀最大值个数,即答案。

考虑如何 pushup。前者是容易的,难点在于后者。我们发现左区间的前缀最大值一定能被选上,而右区间需要取一段最小值大于左区间的 \(\mathit{mx}\) 的后缀。考虑用线段树二分来解决这个问题。在线段树二分中,设当前二分的是大于 \(v\) 的后缀,若左区间的最大值小于 \(v\),则只需在右区间查询;否则说明右区间的一定全部能被选,答案等于左区间的答案加上 \(c(p)-c(\mathit{lp})\)

因为这种线段树的 pushup 是带 \(\log\) 的,所以复杂度为 \(O(n\log^2n)\)

using db=double;
constexpr int MAXN=1e5+5;
int n,m;
struct{
	#define lp mid<<1
	#define rp mid<<1|1
	struct{
		db mx;
		int c;
	}st[MAXN<<1];
	il int qry(db k,int l,int r,int p){
		if(l==r) return st[p].mx>k;
		else if(st[p].mx<=k) return 0;
		int mid=(l+r)>>1;
		if(st[lp].mx<=k) return qry(k,mid+1,r,rp);
		return qry(k,l,mid,lp)+st[p].c-st[lp].c;
	}
	void chg(int x,db k,int s=1,int t=n,int p=1){
		if(s==t) return st[p].mx=k,st[p].c=1,void();
		int mid=(s+t)>>1;
		if(x<=mid) chg(x,k,s,mid,lp);
		else chg(x,k,mid+1,t,rp);
		st[p].mx=max(st[lp].mx,st[rp].mx);
		st[p].c=st[lp].c+qry(st[lp].mx,mid+1,t,rp);
	}
}T;

int main(){
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read();
		T.chg(x,1.*y/x);
		write(T.st[1].c);
	}
	return fw,0;
}

到这里你已经会做这道题了。但是我们发现我们其中有一步的答案是用 \(c(p)\)\(c(\mathit{lp})\) 减出来的。但如果我们维护的不是前缀最大值数量,而是一些别的东西,比如按位与、按位或、最值等,它们不满足可减性,于是就维护不了了。为了让这种线段树摆脱可减性的限制,我们修改 \(c(p)\) 的定义为统计当前区间 \([\mathit{mid}+1,r]\) 的答案。此时叶子节点的 \(c\) 视为未定义,修改维护方式即可。注意此时我们不能直接查询根节点的答案,而是需要再调用一次线段树二分得到答案。

using db=double;
constexpr int MAXN=1e5+5;
int n,m;
struct{
	#define lp mid<<1
	#define rp mid<<1|1
	struct{
		db mx;
		int c;
	}st[MAXN<<1];
	il int qry(db k,int l,int r,int p){
		if(l==r) return st[p].mx>k;
		else if(st[p].mx<=k) return 0;
		int mid=(l+r)>>1;
		if(st[lp].mx<=k) return qry(k,mid+1,r,rp);
		return qry(k,l,mid,lp)+st[p].c;
	}
	il void chg(int x,db k,int s=1,int t=n,int p=1){
		if(s==t) return st[p].mx=k,void();
		int mid=(s+t)>>1;
		if(x<=mid) chg(x,k,s,mid,lp);
		else chg(x,k,mid+1,t,rp);
		st[p].mx=max(st[lp].mx,st[rp].mx);
		st[p].c=qry(st[lp].mx,mid+1,t,rp);
	}
}T;

int main(){
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int x=read(),y=read();
		T.chg(x,1.*y/x);
		write(T.qry(0,1,n,1));
	}
	return fw,0;
}

李超线段树

用于解决在这样一个问题:在一个二维平面上扔若干条直线 / 线段,然后给一个 \(x\) 坐标,让你求这条竖线所交的线段中 \(y\) 坐标最值。

我们可以把一条两端点为 \((x_1,y_1)\)\((x_2,y_2)\) 的线段看作一个定义域为 \([x_1,x_2]\) 的一次函数,这样一条线段可以看作是区间修改,那么我们就可以通过打懒标记来维护。一个区间的懒标记维护的是区间中点处纵坐标的最大值,也可以看作是维护完全包含这个区间的这样的一条线段。

现在要加入一条新线段。当遇到有标记的区间时,只能递归下传标记。具体地,我们分讨两条线段的位置关系,分为相交和不相交两种。如果不相交,则直接取更优的那一条;否则我们可以通过判断两条线段在区间中点处的取值来判断处交点位于左右哪个子区间。那么对于没有交点的子区间,更新懒标记;然后递归处理另一子区间即可。

容易发现,修改一次 \(O(\log^2n)\),查询一次 \(O(\log n)\)

P4097 【模板】李超线段树 / [HEOI2013] Segment

模板题,核心代码:

using db=double;
using pdi=pair<db,int>;
constexpr int MAXM=4e4,M=39989,V=1e9;
constexpr db eps=1e-9;
int n;
struct Node{
	int id=0;
	db k=0,b=0;
	il Node(int x1=0,int y1=0,int x2=0,int y2=0,int id=0):id(id){
		if(x1==x2) b=max(y1,y2);
		else{
			k=1.*(y2-y1)/(x2-x1);
			b=y1-k*x1;
		}
	}
	il bool empty(){
		return !k&&!b;
	}
	il db operator()(int x){
		return k*x+b;
	}
};
il int cmp(db a,db b){
	if(fabs(a-b)<=eps) return 0;
	return b-a>eps?-1:1;
}
il pdi max(const pdi&a,const pdi&b){
	if(cmp(a.first,b.first)) return a.first>b.first?a:b;
	return a.second<b.second?a:b;
}
struct{
	#define lp mid<<1
	#define rp mid<<1|1
	Node st[MAXM<<1];
	il void ins(Node k,int s,int t,int p){
		if(st[p].empty()) return st[p]=move(k),void();
		int mid=(s+t)>>1;
		if(!~cmp(st[p](mid),k(mid))) swap(st[p],k);
		if(s==t||(cmp(st[p](s),k(s))==1&&cmp(st[p](t),k(t))==1)) return;
		if(cmp(st[p](s),k(s))==1) ins(k,mid+1,t,rp);
		else ins(k,s,mid,lp);
	}
	il void mdf(int l,int r,const Node&k,int s=1,int t=M,int p=1){
		if(l<=s&&t<=r) return ins(k,s,t,p);
		int mid=(s+t)>>1;
		if(l<=mid) mdf(l,r,k,s,mid,lp);
		if(mid<r) mdf(l,r,k,mid+1,t,rp);
	}
	il pdi query(int x,int s=1,int t=M,int p=1){
		if(s==t) return {st[p](x),st[p].id};
		int mid=(s+t)>>1;
		pdi res={st[p](x),st[p].id};
		if(x<=mid) return max(res,query(x,s,mid,lp));
		else return max(res,query(x,mid+1,t,rp));
	}
}T;
posted @ 2025-04-06 19:49  Laoshan_PLUS  阅读(40)  评论(0)    收藏  举报