线段树进阶
普通线段树
核心在于向上更新(pushup)和下传标记(pushdown)以及懒标记的设计。
维护一个加法标记和乘法标记。
下传标记时,将乘法标记更新加法标记。
点击查看代码
void pushdown(int u,int l,int r){
int mid=l+r>>1;
tr[u<<1].val=(tr[u<<1].val*tr[u].mul+tr[u].add*(mid-l+1))%P;
tr[u<<1|1].val=(tr[u<<1|1].val*tr[u].mul+tr[u].add*(r-mid))%P;
tr[u<<1].mul=(tr[u<<1].mul*tr[u].mul)%P;
tr[u<<1|1].mul=(tr[u<<1|1].mul*tr[u].mul)%P;
tr[u<<1].add=(tr[u<<1].add*tr[u].mul+tr[u].add)%P;
tr[u<<1|1].add=(tr[u<<1|1].add*tr[u].mul+tr[u].add)%P;
tr[u].add=0;tr[u].mul=1;
}
维护懒标记 \(f(i)\) 和 \(g(i)\) 表示该区间的背包,在容量不超过 \(i\) 时,最大的价值、物品个数。
乘法操作时,我们直接将 \(f(i)\)(\(0\le i\le m\)) 全变成 \(f(i/w)\),\(g(i)\) 全变成 \(g(i/w)\)。区间覆盖操作,我们就将 \(f(i)\) 变成 \(g(i)\times v\)。
下传懒标记的操作与这些相似。
注意乘法标记要每次更新后与 \(m+1\) 取较小值,因为 \(m+1\) 等价于比 \(m\) 大的所有数。
向上更新标记,即将 \(i\) 拆分成 \(i-j\) 与 \(j\) 来更新。
点击查看代码
void pushup(int u){
for(int i=0;i<=m;i++){
tr[u].f[i]=0;tr[u].g[i]=0;
for(int j=0;j<=i;j++){
tr[u].f[i]=max(tr[u].f[i],tr[u<<1].f[j]+tr[u<<1|1].f[i-j]);
tr[u].g[i]=max(tr[u].g[i],tr[u<<1].g[j]+tr[u<<1|1].g[i-j]);
}
}
}
void pushdown(int u){
if(tr[u].lazy1>1){
for(int i=m;i>=0;i--)
tr[u<<1].f[i]=tr[u<<1].f[i/tr[u].lazy1],tr[u<<1|1].f[i]=tr[u<<1|1].f[i/tr[u].lazy1],
tr[u<<1].g[i]=tr[u<<1].g[i/tr[u].lazy1],tr[u<<1|1].g[i]=tr[u<<1|1].g[i/tr[u].lazy1];
tr[u<<1].lazy1=min(tr[u<<1].lazy1*tr[u].lazy1,(ll)m+1);
tr[u<<1|1].lazy1=min(tr[u<<1|1].lazy1*tr[u].lazy1,(ll)m+1);
}
if(tr[u].lazy2){
for(int i=0;i<=m;i++)
tr[u<<1].f[i]=(ll)tr[u<<1].g[i]*tr[u].lazy2,tr[u<<1|1].f[i]=(ll)tr[u<<1|1].g[i]*tr[u].lazy2;
tr[u<<1].lazy2=tr[u<<1|1].lazy2=tr[u].lazy2;
}
tr[u].lazy1=1;tr[u].lazy2=0;
}
权值线段树
可以用于维护一个集合里的所有数,用 \(tag[l,r]\) 表示值在 \(l\sim r\) 中的数有多少个。
但这维护不了一个序列,那我们就建 \(n\) 个线段树!第 \(i\) 个线段树里插的数是 \(a_1\sim a_i\)。
这样的话,我们想问第 \(l\sim r\) 中的区间信息,就将第 \(r\) 棵减去第 \(l-1\) 棵。
然而,这种做法极其浪费空间。
优化1:重复利用
因为两两线段树长得极其相似,所以我们可以重复利用节点。
点击查看代码
void upd(int &now,int old,int l,int r,int x){
now=++cnt;tr[now]=tr[old];tr[now].num++;
if(l==r)return ;
int mid=l+r>>1;
if(x<=mid)upd(tr[now].l,tr[old].l,l,mid,x);
else upd(tr[now].r,tr[old].r,mid+1,r,x);
}
优化2:动态开点
显然很多数都不会出现,虽然我们可以离线下来离散化,但如果题目强制在线……
此时的节点 \(i\) 左节点不一定是 \(i*2\),右节点也不一定是 \(i*2+1\)。我们在下传标记的时候维护一下即可。
类比二叉搜索树,当前在 \([l,r]\),如果 \([l,mid]\) 权值不大于 \(k\),那么第 \(k\) 小就在左边了,反过来的话,将 \(k\) 前去 \([l,mid]\) 的权值,然后跑去右边。直到到达叶子节点 \([a,a]\),\(a\) 便是第 \(k\) 小的元素(不是元素的下标)。
点击查看代码
int qry(int now,int old,int l,int r,int x){
if(l==r)return l;
int mid=l+r>>1,len=tr[tr[old].l].num-tr[tr[now].l].num;//差分
if(x<=len)return qry(tr[now].l,tr[old].l,l,mid,x);
else return qry(tr[now].r,tr[old].r,mid+1,r,x-len);
}
待填坑
线段树合并
往往用在动态开点权值线段树中,因为每个线段树可能都长得不太一样。
P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并
先差分,变成 \(i\) 到根都放上物品 \(z\)。
每个节点都维护一个权值线段树,存储从该点到根的链上所有的数字个数,注意这仅存储操作开端在这个点上的,比如执行 \(son_u\) 到根,只会把 \(son_u\) 的线段树进行修改,而 \(u\) 的则不会。正是因为这样我们才需要合并。
然后依次合并,每个节点都与其儿子的线段树合并,这样就可以得到经过这个点的所有操作后这个点上数字个数,即可得到答案。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MT int _T;cin>>_T;while(_T--)
#define rep(i,a,b) for(int i=a;i<=b;i++)
const int N=1e5+10;
int n,m,ct,root[N],ans[N];
struct node{
int l,r,v,id;
}tr[N*200];
void pushup(int x){
if(tr[tr[x].l].v>=tr[tr[x].r].v)tr[x].v=tr[tr[x].l].v,tr[x].id=tr[tr[x].l].id;
else tr[x].v=tr[tr[x].r].v,tr[x].id=tr[tr[x].r].id;
}
void add(int x,int &p,int l,int r,int v){
if(!p)p=++ct;
if(l==r){
tr[p].v+=v;tr[p].id=l;
return ;
}
int mid=l+r>>1;
if(x<=mid)add(x,tr[p].l,l,mid,v);
else add(x,tr[p].r,mid+1,r,v);
pushup(p);
}
int merge(int a,int b,int l,int r){
if(!a||!b)return a+b;
if(l==r){
tr[a].v+=tr[b].v;
return a;
}
int mid=l+r>>1;
tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
pushup(a);
return a;
}
vector<int>e[N];int fa[20][N],dep[N];
void dfs(int x,int f){
dep[x]=dep[fa[0][x]=f]+1;
rep(i,1,19)fa[i][x]=fa[i-1][fa[i-1][x]];
for(auto i:e[x])if(i!=f)dfs(i,x);
}
int lca(int x,int y){
if(dep[x]>dep[y])swap(x,y);
for(int i=19;i>=0;i--)if(dep[fa[i][y]]>=dep[x])y=fa[i][y];
if(x==y)return x;
for(int i=19;i>=0;i--)if(fa[i][x]!=fa[i][y])x=fa[i][x],y=fa[i][y];
return fa[0][x];
}
void mer(int x,int f){
for(auto i:e[x]){
if(i!=f){
mer(i,x);
root[x]=merge(root[x],root[i],1,1000000000);
}
}
ans[x]=tr[root[x]].v?tr[root[x]].id:0;
}
int main(){
// freopen("a.in","r",stdin);
scanf("%d%d",&n,&m);
rep(i,1,n-1){
int u,v;scanf("%d%d",&u,&v);
e[u].push_back(v);e[v].push_back(u);
}
dfs(1,0);
rep(i,1,m){
int x,y,z;scanf("%d%d%d",&x,&y,&z);
add(z,root[x],1,1000000000,1);
add(z,root[y],1,1000000000,1);
add(z,root[lca(x,y)],1,1000000000,-1);
add(z,root[fa[0][lca(x,y)]],1,1000000000,-1);
}
mer(1,0);
rep(i,1,n)printf("%d\n",ans[i]);
return 0;
}
标记永久化
即:标记不下传。
设要操作的区间为 \([a,b]\),现在节点的区间为 \([l,r]\),若 \(a\le l\) 且 \(r\le b\)(即节点区间完全被操作区间包含),那么我们将标记正常更新。
如何处理询问?我们只需要计算当前区间的对询问区间的贡献就可以了,显然只会对重合部分有贡献,加起来即是答案。
点击查看代码
void add(int x,int l,int r,int L,int R,int v){
if(L<=l&&r<=R){
tr[x].tg+=v;
return ;
}
tr[x].sum+=v*(min(r,y)-min(l,x)+1);
int mid=l+r>>1;
if(L<=mid)add(x<<1,l,mid,L,R,v);
if(R>mid)add(x<<1|1,mid+1,r,L,R,v);
}
int qry(int x,int l,int r,int L,int R){
if(L<=l&&r<=R)return tr[x].sum+(r-l+1)*tr[x].tg;
int sum=(min(r,R)-max(l,L)+1)*tr[x].tg;
int mid=l+r>>1;
if(L<=mid)sum+=qry(x<<1,l,mid,L,R);
if(R>mid)sum+=qry(x<<1|1,mid+1,r,L,R);
return sum;
}
显然我们可以有一个暴力的单调数组做法。
先离散化,然后对每一种波都开一个单调队列,开始时把 \(1\) 全扔进去,然后对于一个星球 \(i\),处理每个队列的队头,如果 \(dis_i-dis_{top}>L\),那就弹出。
然后对于 \(l_i\sim r_i\) 的队列,取队头最小值,于是 \(ans_i=min+1\),最后把 \(i\) 插入 \(l_i\sim r_i\) 的队列即可。
可以用线段树优化。
每个点都开一个单调队列,表示这个区间需要被插入的节点,如果这个节点是叶子,那么它就是指我们原本的第 \(l\) 个单调队列。
于是每个点的队首就应该是从它到根的所有单调队列(包括它自己)的队头最小值。
点击查看代码
void ins(int x,int l,int r,int L,int R,o v){
if(L<=l&&r<=R){
while(!tr[x].empty()&&tr[x].back().b>=v.b)tr[x].pop_back();
tr[x].push_back(v);
return ;
}
while(!tg[x].empty()&&tg[x].back().b>=v.b)tg[x].pop_back();
tg[x].push_back(v);
int mid=l+r>>1;
if(R<=mid)ins(x<<1,l,mid,L,R,v);
else if(L>mid)ins(x<<1|1,mid+1,r,L,R,v);
else ins(x<<1,l,mid,L,mid,v),ins(x<<1|1,mid+1,r,mid+1,R,v);
}
int qry(int x,int l,int r,int L,int R,int w){
while(w-tr[x].front().a>ll&&!tr[x].empty())tr[x].pop_front();
int res=10000000;
if(!tr[x].empty())res=tr[x].front().b;
while(w-tg[x].front().a>ll&&!tg[x].empty())tg[x].pop_front();
if(L<=l&&r<=R){
if(!tg[x].empty())res=min(res,tg[x].front().b);
return res;
}
int mid=l+r>>1;
if(R<=mid)res=min(res,qry(x<<1,l,mid,L,R,w));
else if(L>mid)res=min(res,qry(x<<1|1,mid+1,r,L,R,w));
else res=min(res,min(qry(x<<1,l,mid,L,mid,w),qry(x<<1|1,mid+1,r,mid+1,R,w)));
return res;
}

浙公网安备 33010602011771号