线段树进阶操作

一、李超线段树

1、标记永久化

一言以蔽之:标记永久化就是不再下放标记,而是让标记永久地停留在线段树的节点上,统计答案时再考虑这些标记的影响

2、维护直线

我们以下面这道题为例子来进行讲解

[JSOI2008] Blue Mary开公司

题意:要求支持操作

  • Project:插入一条 \(y=kx+b\) 的直线,给定 \(k,b\)
  • Query:求所有直线中与直线 \(x=t\) 的交点的纵坐标最大值是多少

我们首先建立一棵线段树,每个节点代表一个区间,且有一个标记记录一条直线

对于插入直线的操作:

  • 如果当前区间还没有标记,我们将这个区间标记为当前直线

  • 如果有标记,但插入的直线完全覆盖原先的直线,即替换掉原先的直线

  • 如果插入的直线被原先的直线完全覆盖,即返回

  • 剩下的情况就是插入的和原先的直线在区间内有交点。那么我们令 \(mid=(l+r)/2\),令与直线 \(x=mid\) 交点纵坐标更大的直线作为当前区间被标记的直线。然后递归交点所在的区间子树,继续修改即可

对于查询操作,类似标记永久化,找到所有覆盖了 \(x=t\) 的区间,考虑该区间的贡献即可

时间复杂度:\(O(n\log n)\)

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

const int N=100010,T=50010;
const double eps=1e-12;

struct line
{
	double k,b; //斜率和截距
	int l,r; 
	bool flag; //标记
	#define flag(x)  tree[x].flag
}tree[4*T];

int n;
char op[10];

double calc(line a,int x) //通过x计算y
{
	return (double)x*a.k+a.b;
}

void build(int p,int l,int r)
{
	tree[p]=(line){0,0,1,50000,0};
	if(l==r)
		return;
	
	int mid=(l+r)>>1;
	build(p*2,l,mid);
	build(p*2+1,mid+1,r);
}

void change(int p,int l,int r,line k)
{
	if(k.l<=l && k.r>=r) //完全覆盖区间
	{
		if(!flag(p)) //没有标记
			tree[p]=k,flag(p)=1;
		else if(calc(k,l)-calc(tree[p],l)>eps && calc(k,r)-calc(tree[p],r)>eps) //有标记但插入的更优
			tree[p]=k;
		else if(calc(k,l)-calc(tree[p],l)>eps || calc(k,r)-calc(tree[p],r)>eps) //有交点
		{
			int mid=(l+r)>>1;
			if(calc(k,mid)-calc(tree[p],mid)>eps) //令与x=mid交点更高的作为标记
				swap(tree[p],k);
			
			if(calc(k,l)-calc(tree[p],l)>eps) //递归交点的区间子树
				change(p*2,l,mid,k);
			else
				change(p*2+1,mid+1,r,k);
		}
	}
	else //未完全覆盖
	{
		int mid=(l+r)>>1;
		if(k.l<=mid)
			change(p*2,l,mid,k);
		if(k.r>mid)
			change(p*2+1,mid+1,r,k);
	}
}

double ask(int p,int l,int r,int x) //标记永久化的查询需要不断递归直到一个点
{
	if(l==r)
	{
		if(flag(p))
			return calc(tree[p],x);
		return -1e18;
	}
	
	int mid=(l+r)>>1;
	double val=-1e18;
	if(flag(p))
		val=calc(tree[p],x); //当前点的标记
	if(x<=mid) //递归子树
		return max(val,ask(p*2,l,mid,x));
	return max(val,ask(p*2+1,mid+1,r,x));
 } 

int main()
{
	build(1,1,50000);
	
	scanf("%d",&n);
	for(int i=1; i<=n; i++)
	{
		scanf("%s",op);
		if(op[0]=='P')
		{
			double s,p;
			scanf("%lf%lf",&s,&p);
			
			line now=(line){p,s-p,1,50000,1};
			change(1,1,50000,now);
		}
		else
		{
			int t;
			scanf("%d",&t);
			
			double ans=ask(1,1,50000,t);
			int anss=(int)(ans/100.0);
			if(anss<0)
				printf("0\n");
			else
				printf("%d\n",anss);
		}
	}
		
	return 0;
}

[CEOI2017] Building Bridges

首先 \(O(n^2)\) 的 dp 很好想
\(f_i\) 表示连接第 \(1\) 根和第 \(i\) 根柱子的最小代价,答案即为 \(f_n\)
那么状态转移方程也是显然的:
\(f_i=\min\{f_j+(h_i-h_j)^2+\sum\limits_{k=j+1}^{i-1}w_k\}\)

现在我们令 \(w_i+=w_{i-1}\),即做一次前缀和,则有
\(f_i=\min\{f_j+(h_i-h_j)^2+w_{i-1}-w_j\}\)
考虑优化,将式子化简得:
\(f_i=h_i^2+w_{i-1}+\min\{f_j-2h_ih_j+h_j^2-w_j\}\)
我们令 \(k=-2h_j,\:b=f_j+h_j^2-w_j\),则后面的式子转化为一条直线 \(y=kh_i+b\),问题转化为求所有直线与 \(x=h_i\) 的交点的纵坐标的最小值,并插入当前自己所代表的直线

二、线段树合并

1、知识点

前置知识:动态开点线段树

线段树合并是一个递归的过程。我们合并两棵线段树时,用两个指针 \(p,q\) 从两个根节点出发,以递归的方式同步遍历两棵线段树。

  • \(p,q\) 之一为空,则以非空的那个作为合并的节点

  • \(p,q\) 均不为空,则递归合并两棵左子树和两棵右子树,然后删除节点 \(q\),以 \(p\) 为合并后的新节点,然后删除节点 \(q\),以 \(p\) 作为合并后的节点,更新信息

参考代码

int merge(int p,int q,int l,int r)
{
	if(!p)
	    return q;
	if(!q)
	    return p;
	
	if(l==r)
	{
	    dat(p)+=dat(q)
	    return p;
	}
	
	int mid=(l+r)>>1;
	lc(p)=merge(lc(p),lc(q),l,mid);
	rc(p)=merge(rc(p),rc(q),mid+1,r);
	pushup(p);
	
	return p;
}

时间复杂度 & 空间复杂度: \(O(n\log n)\)

2、一些习题

【模板】线段树合并 / [Vani有约会] 雨天的尾巴

根据套路,容易想到先找出 \(x,y\) 的最近公共祖先 \(lca\),之后进行树上差分。设 \(b[z]\) 为差分数组,对于每次操作,令 \(b[z][x]+1\)\(b[z][y]+1\)\(b[z][lca]-1\)\(b[z][fa[lca]]-1\) 即可。

现在考虑优化,我们可以对每一个点建立一棵权值线段树来替代差分数组,维护存放最多的救济粮的类型和数量,之后从根节点开始进行一次 \(\mathrm{dfs}\),对于 \(x\) 节点的所有儿子 \(y\),将它们的线段树合并起来,即完成差分数组最后的求前缀和的过程

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

const int N=100010,M=100000;

struct SegmentTree
{
	int val,ki;
	int lc,rc;
	#define lc(x)  tree[x].lc
	#define rc(x)  tree[x].rc 
	#define val(x)  tree[x].val
	#define ki(x)  tree[x].ki
}tree[80*N];

int n,m,t;
int dep[N],f[N][20],tot;
int rt[N],ans[N];
vector <int> g[N];
queue <int> q;

void bfs()
{
	q.push(1);
	dep[1]=1;
	
	while(q.size())
	{
		int x=q.front();  q.pop();
		for(int i=0; i<g[x].size(); i++)
		{
			int y=g[x][i];
			if(dep[y])
				continue;
			
			dep[y]=dep[x]+1;
			f[y][0]=x;
			for(int j=1; j<=t; j++)
				f[y][j]=f[f[y][j-1]][j-1];
				
			q.push(y); 
		}
	}
}

int LCA(int x,int y)
{
	if(dep[x]>dep[y])
		swap(x,y);
	for(int i=t; i>=0; i--)
		if(dep[f[y][i]]>=dep[x])
			y=f[y][i];
			
	if(x==y)
		return x;
		
	for(int i=t; i>=0; i--)
		if(f[x][i]!=f[y][i])
			x=f[x][i],y=f[y][i];
	
	return f[x][0];
}

void pushup(int p)
{
	if(val(lc(p))>=val(rc(p)))
		val(p)=val(lc(p)),ki(p)=ki(lc(p));
	else
		val(p)=val(rc(p)),ki(p)=ki(rc(p));
}

void change(int &p,int l,int r,int pos,int v)
{
	if(!p)
		p=++tot;
		
	if(l==r)
	{
		val(p)+=v;
		ki(p)=pos;
		return; 
	}
	
	int mid=(l+r)>>1;
	if(pos<=mid)
		change(lc(p),l,mid,pos,v);
	else
		change(rc(p),mid+1,r,pos,v);
		
	pushup(p);
}

int merge(int p,int q,int l,int r)
{
	if(!p)
		return q;
	if(!q)
		return p;
	
	if(l==r)
	{
		val(p)+=val(q);
		ki(p)=l;
		return p;
	}
	
	int mid=(l+r)>>1;
	lc(p)=merge(lc(p),lc(q),l,mid);
	rc(p)=merge(rc(p),rc(q),mid+1,r);
	pushup(p);
	
	return p;
}

void dfs(int x,int fa)
{
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
		
		dfs(y,x);
		rt[x]=merge(rt[x],rt[y],1,M);
	}
	
	if(val(rt[x]))
		ans[x]=ki(rt[x]);
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	
	t=(log(n)/log(2))+1;
	bfs();
	
	for(int i=1; i<=m; i++)
	{
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		
		int lca=LCA(x,y);
		
		change(rt[x],1,M,z,1);
		change(rt[y],1,M,z,1);
		change(rt[lca],1,M,z,-1);
		change(rt[f[lca][0]],1,M,z,-1);
	}
	
	dfs(1,0);
	
	for(int i=1; i<=n; i++)
		printf("%d\n",ans[i]);
			
	return 0;
}

CF600E Lomsat gelral

对每个节点建立一棵权值线段树,维护占主导地位的颜色的出现次数和编号和,再进行一次 \(\mathrm{dfs}\) 合并线段树即可

[HNOI2012] 永无乡

考虑用并查集去维护每座岛之间的连通性,并用连通块的祖先去代表整个连通块

对每座岛建立一棵权值线段树,每次查询操作在线段树上二分即可

对于建桥操作,合并两个连通块即可

[POI2011] ROT-Tree Rotations

对于节点 \(x\),设它的儿子子树分别为 \(y_1,y_2\),并且 \(y_1\)\(y_2\) 左边。分析逆序对的来源:

  • 在同一个 \(y\) 子树

  • 在不同的 \(y\) 子树

对于节点 \(x\) 来说,要做的就是合并 \(y_1,y_2\) 并计算贡献。显然第 \(1\) 种来源已经在以 \(y_1,y_2\) 为根的子树中计算过了,所以我们只需通过合并操作计算来源 \(2\)

考虑对每个节点建立权值线段树,设值域区间内数字的个数为 \(sum\),那么逆序对个数就是 \(sum(rc(p))*sum(lc(q))\)

现在考虑交换子树的操作。显然交换子树的操作只会对来源2产生影响。那我们取 \(sum(lc(p))*sum(rc(q))\)\(sum(rc(p))*sum(lc(q))\) 的最小值即可

CF208E Blood Cousins

直接找 \(a\)\(p\) 级表亲是比较困难的,所以考虑先求出 \(a\)\(p\) 级祖先 \(z\),将询问离线转化到 \(z\) 上,再求 \(z\) 子树内有多少个深度等于 \(dep[z]+p\) 的节点,记为 \(cnt\),那么该询问的答案就是 \(cnt-1\)

对于节点 \(x\),考虑如何求出其子树内有多少个深度为 \(dep[x]+p\) 的节点。我们可以以深度为下标建立权值线段树,查询时单点查询,然后不断合并即可。

具体实现时,在树的遍历时,可以开两个数组对询问进行处理。求 \(x\)\(p\) 级祖先,可以树上倍增,也可以开一个栈,在遍历到节点 \(x\) 时入栈,返回时出栈,设当前栈顶下标为 \(t\),这样显然 \(x\)\(p\) 级祖先就是 \(s[t-p]\)

一个小细节,该题的图不一定只有一课树,可能是多棵树,需要注意。

[Cnoi2019] 雪松果树

(上一题 Blood Cousins 的卡空间版)

大部分代码和上一题相同,这里主要讲如何优化空间

首先合并时,将子树按 \(size\) 从大到小合并,这样可以减少合并时的空间浪费

其次,合并完后,将无用的那个子树的空间回收起来,以后在动态开点时,优先从回收站里拿空间

最后,把 vector 换成链式前向星

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

const int N=1000010,M=1000000;

struct SegmentTree
{
	int lc,rc;
	int sum;
	#define lc(x)  tree[x].lc
	#define rc(x)  tree[x].rc
	#define sum(x)  tree[x].sum
}tree[8*N];

struct node
{
	int k,id,nxtt;
}qa[N],qb[N]; //qa,qb与上一题相同
int n,q,rt[N],tot,ans[N];
int dep[N],size[N],a[N],cnt;
int ha[N],tota,hb[N],totb;
int sa[N],ta,sb[N],tb; //sa是求k-祖先的栈,sb是回收空间用的
vector <int> g[N];

void adda(int x,int k,int id)
{
	qa[++tota]=(node){k,id,ha[x]};
	ha[x]=tota;
}

void addb(int x,int k,int id)
{
	qb[++totb]=(node){k,id,hb[x]};
	hb[x]=totb;
}

bool cmp(int x,int y)
{
	return size[x]>size[y];
}

void pushup(int p)
{
	sum(p)=sum(lc(p))+sum(rc(p));
}

void change(int &p,int l,int r,int pos,int v)
{
	if(!p)
	{
		if(tb)
			p=sb[tb--]; //优先拿回收站
		else
			p=++tot;
	}
		
	if(l==r)
	{
		sum(p)+=v;
		return; 
	}
	
	int mid=(l+r)>>1;
	if(pos<=mid)
		change(lc(p),l,mid,pos,v);
	else
		change(rc(p),mid+1,r,pos,v); 
		
	pushup(p);
}

int merge(int p,int q,int l,int r)
{
	if(!p)
		return q;
	if(!q)
		return p;
	
	if(l==r)
	{
		sum(p)+=sum(q);
		lc(q)=rc(q)=sum(q)=0; //回收
		sb[++tb]=q;
		return p;
	}
	
	int mid=(l+r)>>1;
	lc(p)=merge(lc(p),lc(q),l,mid);
	rc(p)=merge(rc(p),rc(q),mid+1,r);
	pushup(p);
	
	lc(q)=rc(q)=sum(q)=0;
	sb[++tb]=q;
	
	return p;
}

int ask(int p,int l,int r,int pos)
{
	if(l==r)
		return sum(p);
	
	int mid=(l+r)>>1;
	if(pos<=mid)
		return ask(lc(p),l,mid,pos);
	return ask(rc(p),mid+1,r,pos);
}

void solve(int x)
{
	sa[++ta]=x;
	for(int i=ha[x]; i; i=qa[i].nxtt)
	{
		int k=qa[i].k,id=qa[i].id;
		if(ta>k)
			addb(sa[ta-k],dep[x],id);
	}
	
	int l=cnt;
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		a[++cnt]=y;
	}
	int r=cnt;
	
	sort(a+l+1,a+r+1,cmp); //按子树大小从大到小排序
	
	for(int i=l+1; i<=r; i++)
	{
		int y=a[i];
		
		solve(y);
		rt[x]=merge(rt[x],rt[y],1,M);
	}
	
	change(rt[x],1,M,dep[x],1);
	
	for(int i=hb[x]; i; i=qb[i].nxtt)
	{
		int k=qb[i].k,id=qb[i].id;
		ans[id]=ask(rt[x],1,M,k);
	}
	
	ta--;
}

void dfs(int x,int fa)
{
	dep[x]=dep[fa]+1;
	size[x]=1;
	
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		
		dfs(y,x);
		size[x]+=size[y];
	}
}

int main()
{
	scanf("%d%d",&n,&q);
	for(int i=2; i<=n; i++)
	{
		int x;
		scanf("%d",&x);
		g[x].push_back(i);
	}
	
	dfs(1,0);
	
	for(int i=1; i<=q; i++)
	{
		int x,k;
		scanf("%d%d",&x,&k);
		adda(x,k,i);
	}
	
	solve(1);
	
	for(int i=1; i<=q; i++)
		printf("%d ",max(ans[i]-1,0));
	
	return 0;
}

[湖南集训] 更为厉害

因为 \(a,b\) 都是 \(c\) 的祖先,所以容易看出 \(a,b,c\) 在同一条链上

如果 \(b\)\(a\) 的上方,显然答案就是 \((size[a]-1)\times \min(dep[a]-1,k)\)

如果 \(b\)\(a\) 的下方,那么 \(a\) 子树内所有深度在 \([dep[a]+1,dep[a]+k]\) 范围内的点都可以作为 \(b\) 的候选项,此时 \(c\) 的数量就是 \(size[b]-1\)。因此我们可以以深度为下标建立权值线段树,求下标为 \([dep[a]+1,dep[a]+k]\) 内的 \(size-1\) 的和即可

CF570D Tree Requests

在每个节点同样以深度为下标建立线段树,存储该子树内深度相同的点所构成的字符集合

因为题目要求的是能否构成回文串。显然只要所有的字符都出现偶数次或只有一个字符出现次数为 \(1\) 即可,那么考虑将 \(26\) 个字母二进制压缩,每次 check 一下这个二进制数是否合法即可

CF246E Blood Cousins Return

容易想到用 map 给每个名字编号

对每个节点同样以深度为下标建立权值线段树,因为要去重计数,所以对线段树的每个叶子节点开一个 set,查询时返回 set 的大小即可

合并时,同 \(\text{雪松果树}\),将小的 set 合并到大的上

CF932F Escape Through Leaf

(李超线段树的合并)

考虑树形 \(\mathrm{dp}\),设 \(x\) 的儿子为 \(y\)\(f[x]\) 表示 \(x\) 跳到叶子节点费用的最小值。则显然

\[f[x]=\min\{f[y]+a_x\times b_y\} \]

\(b_y\) 当作斜率,\(f[y]\) 当作截距,则变成李超线段树的模板,求平面内所有直线与直线 \(x=a_x\) 的交点的纵坐标最小值是多少

在合并时,对于表示相同区间的节点 \(p,q\)\(p,q\) 非空),要做的就是把在 \(p\) 这里插入一条 \(tree[q]\) 的直线即可

code
#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=100010,M=100000;
const LL INF=1e16;

struct line
{
	LL k,b;
	int lc,rc;
	bool flag;
	#define k(x)  tree[x].k
	#define b(x)  tree[x].b
	#define lc(x)  tree[x].lc
	#define rc(x)  tree[x].rc
	#define flag(x)  tree[x].flag
}tree[20*N];

int n,a[N],b[N];
int rt[N],tot;
LL f[N];
vector <int> g[N];

LL calc(line a,int x)
{
	return 1LL*a.k*x+a.b;
}

void change(int &p,int l,int r,line k)
{
	if(!p)
		p=++tot;
		
	if(!flag(p))
		k(p)=k.k,b(p)=k.b,flag(p)=1;	
	else if(calc(k,l)<calc(tree[p],l) && calc(k,r)<calc(tree[p],r))
		k(p)=k.k,b(p)=k.b;
	else if(calc(k,l)<calc(tree[p],l) || calc(k,r)<calc(tree[p],r))
	{
		int mid=(l+r)>>1;
		
		if(calc(k,mid)<calc(tree[p],mid))
			swap(k(p),k.k),swap(b(p),k.b);
		if(calc(k,l)<calc(tree[p],l))
			change(lc(p),l,mid,k);
		else
			change(rc(p),mid+1,r,k);
	}
}

int merge(int p,int q,int l,int r)
{
	if(!p)
		return q;
	if(!q)
		return p;
	
	change(p,l,r,tree[q]);
	if(l==r)
		return p;
	
	int mid=(l+r)>>1;
	lc(p)=merge(lc(p),lc(q),l,mid);
	rc(p)=merge(rc(p),rc(q),mid+1,r);
	
	return p;
}

LL ask(int p,int l,int r,int x)
{
	if(!p)
		return INF;
	if(l==r)
	{
		if(flag(p))
			return calc(tree[p],x);
		return INF;
	}
	
	int mid=(l+r)>>1;
	LL val=INF;
	if(flag(p))
		val=calc(tree[p],x);
	if(x<=mid)
		return min(val,ask(lc(p),l,mid,x));
	return min(val,ask(rc(p),mid+1,r,x)); 
}

void dfs(int x,int fa)
{
	for(int i=0; i<g[x].size(); i++)
	{
		int y=g[x][i];
		if(y==fa)
			continue;
		
		dfs(y,x);
		
		rt[x]=merge(rt[x],rt[y],-M,M);
	}
	
	f[x]=ask(rt[x],-M,M,a[x]);
	if(f[x]==INF)
		f[x]=0;
	
	line now={(LL)b[x],f[x],0,0,1};
	change(rt[x],-M,M,now);
}

int main()
{
	scanf("%d",&n);
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]);
	for(int i=1; i<=n; i++)
		scanf("%d",&b[i]);
	for(int i=1; i<n; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		g[x].push_back(y);
		g[y].push_back(x);
	}
	
	dfs(1,0);
	
	for(int i=1; i<=n; i++)
		printf("%lld ",f[i]);
	
	return 0;
}

三、势能线段树(Seg-beats)

有时候,题目中的操作可能让信息量趋于减小,此时可能产生均摊的做法。这时分析复杂度时,用势能来分析是不不错的方法

CF438D The Child and Sequence

题意:区间对一个数取模、单点修改、区间求和

后两个操作是线段树基本操作,主要考虑第一个

我们发现,当区间 \([l,r]\)\(x\) 取模时,若 \([l,r]\) 的最大值小于 \(x\),那我们就不必操作。而每个数取模后至少会减小到原来的一半。所以我们想到可以暴力递归修改

来证明下复杂度为啥是对的。由上一段我们知道,一个数 \(k\) 最多进行 \(\log k\) 次有意义的取模。定义势能函数 \(\phi(x)=\log a_x\) 表示第 \(x\) 最多可以进行多少次有意义的取模,总势能 \(\phi\) 为所有叶子节点势能之和,一开始总势能 \(\leq n\log V\)。每次暴力取模会有 \(\mathcal{O}(\log n)\) 的复杂度,但会使势能减少 \(1\)。询问不会改变势能,一次单点修改操作最多使得势能增加 \(\log V\),所以总的时间复杂度不会超过 \(\mathcal{O}((n+m)\log V\log n)\)

code
#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=1e5+10;

int n,m,a[N];

#define lc(p) p<<1
#define rc(p) p<<1|1
struct  SegmentTree
{
	int dat;  LL sum;
	#define dat(x) tree[x].dat
	#define sum(x) tree[x].sum
}tree[N<<2];

void pushup(int p)
{
	dat(p)=max(dat(lc(p)),dat(rc(p)));
	sum(p)=sum(lc(p))+sum(rc(p));
}

void build(int p,int l,int r)
{
	if(l==r)
	{
		dat(p)=sum(p)=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(lc(p),l,mid);
	build(rc(p),mid+1,r);
	pushup(p);
}

void cmod(int p,int l,int r,int ql,int qr,int x)
{
	if(dat(p)<x)
		return;
	if(l==r)
	{
		dat(p)%=x;  sum(p)%=x;
		return;
	}
	int mid=(l+r)>>1;
	if(ql<=mid)
		cmod(lc(p),l,mid,ql,qr,x);
	if(qr>mid)
		cmod(rc(p),mid+1,r,ql,qr,x);
	pushup(p);
}

void change(int p,int l,int r,int pos,int v)
{
	if(l==r)
	{
		dat(p)=sum(p)=v;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid)
		change(lc(p),l,mid,pos,v);
	else
		change(rc(p),mid+1,r,pos,v);
	pushup(p);
}

LL ask(int p,int l,int r,int ql,int qr)
{
	if(ql<=l && qr>=r)
		return sum(p);
	int mid=(l+r)>>1;
	LL res=0;
	if(ql<=mid)
		res+=ask(lc(p),l,mid,ql,qr);
	if(qr>mid)
		res+=ask(rc(p),mid+1,r,ql,qr);
	return res;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]);

	build(1,1,n);

	while(m--)
	{
		int op,l,r,x,k;
		scanf("%d",&op);
		if(op==1)
		{
			scanf("%d%d",&l,&r);
			printf("%lld\n",ask(1,1,n,l,r));
		}
		else if(op==2)
		{
			scanf("%d%d%d",&l,&r,&x);
			cmod(1,1,n,l,r,x);
		}
		else
		{
			scanf("%d%d",&k,&x);
			change(1,1,n,k,x);
		}
	}
	
	return 0;
}

CF-gym-103107 And RMQ

题意:区间与、单点修改、区间求 \(\max\)

仍然是暴力修改。

我们发现,对区间 \([l,r]\) 进行修改时,若区间每一个数 \(\&\)\(x\) 后都没有变化则无需修改。所以我们维护区间或的值,当它 \(\& \,x\) 没有变化则不操作

由于每次变化必然是某一位的 \(1\rightarrow 0\),所以可设势能函数 \(\phi(x)\) 表示 \(x\) 这个节点区间内的所有数的或和在二进制下 \(1\) 的个数。初始势能为 \(n\log V\),每次与操作都会使总势能最少减少 \(1\),单点修改操作最多使势能增加 \(\log n\log V\),因此总势能不会超过 \((n+m\log n)\log V\),时间复杂度 \(\mathcal{O}((n+m\log n)\log V)\)

Uoj#228.基础数据结构练习题

题意:区间加、区间开根、区间求和

先考虑这个问题的弱化版 P4145 上帝造题的七分钟2/花神游历各国

弱化版省去了区间加操作。我们发现对于一个数 \(x\),它在开根 \(\log \log x\) 次后就会变成 \(1\)。所以定义势能函数 \(\phi(x)=\log \log x\),势能总和为所有叶子节点的势能之和,为 \(n\log \log x\)。每次区间开根操作如果有大于 \(1\) 的数,就暴力递归,至少会使势能减少 \(1\),所以时间复杂度为 \(\mathcal{O}(n\log n\log \log V)\)

现在考虑这道题。我们引入一个典型的势能分析方法:容均摊

即:找出一种能概括信息量的“特征值”,证明其消长和时间消耗有关,最终通过求和得到复杂度。

在本题中,我们将每个节点的容定义为这个节点区间内的数的极差,记为 \(S\)

\(S\ne 0\),一次区间开根操作至少能使 \(S\) 开根

证明 设原来极差由 $x^2-y^2$ 产生,则新的极差为 $x-y$,而 $\sqrt{x^2-y^2}=\sqrt{(x-y)(x+y)}$,所以 $x-y<\sqrt{x^2-y^2}$

设势能函数 \(\phi(x)=\log \log S_x\)\(S_x\) 表示 \(x\) 所代表的区间的极差),每次开根操作都会使势能至少减少 \(1\),区间加会增加 \(\log n\log \log V\) 的势能,所以总的时间复杂度为 \(\mathcal{O}((n+m\log n)\log \log V)\)

然而这样是不严谨的,考虑向下取整带来的误差,如 \(3,4,3,4\) 开根号后变成 \(1,2,1,2\),极差并没有变化,意味着势能也没有减少,如果这时候区间加 \(2\) 的话又变成了 \(3,4,3,4\)。那我们每次区间开根都得暴力,复杂度退化

仔细思考可以发现向下取整带来的误差最多是 \(1\),所以产生这种情况的话极差只能是 \(1\),这时候两种数的变化量是相同的,我们可以直接转化成区间减法操作

code

CF1290E Cartesian Tree

笛卡尔树建树的过程可以看做是每次选取最大值然后进行分治。设 \(pre_i,nxt_i\) 表示 \(i\) 左边/右边第一个比它大的位置,则容易证明区间 \((pre_i,nxt_i)\) 都是 \(i\) 的子树,\(i\) 的子树大小即为 \(nxt_i-pre_i-1\)。令 \(k\) 从小到大增加,维护 \(pre_i,nxt_i\) 即可求出答案,下面以求解 \(nxt_i\) 为例(求解 \(pre_i\) 只要把原序列翻转即可)

每次加入一个数,都会令其左侧的 \(nxt\)\(\min\),由于我们是不断“插入”新的数,空的位置不会计入下标,所以空的位置要记成 \(0\),右侧的所有 \(nxt\) 都要 \(+1\)

于是我们转化成了下面的问题:

写一棵线段树,支持区间加、区间取 \(\min\)、区间求和

我们维护区间最大值 \(mx\),最大值个数 \(cmx\),区间严格次大值 \(mx_2\),区间和 \(sum\)

对于一个修改 \(a_i=\min(a_i,v)\) 来说

  • \(v\geq mx\),显然无影响

  • \(mx>y>mx_2\),则只会对最大值产生影响,利用 \(cmx\) 计算贡献并打下标记

  • \(y\leq mx_2\),则此次操作会影响到最大值以外的数,我们无法在当前节点处理,只能向深处 dfs,直到能处理为止

分析一下时间复杂度:

设节点的容为所表示区间的数的种类数,所有点的容的总和为 \(n\log n\)

如果在 dfs 的过程中经过了该节点,则会将最大值与次大值合并使容至少减少 \(1\),所以 dfs 的复杂度就是 \(\mathcal{O}(n\log n)\)

总的时间复杂度应该是 \(\mathcal{O}((n+m)\log n)\)

下面考虑加入区间加操作

仍然维护上述四个变量 \(mx,cmx,mx_2,sum\)

把标记改进成 \((ad_1,v)\) 的形式,意义为先加上 \(ad_1\) 再对 \(v\) 取最小值

\(\min\) 时仍按照上述方法 dfs

根据论文中的证明,复杂度有上界 \(\mathcal{O}(n\log^2 n)\),实际近似于 \(\mathcal{O}(n \log n)\)

总结:我们通过暴力 dfs,将区间取 \(\min\) 转化为区间加法操作。在实现时,我们可以维护两套标记,一套对最大值生效,另一套对所有数(或其它数)生效

code

四、历史值问题

在维护序列 \(A\) 的同时维护序列 \(B\)\(B\) 一开始等于 \(A\)

  • 历史最大值:每次操作后 \(B_i\leftarrow \max(B_i,A_i)\)
  • 历史版本和:每次操作后 \(B_i\leftarrow B_i+A_i\)

1、历史最大值

基础操作:区间加、查询区间最大值、查询区间历史最大值

我们用标记来解决这类历史值问题。

在非历史值问题中,我们关注的是节点当下的标记是什么,所以我们直接合并标记。但在历史值问题中,我们还要考虑历史上推上来的标记的依次作用

先不合并标记,假设每个节点有一个队列存放着历史推上来的所有的标记。递归时将所有标记下推到儿子处,并清空队列

对于每个节点记录 \(dat,hdat\) 分别表示区间最大值、区间历史最大值。每次有一个区间加标记 \(ad\) 进来时,\(dat\leftarrow dat+ad\),然后 \(hdat\leftarrow \max(hdat,dat)\)

这样是正确的,但是我们根本无法存下所有的标记,所以我们要考虑如何概括一个队列所有的标记对当前节点的影响。

\(t[1\dots k]\) 表示推进来的加法标记,\(s[i]\)\(t[i]\) 的前缀和。则打上第 \(i\) 个标记后,\(dat\) 的值为 \(dat+s[i]\)\(hdat\) 的值为 \(\max\{s[i]+dat\}=dat+\max\{s[i]\}\)。于是只需记录 \(\max\{s[i]\}\) 就可以知道这个队列标记的影响。记 \(ad\) 为加法标记,合并时直接求和,所以 \(ad\) 刚好等于 \(s[i]\)。记 \(had\) 为加法标记的历史最大值,则 \(had=\max\{s[i]\}\)

现在考虑两个标记队列 \(t_1[1\dots p_1],t_2[1\dots p_2]\) 如何合并,设合并的结果为 \(t_3[1\dots p_1+p_2]\)

注意到 \(s_3[i]=\begin{cases}s_i[i]&1\leq i\leq p_1 \\ s_1[p_1]+s_2[i-p_1]&p_1<i\leq p_1+p_2 \end{cases}\)

我们需快速求出 \(\max\{s_3[i]\}=\max(\max\{s_1[j]\},s_1[p_1]+\max\{s_2[k]\})\),只需维护 \(s_1[p_1]\),这正是目前加法标记的值

具体地,每次 \(u\rightarrow v\) 下推时,令

hdat(v)=max(hdat(v),dat(v)+had(u));
had(v)=max(had(v),ad(v)+had(u));
dat(v)+=ad(u);
ad(v)+=ad(u);
ad(u)=had(u)=0;

P4314 CPU 监控

题意:区间加、区间覆盖、查询区间最大值、查询区间历史最大值

就是基础操作加上了赋值操作

对于线段树历史值问题,我们需要完整地考虑每个标记的历史影响

现在标记队列里有两种标记,加法标记和赋值标记,标记混杂不好处理

考虑赋值操作的影响,区间都变成一个数,那这之后的加法操作其实也可以等价成为赋值操作。那么标记队列就变成一个加法标记队列后面跟着一个赋值队列,加法标记用前文的方法处理

对于赋值操作 \(c[1\dots p]\),产生的历史最大值为 \(\max{c[i]}\),记录这个即可。

code
#include<bits/stdc++.h>
#define lc(p) p*2
#define rc(p) p*2+1
#define pii pair<int,int>
using namespace std;

const int N=100010,INF=(1<<31);

int n,m,a[N];

struct SegmentTree
{
	int dat,hdat,ad,had,fu,hfu;
	#define dat(x) tree[x].dat
	#define hdat(x) tree[x].hdat
	#define ad(x) tree[x].ad
	#define had(x) tree[x].had
	#define fu(x) tree[x].fu
	#define hfu(x) tree[x].hfu
	
	void add(int v,int mv)
	{
		hdat=max(hdat,dat+mv);  dat+=v;	 
		if(hfu!=-INF)
			hfu=max(hfu,fu+mv),fu+=v;
		else
			had=max(had,ad+mv),ad+=v;
	}
	
	void cov(int v,int mv)
	{
		hdat=max(hdat,mv);
		hfu=max(hfu,mv);
		fu=dat=v;
	}
}tree[4*N];

void pushup(int p)
{
	dat(p)=max(dat(lc(p)),dat(rc(p)));
	hdat(p)=max(hdat(lc(p)),hdat(rc(p)));
}

void spread(int p)
{
	if(ad(p) || had(p))  //*
	{
		tree[lc(p)].add(ad(p),had(p));
		tree[rc(p)].add(ad(p),had(p));
		ad(p)=had(p)=0;
	} 
	if(hfu(p)!=-INF)
	{
		tree[lc(p)].cov(fu(p),hfu(p));
		tree[rc(p)].cov(fu(p),hfu(p));
		fu(p)=hfu(p)=-INF;
	}
}

void build(int p,int l,int r)
{
	fu(p)=hfu(p)=-INF;
	if(l==r)
	{
		dat(p)=hdat(p)=a[l];
		return;
	}
	
	int mid=(l+r)>>1;
	build(lc(p),l,mid);
	build(rc(p),mid+1,r);
	pushup(p);
}

void add(int p,int l,int r,int ql,int qr,int v)
{
	if(ql<=l && qr>=r)
	{
		tree[p].add(v,max(v,0));
		return;
	}
	
	spread(p);
	
	int mid=(l+r)>>1;
	if(ql<=mid)
		add(lc(p),l,mid,ql,qr,v);
	if(qr>mid)
		add(rc(p),mid+1,r,ql,qr,v);
	pushup(p);
}

void cov(int p,int l,int r,int ql,int qr,int v)
{
	if(ql<=l && qr>=r)
	{
		tree[p].cov(v,v);
		return;
	}
	
	spread(p);
	
	int mid=(l+r)>>1;
	if(ql<=mid)
		cov(lc(p),l,mid,ql,qr,v);
	if(qr>mid)
		cov(rc(p),mid+1,r,ql,qr,v);
	pushup(p);
}

pii ask(int p,int l,int r,int ql,int qr)
{
	if(ql<=l && qr>=r)
		return {dat(p),hdat(p)};
	
	spread(p);
	
	int mid=(l+r)>>1;
	pii lans={-INF,-INF},rans={-INF,-INF};
	if(ql<=mid)
		lans=ask(lc(p),l,mid,ql,qr);
	if(qr>mid)
		rans=ask(rc(p),mid+1,r,ql,qr);
	return {max(lans.first,rans.first),max(lans.second,rans.second)};
}

int main()
{
	scanf("%d",&n);
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]);
	
	build(1,1,n);
	
	scanf("%d",&m);
	for(int i=1; i<=m; i++)
	{
		char op[2];  int l,r,v;
		scanf("%s%d%d",op,&l,&r);
		if(op[0]=='Q')
			printf("%d\n",ask(1,1,n,l,r).first);
		else if(op[0]=='A')
			printf("%d\n",ask(1,1,n,l,r).second);
		else if(op[0]=='P')
			scanf("%d",&v),add(1,1,n,l,r,v);
		else
			scanf("%d",&v),cov(1,1,n,l,r,v);
	}

	return 0;
}


P6242【模板】线段树 3

题意:区间加、区间求和、区间取 \(\rm min\),区间求最大值、区间求历史最大值

吉司机线段树!!!!!

维护两套标记,一套对最大值生效,另一套对其它数生效(历史最大值的标记合并讲究顺序,所以标记影响的对象不交才好维护)

一些注意点:

  • 最大值标记下推时,要判断儿子是否含有相同的最大值。最大值的比较应在儿子中进行

  • 下推标记时,若儿子的最大值不为区间最大值,要给儿子的最大值打上非最大值的加法标记

code
#include<bits/stdc++.h>
#define LL long long
using namespace std;

const int N=5e5+10;
const LL INF=1e9;

int n,m,a[N];
struct Ask{LL s,mx1,mx2;};

#define lc(p) p<<1
#define rc(p) p<<1|1
struct SegmentTree
{
	int mx,cmx,mx2,hmx,ad1,had1,ad2,had2,len;
	LL sum;
	#define mx(x) tree[x].mx
	#define cmx(x) tree[x].cmx
	#define mx2(x) tree[x].mx2
	#define hmx(x) tree[x].hmx
	#define ad1(x) tree[x].ad1
	#define had1(x) tree[x].had1
	#define ad2(x) tree[x].ad2
	#define had2(x) tree[x].had2
	#define sum(x) tree[x].sum
	#define len(x) tree[x].len

	void add(int v1,int mv1,int v2,int mv2)
	{
		sum+=1LL*(len-cmx)*v1+1LL*cmx*v2;
		hmx=max(hmx,mx+mv2);
		had1=max(had1,ad1+mv1);  ad1+=v1;
		had2=max(had2,ad2+mv2);  ad2+=v2;
		mx+=v2;  mx2+=v1;
	}
}tree[N<<2];

void pushup(int p)
{
	sum(p)=sum(lc(p))+sum(rc(p));
	hmx(p)=max(hmx(lc(p)),hmx(rc(p)));
	if(mx(lc(p))==mx(rc(p)))
		mx(p)=mx(lc(p)),cmx(p)=cmx(lc(p))+cmx(rc(p)),mx2(p)=max(mx2(lc(p)),mx2(rc(p)));
	else
	{
		int m1=max(mx(lc(p)),mx(rc(p))),m2=min(mx(lc(p)),mx(rc(p))),m3=max(mx2(lc(p)),mx2(rc(p)));
		mx(p)=m1;  cmx(p)=(m1==mx(lc(p))? cmx(lc(p)):cmx(rc(p)));
		mx2(p)=max(m2,m3);
	}
}

void spread(int p)
{
	if(ad1(p) || had1(p) || ad2(p) || had2(p))
	{
		int mm=max(mx(lc(p)),mx(rc(p)));
		if(mx(lc(p))==mm)
			tree[lc(p)].add(ad1(p),had1(p),ad2(p),had2(p));
		else
			tree[lc(p)].add(ad1(p),had1(p),ad1(p),had1(p));
		if(mx(rc(p))==mm)
			tree[rc(p)].add(ad1(p),had1(p),ad2(p),had2(p));
		else
			tree[rc(p)].add(ad1(p),had1(p),ad1(p),had1(p));
		ad1(p)=had1(p)=ad2(p)=had2(p)=0;
	}
}

void build(int p,int l,int r)
{
	len(p)=r-l+1;
	if(l==r)
	{
		cin>>a[l];
		mx(p)=hmx(p)=sum(p)=a[l];
		cmx(p)=1;  mx2(p)=-INF;
		return;
	}
	int mid=(l+r)>>1;
	build(lc(p),l,mid);
	build(rc(p),mid+1,r);
	pushup(p);
}

void add(int p,int l,int r,int ql,int qr,int v)
{
	if(ql<=l && qr>=r)
	{
		tree[p].add(v,max(v,0),v,max(v,0));
		return;
	}
	spread(p);
	int mid=(l+r)>>1;
	if(ql<=mid)
		add(lc(p),l,mid,ql,qr,v);
	if(qr>mid)
		add(rc(p),mid+1,r,ql,qr,v);
	pushup(p);
}

void change(int p,int l,int r,int ql,int qr,int v)
{
	if(v>=mx(p))
		return;
	if(ql<=l && qr>=r && v>mx2(p))
	{
		tree[p].add(0,0,v-mx(p),v-mx(p));
		return;
	}
	spread(p);
	int mid=(l+r)>>1;
	if(ql<=mid)
		change(lc(p),l,mid,ql,qr,v);
	if(qr>mid)
		change(rc(p),mid+1,r,ql,qr,v);
	pushup(p);
}

Ask ask(int p,int l,int r,int ql,int qr)
{
	if(ql<=l && qr>=r)
		return {sum(p),mx(p),hmx(p)};
	spread(p);
	int mid=(l+r)>>1;
	Ask lval={0,-INF,-INF},rval={0,-INF,-INF};
	if(ql<=mid)
		lval=ask(lc(p),l,mid,ql,qr);
	if(qr>mid)
		rval=ask(rc(p),mid+1,r,ql,qr);
	return {lval.s+rval.s,max(lval.mx1,rval.mx1),max(lval.mx2,rval.mx2)};
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);  cout.tie(0);

	cin>>n>>m;

	build(1,1,n);

	while(m--)
	{
		int op,l,r,x,k,v;
		cin>>op>>l>>r;
		if(op==1)
			cin>>k,add(1,1,n,l,r,k);
		else if(op==2)
			cin>>v,change(1,1,n,l,r,v);
		else if(op==3)
			cout<<ask(1,1,n,l,r).s<<"\n";
		else if(op==4)
			cout<<ask(1,1,n,l,r).mx1<<"\n";
		else
			cout<<ask(1,1,n,l,r).mx2<<"\n";
	}
	
	return 0;
}

2、历史版本和

记原序列为 \(A\),版本和序列为 \(B\)。把更新 \(B\) 序列也看成一种标记,每次操作后给整棵树打一个

考虑一个加法标记和更新标记相互出现的队列 \(t[1\dots p]=\{v_1,v_2,upd,v_4\dots\}\),设当前节点区间和为 \(sum\),区间历史和为 \(hsum\),区间长度为 \(len\)

加法标记 \(ad\) 会使 \(sum\leftarrow sum+ad\),更新标记会使得 \(hsum\leftarrow hsum+sum\)

\(s[i]\) 表示前 \(i\) 个加法标记的和,则对 \(hsum\) 的总贡献为 \(\sum\limits_{i=1}^p[t[i]=upd]\left(sum+s[i]\times len\right)=sum\times\left(\sum\limits_{i=1}^p{\left[t[i]=upd\right]}\right)+len\times\left(\sum\limits_{i=1}^p{[t[i]=upd]s[i]}\right)\)

所以只需记录 \(\sum\limits_{i=1}^p{\left[t[i]=upd\right]}\)\(\sum\limits_{i=1}^p{[t[i]=upd]s[i]}\) 即可。分别表示更新标记的总个数,记为 \(upd\)。以及更新标记打上时 \(s[i]\) 的和,即加法标记的历史版本和,记为 \(had\)

下面考虑两个标记队列 \(t_1[1\dots p_1],t_2[1\dots p_2]\) 如何合并,设合并的结果为 \(t_3[1\dots p_1+p_2]\)

\[\begin{aligned}&\sum_{i=1}^{p_3}{[t_3[i]=upd]s_3[i]}\\=&\sum_{i=1}^{p_1}{[t_1[i]=upd]s_1[i]}+\sum_{i=1}^{p_2}{[t_2[i]=upd]\left(s_1[p_1]+s_2[i]\right)}\\=&s_1[p_1]\left(\sum_{i=1}^{p_1}[t_2[i]=upd]\right)+\sum_{i=1}^{p_1}{[t_1[i]=upd]s_1[i]}+\sum_{i=1}^{p_2}{[t_2[i]=upd]s_2[i]}\end{aligned} \]

于是再记录 \(s_1[p_1]\) 表示合并后的加法标记就可以实现标记队列的合并,记为 \(ad\)

具体地,每次 \(u\rightarrow v\) 下推时,令

hsum(v)+=sum(v)*upd(u)+len*had(p);
had(v)+=ad(v)*upd(u)+had(u);
sum(v)+=len*ad(u);
ad(v)+=ad(u);
upd(v)+=upd(u);

P3246 [HNOI2016] 序列

题意:给定一个区间,求所有子区间的最小值之和

\(f[l][r]\) 表示区间 \([l,r]\) 的最小值。往右侧加入一个元素时,可以用单调栈维护最小值,再用线段树做区间修改,就可以维护出 \(f\) 数组

将右端点理解成版本,对于一个询问 \([l,r]\),答案就是线段树上 \([l,r]\) 的历史版本和

code
#include<bits/stdc++.h>
#define lc(p) p*2
#define rc(p) p*2+1
#define LL long long
using namespace std;

const int N=100010;

int n,q,a[N],sta[N],top;
LL ans[N];
struct node{int l,id;};
vector <node> qq[N];

struct SegmentTree
{
	LL ad,had,sum,hsum,upd;
	int len;
	#define ad(x) tree[x].ad
	#define had(x) tree[x].had
	#define sum(x) tree[x].sum
	#define hsum(x) tree[x].hsum
	#define upd(x) tree[x].upd
	#define len(x) tree[x].len
	
	void add(LL v,LL sv,LL uv)
	{
		hsum+=sum*uv+len*sv;
		had+=ad*uv+sv;
		upd+=uv;
		sum+=v*len;
		ad+=v;
	} 
}tree[4*N];

void pushup(int p)
{
	sum(p)=sum(lc(p))+sum(rc(p));
	hsum(p)=hsum(lc(p))+hsum(rc(p));
}

void spread(int p)
{
	if(ad(p) || had(p) || upd(p))
	{
		tree[lc(p)].add(ad(p),had(p),upd(p));
		tree[rc(p)].add(ad(p),had(p),upd(p));
		ad(p)=had(p)=upd(p)=0;
	}
}

void build(int p,int l,int r)
{
	len(p)=r-l+1;
	if(l==r)
		return;
	int mid=(l+r)>>1;
	build(lc(p),l,mid);
	build(rc(p),mid+1,r);
}

void add(int p,int l,int r,int ql,int qr,LL v)
{
	if(ql<=l && qr>=r)
	{
		tree[p].add(v,0,0);
		return;
	}
	
	spread(p);
	
	int mid=(l+r)>>1;
	if(ql<=mid)
		add(lc(p),l,mid,ql,qr,v);
	if(qr>mid)
		add(rc(p),mid+1,r,ql,qr,v);
	pushup(p);
}

LL ask(int p,int l,int r,int ql,int qr)
{
	if(ql<=l && qr>=r)
		return hsum(p);
	
	spread(p);
	
	int mid=(l+r)>>1;  LL res=0;
	if(ql<=mid)
		res+=ask(lc(p),l,mid,ql,qr);
	if(qr>mid)
		res+=ask(rc(p),mid+1,r,ql,qr);
	return res;
}

int main()
{
	scanf("%d%d",&n,&q);
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]);
	for(int i=1; i<=q; i++)
	{
		int l,r;
		scanf("%d%d",&l,&r);
		qq[r].push_back({l,i});
	}
	
	build(1,1,n);
	
	for(int i=1; i<=n; i++)
	{
		while(top && a[sta[top]]>a[i])
		{
			add(1,1,n,sta[top-1]+1,sta[top],1LL*(a[i]-a[sta[top]]));
			top--;
		}
		sta[++top]=i;  add(1,1,n,i,i,a[i]);
		tree[1].add(0,0,1);
		
		for(auto x:qq[i])
			ans[x.id]=ask(1,1,n,x.l,i); 
	}
	
	for(int i=1; i<=q; i++)
		printf("%lld\n",ans[i]);
	
	return 0;
}

CF997E Good Subsegments

先咕着

posted @ 2023-08-28 21:52  xishanmeigao  阅读(76)  评论(0)    收藏  举报