线段树进阶

普通线段树

核心在于向上更新(pushup)和下传标记(pushdown)以及懒标记的设计。

P3373 【模板】线段树 2

维护一个加法标记和乘法标记。

下传标记时,将乘法标记更新加法标记。

点击查看代码
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;
}

Q4.4.5.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\)。我们在下传标记的时候维护一下即可。

P3834 【模板】可持久化线段树 2

类比二叉搜索树,当前在 \([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);
}

P3402 可持久化并查集

待填坑

线段树合并

往往用在动态开点权值线段树中,因为每个线段树可能都长得不太一样。

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;
}

Q5.2.4.3. 大sz的游戏

显然我们可以有一个暴力的单调数组做法。

先离散化,然后对每一种波都开一个单调队列,开始时把 \(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;
}

posted @ 2023-09-07 08:28  include_c  阅读(45)  评论(0)    收藏  举报