线段树进阶操作

一、李超线段树

1、标记永久化

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

2、维护直线

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

[JSOI2008] Blue Mary开公司

题意:要求支持操作

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

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

对于插入直线的操作:

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

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

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

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

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

时间复杂度:O(nlogn)

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(n2) 的 dp 很好想
fi 表示连接第 1 根和第 i 根柱子的最小代价,答案即为 fn
那么状态转移方程也是显然的:
fi=min{fj+(hihj)2+k=j+1i1wk}

现在我们令 wi+=wi1,即做一次前缀和,则有
fi=min{fj+(hihj)2+wi1wj}
考虑优化,将式子化简得:
fi=hi2+wi1+min{fj2hihj+hj2wj}
我们令 k=2hj,b=fj+hj2wj,则后面的式子转化为一条直线 y=khi+b,问题转化为求所有直线与 x=hi 的交点的纵坐标的最小值,并插入当前自己所代表的直线

二、线段树合并

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(nlogn)

2、一些习题

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

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

现在考虑优化,我们可以对每一个点建立一棵权值线段树来替代差分数组,维护存放最多的救济粮的类型和数量,之后从根节点开始进行一次 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

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

[HNOI2012] 永无乡

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

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

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

[POI2011] ROT-Tree Rotations

对于节点 x,设它的儿子子树分别为 y1,y2,并且 y1y2 左边。分析逆序对的来源:

  • 在同一个 y 子树

  • 在不同的 y 子树

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

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

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

CF208E Blood Cousins

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

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

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

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

[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 在同一条链上

如果 ba 的上方,显然答案就是 (size[a]1)×min(dep[a]1,k)

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

CF570D Tree Requests

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

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

CF246E Blood Cousins Return

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

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

合并时,同 雪松果树,将小的 set 合并到大的上

CF932F Escape Through Leaf

(李超线段树的合并)

考虑树形 dp,设 x 的儿子为 yf[x] 表示 x 跳到叶子节点费用的最小值。则显然

f[x]=min{f[y]+ax×by}

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

在合并时,对于表示相同区间的节点 p,qp,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 最多进行 logk 次有意义的取模。定义势能函数 ϕ(x)=logax 表示第 x 最多可以进行多少次有意义的取模,总势能 ϕ 为所有叶子节点势能之和,一开始总势能 nlogV。每次暴力取模会有 O(logn) 的复杂度,但会使势能减少 1。询问不会改变势能,一次单点修改操作最多使得势能增加 logV,所以总的时间复杂度不会超过 O((n+m)logVlogn)

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 没有变化则不操作

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

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

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

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

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

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

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

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

S0,一次区间开根操作至少能使 S 开根

证明 设原来极差由 x2y2 产生,则新的极差为 xy,而 x2y2=(xy)(x+y),所以 xy<x2y2

设势能函数 ϕ(x)=loglogSxSx 表示 x 所代表的区间的极差),每次开根操作都会使势能至少减少 1,区间加会增加 lognloglogV 的势能,所以总的时间复杂度为 O((n+mlogn)loglogV)

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

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

code

CF1290E Cartesian Tree

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

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

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

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

我们维护区间最大值 mx,最大值个数 cmx,区间严格次大值 mx2,区间和 sum

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

  • vmx,显然无影响

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

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

分析一下时间复杂度:

设节点的容为所表示区间的数的种类数,所有点的容的总和为 nlogn

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

总的时间复杂度应该是 O((n+m)logn)

下面考虑加入区间加操作

仍然维护上述四个变量 mx,cmx,mx2,sum

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

min 时仍按照上述方法 dfs

根据论文中的证明,复杂度有上界 O(nlog2n),实际近似于 O(nlogn)

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

code

四、历史值问题

在维护序列 A 的同时维护序列 BB 一开始等于 A

  • 历史最大值:每次操作后 Bimax(Bi,Ai)
  • 历史版本和:每次操作后 BiBi+Ai

1、历史最大值

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

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

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

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

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

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

t[1k] 表示推进来的加法标记,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]}

现在考虑两个标记队列 t1[1p1],t2[1p2] 如何合并,设合并的结果为 t3[1p1+p2]

注意到 s3[i]={si[i]1ip1s1[p1]+s2[ip1]p1<ip1+p2

我们需快速求出 max{s3[i]}=max(max{s1[j]},s1[p1]+max{s2[k]}),只需维护 s1[p1],这正是目前加法标记的值

具体地,每次 uv 下推时,令

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[1p],产生的历史最大值为 maxc[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

题意:区间加、区间求和、区间取 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[1p]={v1,v2,upd,v4},设当前节点区间和为 sum,区间历史和为 hsum,区间长度为 len

加法标记 ad 会使 sumsum+ad,更新标记会使得 hsumhsum+sum

s[i] 表示前 i 个加法标记的和,则对 hsum 的总贡献为 i=1p[t[i]=upd](sum+s[i]×len)=sum×(i=1p[t[i]=upd])+len×(i=1p[t[i]=upd]s[i])

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

下面考虑两个标记队列 t1[1p1],t2[1p2] 如何合并,设合并的结果为 t3[1p1+p2]

i=1p3[t3[i]=upd]s3[i]=i=1p1[t1[i]=upd]s1[i]+i=1p2[t2[i]=upd](s1[p1]+s2[i])=s1[p1](i=1p1[t2[i]=upd])+i=1p1[t1[i]=upd]s1[i]+i=1p2[t2[i]=upd]s2[i]

于是再记录 s1[p1] 表示合并后的加法标记就可以实现标记队列的合并,记为 ad

具体地,每次 uv 下推时,令

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 @   xishanmeigao  阅读(46)  评论(0编辑  收藏  举报
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示