线段树进阶操作
一、李超线段树
1、标记永久化
一言以蔽之:标记永久化就是不再下放标记,而是让标记永久地停留在线段树的节点上,统计答案时再考虑这些标记的影响
2、维护直线
我们以下面这道题为例子来进行讲解
[JSOI2008] Blue Mary开公司
题意:要求支持操作
Project
:插入一条的直线,给定 Query
:求所有直线中与直线的交点的纵坐标最大值是多少
我们首先建立一棵线段树,每个节点代表一个区间,且有一个标记记录一条直线
对于插入直线的操作:
-
如果当前区间还没有标记,我们将这个区间标记为当前直线
-
如果有标记,但插入的直线完全覆盖原先的直线,即替换掉原先的直线
-
如果插入的直线被原先的直线完全覆盖,即返回
-
剩下的情况就是插入的和原先的直线在区间内有交点。那么我们令
,令与直线 交点纵坐标更大的直线作为当前区间被标记的直线。然后递归交点所在的区间子树,继续修改即可
对于查询操作,类似标记永久化,找到所有覆盖了
时间复杂度:
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
首先
设
那么状态转移方程也是显然的:
现在我们令
考虑优化,将式子化简得:
我们令
二、线段树合并
1、知识点
前置知识:动态开点线段树
线段树合并是一个递归的过程。我们合并两棵线段树时,用两个指针
-
若
之一为空,则以非空的那个作为合并的节点 -
若
均不为空,则递归合并两棵左子树和两棵右子树,然后删除节点 ,以 为合并后的新节点,然后删除节点 ,以 作为合并后的节点,更新信息
参考代码
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;
}
时间复杂度 & 空间复杂度:
2、一些习题
【模板】线段树合并 / [Vani有约会] 雨天的尾巴
根据套路,容易想到先找出
现在考虑优化,我们可以对每一个点建立一棵权值线段树来替代差分数组,维护存放最多的救济粮的类型和数量,之后从根节点开始进行一次
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
对每个节点建立一棵权值线段树,维护占主导地位的颜色的出现次数和编号和,再进行一次
[HNOI2012] 永无乡
考虑用并查集去维护每座岛之间的连通性,并用连通块的祖先去代表整个连通块
对每座岛建立一棵权值线段树,每次查询操作在线段树上二分即可
对于建桥操作,合并两个连通块即可
[POI2011] ROT-Tree Rotations
对于节点
-
在同一个
子树 -
在不同的
子树
对于节点
考虑对每个节点建立权值线段树,设值域区间内数字的个数为
现在考虑交换子树的操作。显然交换子树的操作只会对来源2产生影响。那我们取
CF208E Blood Cousins
直接找
对于节点
具体实现时,在树的遍历时,可以开两个数组对询问进行处理。求
一个小细节,该题的图不一定只有一课树,可能是多棵树,需要注意。
[Cnoi2019] 雪松果树
(上一题 Blood Cousins
的卡空间版)
大部分代码和上一题相同,这里主要讲如何优化空间
首先合并时,将子树按
其次,合并完后,将无用的那个子树的空间回收起来,以后在动态开点时,优先从回收站里拿空间
最后,把 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;
}
[湖南集训] 更为厉害
因为
如果
如果
CF570D Tree Requests
在每个节点同样以深度为下标建立线段树,存储该子树内深度相同的点所构成的字符集合
因为题目要求的是能否构成回文串。显然只要所有的字符都出现偶数次或只有一个字符出现次数为 check
一下这个二进制数是否合法即可
CF246E Blood Cousins Return
容易想到用 map
给每个名字编号
对每个节点同样以深度为下标建立权值线段树,因为要去重计数,所以对线段树的每个叶子节点开一个 set
,查询时返回 set
的大小即可
合并时,同 set
合并到大的上
CF932F Escape Through Leaf
(李超线段树的合并)
考虑树形
把
在合并时,对于表示相同区间的节点
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
题意:区间对一个数取模、单点修改、区间求和
后两个操作是线段树基本操作,主要考虑第一个
我们发现,当区间
来证明下复杂度为啥是对的。由上一段我们知道,一个数
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
题意:区间与、单点修改、区间求
仍然是暴力修改。
我们发现,对区间
由于每次变化必然是某一位的
Uoj#228.基础数据结构练习题
题意:区间加、区间开根、区间求和
先考虑这个问题的弱化版 P4145 上帝造题的七分钟2/花神游历各国
弱化版省去了区间加操作。我们发现对于一个数
现在考虑这道题。我们引入一个典型的势能分析方法:容均摊
即:找出一种能概括信息量的“特征值”,证明其消长和时间消耗有关,最终通过求和得到复杂度。
在本题中,我们将每个节点的容定义为这个节点区间内的数的极差,记为
若
证明
设原来极差由设势能函数
然而这样是不严谨的,考虑向下取整带来的误差,如
仔细思考可以发现向下取整带来的误差最多是
CF1290E Cartesian Tree
笛卡尔树建树的过程可以看做是每次选取最大值然后进行分治。设
每次加入一个数,都会令其左侧的
于是我们转化成了下面的问题:
写一棵线段树,支持区间加、区间取
、区间求和
我们维护区间最大值
对于一个修改
-
若
,显然无影响 -
若
,则只会对最大值产生影响,利用 计算贡献并打下标记 -
若
,则此次操作会影响到最大值以外的数,我们无法在当前节点处理,只能向深处 dfs,直到能处理为止
分析一下时间复杂度:
设节点的容为所表示区间的数的种类数,所有点的容的总和为
如果在 dfs 的过程中经过了该节点,则会将最大值与次大值合并使容至少减少
总的时间复杂度应该是
下面考虑加入区间加操作
仍然维护上述四个变量
把标记改进成
取
根据论文中的证明,复杂度有上界
总结:我们通过暴力 dfs,将区间取
四、历史值问题
在维护序列
- 历史最大值:每次操作后
- 历史版本和:每次操作后
1、历史最大值
基础操作:区间加、查询区间最大值、查询区间历史最大值
我们用标记来解决这类历史值问题。
在非历史值问题中,我们关注的是节点当下的标记是什么,所以我们直接合并标记。但在历史值问题中,我们还要考虑历史上推上来的标记的依次作用
先不合并标记,假设每个节点有一个队列存放着历史推上来的所有的标记。递归时将所有标记下推到儿子处,并清空队列
对于每个节点记录
这样是正确的,但是我们根本无法存下所有的标记,所以我们要考虑如何概括一个队列所有的标记对当前节点的影响。
设
现在考虑两个标记队列
注意到
我们需快速求出
具体地,每次
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 监控
题意:区间加、区间覆盖、查询区间最大值、查询区间历史最大值
就是基础操作加上了赋值操作
对于线段树历史值问题,我们需要完整地考虑每个标记的历史影响
现在标记队列里有两种标记,加法标记和赋值标记,标记混杂不好处理
考虑赋值操作的影响,区间都变成一个数,那这之后的加法操作其实也可以等价成为赋值操作。那么标记队列就变成一个加法标记队列后面跟着一个赋值队列,加法标记用前文的方法处理
对于赋值操作
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
题意:区间加、区间求和、区间取
,区间求最大值、区间求历史最大值
吉司机线段树!!!!!
维护两套标记,一套对最大值生效,另一套对其它数生效(历史最大值的标记合并讲究顺序,所以标记影响的对象不交才好维护)
一些注意点:
-
最大值标记下推时,要判断儿子是否含有相同的最大值。最大值的比较应在儿子中进行
-
下推标记时,若儿子的最大值不为区间最大值,要给儿子的最大值打上非最大值的加法标记
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、历史版本和
记原序列为
考虑一个加法标记和更新标记相互出现的队列
加法标记
设
所以只需记录
下面考虑两个标记队列
于是再记录
具体地,每次
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] 序列
题意:给定一个区间,求所有子区间的最小值之和
设
将右端点理解成版本,对于一个询问
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
先咕着
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?