====== 线段树合并/分裂 ====== ===== 算法简介 ===== 一种快速合并、分裂线段树(一般为权值线段树)的算法,时空间复杂度 $O(m\log n)$。 ===== 算法思想 ===== 更新、分裂线段树时动态开点,易知更新操作时空间复杂度 $O(\log n)$。 关于分裂操作,遇到空结点则返回。其余操作同线段树查询,遇到分裂区间的子区间则将原节点转移给新节点,然后返回。 所以分裂操作的时空间复杂度同线段树查询操作的时间复杂度,即 $O(\log n)$。 关于合并操作,如果遇到叶子节点或空结点就直接 $\text{return}$,否则跑子树。 关于合并操作的时间复杂度,每次合并时间复杂度为两棵线段树的重叠部分的结点数,而每次合并一个节点等价于删除一个节点。 每个节点至多被删除一次,所以合并操作的总时间复杂度等于空间复杂度,即 $O(m\log n)$。 ===== 代码模板 ===== [[https://www.luogu.com.cn/problem/P5494|洛谷p5494]] const int MAXN=2e5+5,MAXM=30; struct Node{ LL cnt; int ch[2]; }node[MAXN*MAXM]; struct Pool{ int Stack[MAXN*MAXM],top,tot; int New(){return top?Stack[top--]:++tot;} void Delate(int x){ node[x].cnt=node[x].ch[0]=node[x].ch[1]=0; Stack[++top]=x; } }pool; void push_up(int k){node[k].cnt=node[node[k].ch[0]].cnt+node[node[k].ch[1]].cnt;} void update(int &k,int lef,int rig,int pos,int v){ if(!k)k=pool.New(); if(lef==rig) return node[k].cnt+=v,void(); int mid=lef+rig>>1; if(mid>=pos) update(node[k].ch[0],lef,mid,pos,v); else update(node[k].ch[1],mid+1,rig,pos,v); push_up(k); } void Merge(int &k1,int k2,int lef,int rig){ if(!k1||!k2) return k1|=k2,void(); if(lef==rig) return node[k1].cnt+=node[k2].cnt,pool.Delate(k2); int mid=lef+rig>>1; Merge(node[k1].ch[0],node[k2].ch[0],lef,mid); Merge(node[k1].ch[1],node[k2].ch[1],mid+1,rig); pool.Delate(k2); push_up(k1); } void split(int &k1,int &k2,int lef,int rig,int L,int R){ if(!k1)return; if(L<=lef&&rig<=R){ k2=k1; k1=0; return; } k2=pool.New(); int mid=lef+rig>>1; if(mid>=L) split(node[k1].ch[0],node[k2].ch[0],lef,mid,L,R); if(mid>1; if(mid>=R) return Count(node[k].ch[0],lef,mid,L,R); else if(mid>1; if(lef==rig)return mid; if(rk<=node[node[k].ch[0]].cnt) return Rank(node[k].ch[0],lef,mid,rk); else return Rank(node[k].ch[1],mid+1,rig,rk-node[node[k].ch[0]].cnt); } int root[MAXN],root_cnt=1; int main() { int n=read_int(),m=read_int(),v,p,x,y,opt; _rep(i,1,n){ v=read_int(); if(v) update(root[1],1,n,i,v); } while(m--){ opt=read_int(),p=read_int(); switch(opt){ case 0: x=read_int(),y=read_int(); split(root[p],root[++root_cnt],1,n,x,y); break; case 1: x=read_int(); Merge(root[p],root[x],1,n); break; case 2: x=read_int(),y=read_int(); update(root[p],1,n,y,x); break; case 3: x=read_int(),y=read_int(); enter(Count(root[p],1,n,x,y)); break; case 4: x=read_int(); enter(Rank(root[p],1,n,x)); break; } } return 0; } ===== 算法练习 ===== ==== 习题一 ==== [[https://www.luogu.com.cn/problem/P4556|洛谷p4556]] === 题意 === 给定一棵 $n$ 个节点的数,$m$ 个操作。 每个操作三个参数 $x,y,z$,表示给结点 $x$ 到 $y$ 的树链上的每个点打上一个 $z$ 号标记。 经过所有操作后输出每个结点被打上的最多的标记的编号(满足条件的标记存在多个时输出编号最小的),如果该结点未被标记过,输出 $0$。 === 题解 1 === 离散化处理标记编号,防止 $\text{MLE}$。 每个结点用一棵权值线段树维护该结点的所有标记状态,树上差分打标记,最后从叶子结点开始向上合并即可。 const int MAXN=1e5+5,MAXM=20; struct Edge{ int to,next; }edge[MAXN<<1]; int head[MAXN],edge_cnt; void Insert(int u,int v){ edge[++edge_cnt].to=v; edge[edge_cnt].next=head[u]; head[u]=edge_cnt; } struct LCA{ int d[MAXN],anc[MAXN][MAXM],log2[MAXN]; void dfs(int u,int fa,int dep){ anc[u][0]=fa,d[u]=dep; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dfs(v,u,dep+1); } } void build(int root,int n){ log2[1]=0; _rep(i,2,n) log2[i]=log2[i>>1]+1; dfs(root,-1,1); _rep(i,1,n){//下标从1开始 for(int j=1;(1<=0;i--){ if(d[p]-(1<=d[q]) p=anc[p][i]; } if(p==q) return p; for(int i=log2[d[p]];i>=0;i--){ if(anc[p][i]!=-1&&anc[p][i]!=anc[q][i]){ p=anc[p][i],q=anc[q][i]; } } return anc[p][0]; } }lca; const int MAXS=MAXN*60; int root[MAXN],tot; struct Node{ int max_cnt,ans; int ch[2]; }node[MAXS]; void push_up(int k){ if(node[node[k].ch[0]].max_cnt>=node[node[k].ch[1]].max_cnt) node[k].max_cnt=node[node[k].ch[0]].max_cnt,node[k].ans=node[node[k].ch[0]].ans; else node[k].max_cnt=node[node[k].ch[1]].max_cnt,node[k].ans=node[node[k].ch[1]].ans; } void update(int &k,int lef,int rig,int pos,int v){ if(!k) k=++tot; if(lef==rig) return node[k].max_cnt+=v,node[k].ans=pos,void(); int mid=lef+rig>>1; if(pos>mid) update(node[k].ch[1],mid+1,rig,pos,v); else update(node[k].ch[0],lef,mid,pos,v); push_up(k); } void Merge(int &k1,int k2,int lef,int rig){ if(!k1||!k2) return k1|=k2,void(); if(lef==rig) return node[k1].max_cnt+=node[k2].max_cnt,void(); int mid=lef+rig>>1; Merge(node[k1].ch[0],node[k2].ch[0],lef,mid); Merge(node[k1].ch[1],node[k2].ch[1],mid+1,rig); push_up(k1); } int X[MAXN],Y[MAXN],Z[MAXN],b[MAXN],ans[MAXN],Max_z; void dfs(int u){ for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(lca.anc[u][0]==v) continue; dfs(v); Merge(root[u],root[v],1,Max_z); } ans[u]=node[root[u]].max_cnt>0?node[root[u]].ans:0; } int main() { int n=read_int(),q=read_int(),u,v,p; _for(i,1,n){ u=read_int(),v=read_int(); Insert(u,v); Insert(v,u); } lca.build(1,n); _for(i,0,q) X[i]=read_int(),Y[i]=read_int(),Z[i]=read_int(); memcpy(b,Z,sizeof(Z)); sort(b,b+q); Max_z=unique(b,b+q)-b; _for(i,0,q){ Z[i]=lower_bound(b,b+Max_z,Z[i])-b+1; update(root[X[i]],1,Max_z,Z[i],1);update(root[Y[i]],1,Max_z,Z[i],1); p=lca.query(X[i],Y[i]); update(root[p],1,Max_z,Z[i],-1); p=lca.anc[p][0]; if(p!=-1) update(root[p],1,Max_z,Z[i],-1); } dfs(1); _rep(i,1,n) if(ans[i]) enter(b[ans[i]-1]); else enter(0); return 0; } === 题解 2 === 考虑树剖,将树上问题转换为区间问题,更新路径时只打差分打标记。 最后询问时只建一棵权值线段树,依次释放区间上每个位置的标记,维护标记前缀和、答案。 时间复杂度 $O(m\log^2 n)$,空间复杂度 $O(m\log n)$,但常数远小于线段树合并。 const int MAXN=1e5+5,MAXM=20; struct Edge{ int to,next; }edge[MAXN<<1]; int head[MAXN],edge_cnt; void Insert(int u,int v){ edge[++edge_cnt].to=v; edge[edge_cnt].next=head[u]; head[u]=edge_cnt; } struct lazy_tag{ int v,next; }lazy[MAXN*MAXM]; int head_2[MAXN],lazy_cnt; void Insert_2(int u,int v){ lazy[++lazy_cnt].v=v; lazy[lazy_cnt].next=head_2[u]; head_2[u]=lazy_cnt; } int Max_cnt[MAXN<<2],Ans[MAXN<<2],lef[MAXN<<2],rig[MAXN<<2]; void build(int k,int L,int R){ lef[k]=L,rig[k]=R; int M=L+R>>1; if(L==R)return Ans[k]=M,void(); build(k<<1,L,M); build(k<<1|1,M+1,R); } void push_up(int k){ if(Max_cnt[k<<1]>=Max_cnt[k<<1|1]){ Max_cnt[k]=Max_cnt[k<<1]; Ans[k]=Ans[k<<1]; } else{ Max_cnt[k]=Max_cnt[k<<1|1]; Ans[k]=Ans[k<<1|1]; } } void update(int k,int pos,int v){ if(lef[k]==rig[k]) return Max_cnt[k]+=v,void(); int mid=lef[k]+rig[k]>>1; if(pos<=mid) update(k<<1,pos,v); else update(k<<1|1,pos,v); push_up(k); } int d[MAXN],sz[MAXN],f[MAXN],dfs_id[MAXN],inv_id[MAXN],dfs_t; int h_son[MAXN],mson[MAXN],p[MAXN]; void dfs_1(int u,int fa,int depth){ sz[u]=1;f[u]=fa;d[u]=depth;mson[u]=0; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dfs_1(v,u,depth+1); sz[u]+=sz[v]; if(sz[v]>mson[u]){ h_son[u]=v; mson[u]=sz[v]; } } } void dfs_2(int u,int top){ dfs_id[u]=++dfs_t;inv_id[dfs_t]=u;p[u]=top; if(mson[u]) dfs_2(h_son[u],top); for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(v==f[u]||v==h_son[u]) continue; dfs_2(v,v); } } void update_path(int u,int v,int w){ while(p[u]!=p[v]){ if(d[p[u]]d[v]) swap(u,v); Insert_2(dfs_id[u],w); Insert_2(dfs_id[v]+1,-w); } void update_node(int u){ for(int i=head_2[u];i;i=lazy[i].next){ if(lazy[i].v>0) update(1,lazy[i].v,1); else update(1,-lazy[i].v,-1); } } int X[MAXN],Y[MAXN],Z[MAXN],b[MAXN],ans[MAXN]; int main() { int n=read_int(),q=read_int(),u,v,w; _for(i,1,n){ u=read_int(),v=read_int(); Insert(u,v); Insert(v,u); } _for(i,0,q) X[i]=read_int(),Y[i]=read_int(),Z[i]=read_int(); memcpy(b,Z,sizeof(Z)); sort(b,b+q); int Max_z=unique(b,b+q)-b; dfs_1(1,-1,0); dfs_2(1,1); build(1,1,Max_z); _for(i,0,q) update_path(X[i],Y[i],lower_bound(b,b+Max_z,Z[i])-b+1); _rep(i,1,n){ update_node(i); ans[inv_id[i]]=Max_cnt[1]>0?b[Ans[1]-1]:0; } _rep(i,1,n) enter(ans[i]); return 0; } ==== 习题二 ==== [[https://www.luogu.com.cn/problem/P3224|洛谷p3224]] === 题意 === 给定 $n$ 个点,所有点的点权恰好为 $1 \sim n$ 的排列,接下来两种操作。 - 询问与某点 $x$ 联通的点集中的第 $k$ 大元素的编号,若不存在输出 $-1$。 - 连接点 $u$、$v$。 === 题解 === 每个点维护一棵权值线段树,并查集维护连通分量。 对操作 $1$ 类似名次树操作,对操作 $2$ 使用线段树合并即可,时空间复杂度 $O(n\log n)$。 const int MAXN=1e5+5,MAXS=MAXN*40; int root[MAXN],tot; struct Node{ int sz; int ch[2]; }node[MAXS]; void push_up(int k){node[k].sz=node[node[k].ch[0]].sz+node[node[k].ch[1]].sz;} void update(int &k,int lef,int rig,int pos){ k=++tot; node[k].sz++; if(lef==rig) return; int mid=lef+rig>>1; if(pos>mid) update(node[k].ch[1],mid+1,rig,pos); else update(node[k].ch[0],lef,mid,pos); } void Merge(int &k1,int k2,int lef,int rig){ if(!k1||!k2) return k1|=k2,void(); int mid=lef+rig>>1; Merge(node[k1].ch[0],node[k2].ch[0],lef,mid); Merge(node[k1].ch[1],node[k2].ch[1],mid+1,rig); push_up(k1); } int query(int k,int lef,int rig,int rk){ if(rk>node[k].sz)return 0; int mid=lef+rig>>1; if(lef==rig)return mid; if(rk<=node[node[k].ch[0]].sz) return query(node[k].ch[0],lef,mid,rk); else return query(node[k].ch[1],mid+1,rig,rk-node[node[k].ch[0]].sz); } int kth[MAXN],Rank[MAXN],p[MAXN],n; int Find(int x){return x==p[x]?x:p[x]=Find(p[x]);} void Merge(int u,int v){ int x=Find(u),y=Find(v); if(x!=y){ p[y]=x; Merge(root[x],root[y],1,n); } } int main() { n=read_int(); int m=read_int(),u,v; Rank[0]=-1; _rep(i,1,n){ kth[i]=read_int(),p[i]=i,Rank[kth[i]]=i; update(root[i],1,n,kth[i]); } while(m--){ u=read_int(),v=read_int(); Merge(u,v); } int q=read_int(); char opt; while(q--){ opt=get_char(),u=read_int(),v=read_int(); if(opt=='Q') enter(Rank[query(root[Find(u)],1,n,v)]); else Merge(u,v); } return 0; } ==== 习题三 ==== [[https://www.luogu.com.cn/problem/P1600|洛谷p1600]] === 题意 === 一棵 $n$ 个结点的树,树上有 $m$ 个玩家,第 $i$ 个玩家的起点为 $s_i$,终点为 $t_i$,每个玩家每秒移动一条边。 在每个结点放置一名观察者,第 $i$ 个结点的观察者会在第 $w_i$ 秒观察到达该点的玩家。 玩家到终点后立即消失,但可以在该时刻被观察者观测。 问每个观察者观察到的玩家数量。 === 题解 1 === 先将树转化为一棵有根树,考虑玩家对的结点 $u$ 的观察者的贡献。 可以把玩家的运动分解为 $s\to \text{LCA}(s,t)\to t$。 考虑 $s\to \text{LCA}(s,t)$,只当且仅当 $u\in \{s\to \text{LCA}(s,t)\}$ 且 $w_u=d_s-d_u$ 才产生贡献。 考虑 $\text{LCA}(s,t)\to t$,只当且仅当 $u\in \{\text{LCA}(s,t)\to t\}$ 且 $\text{dist}(s,t)-w_u=d_t-d_u$ 才产生贡献。 分离变量,得 $w_u+d_u=d_s$ 和 $w_u-d_u=\text{dist}(s,t)-d_t$,可以考虑每个结点用两个桶维护可以产生贡献的 $d_s$ 和 $\text{dist}(s,t)-d_t$ 的数量。 然后考虑怎么处理 $u\in \{s\to \text{LCA}(s,t)\}$ 和 $u\in \{\text{LCA}(s,t)\to t\}$ 这两个限制条件。 考虑树上差分,在 $s$ 和 $t$ 结点打上增加标记, $\text{LCA}(s,t)$ 结点打上删除标记。 对每个结点,先 $\text{dfs}$ 子节点,然后处理增加标记,然后查询答案,最后处理删除标记。 考虑如果 $\text{LCA}(s,t)$ 满足 $w_{\text{LCA}(s,t)}+d_{\text{LCA}(s,t)}=d_s$,必有 $w_{\text{LCA}(s,t)}-d_{\text{LCA}(s,t)}=\text{dist}(s,t)-d_t$,导致 $s$ 和 $t$ 重复贡献,所以要提前处理。 const int MAXN=3e5+5; int head[MAXN],edge_cnt; struct Edge{ int to,next; }edge[MAXN<<1]; void Insert(int u,int v){ edge[++edge_cnt].next=head[u]; edge[edge_cnt].to=v; head[u]=edge_cnt; } struct LCA{ int d[MAXN],sz[MAXN],f[MAXN]; int h_son[MAXN],mson[MAXN],p[MAXN]; void dfs_1(int u,int fa,int depth){ sz[u]=1;f[u]=fa;d[u]=depth;mson[u]=0; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(v==fa) continue; dfs_1(v,u,depth+1); sz[u]+=sz[v]; if(sz[v]>mson[u]) h_son[u]=v,mson[u]=sz[v]; } } void dfs_2(int u,int top){ p[u]=top; if(mson[u])dfs_2(h_son[u],top); for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(v==f[u]||v==h_son[u]) continue; dfs_2(v,v); } } void Init(int root){dfs_1(root,-1,0);dfs_2(root,root);} int query_lca(int u,int v){ while(p[u]!=p[v]){ if(d[p[u]] === 题解 2 === 树上差分的思路同题解 1,但这次考虑用线段树代替桶维护答案,对每个点可以直接处理贡献,然后从叶子结点向上合并线段树即可。 贴了一段别人的代码。 #include using namespace std; char ch; bool fi; template inline void read(_Type&d) { d=0, fi=0, ch=getchar(); while(!isdigit(ch) && ch!='-') ch=getchar(); if(ch=='-') fi=1, ch=getchar(); do d=d*10+ch-'0', ch=getchar(); while(isdigit(ch)); if(fi) d=~d+1; } const int N=300000+1; struct DymSegTree { int ls[30*N],rs[30*N],sum[30*N],rt[N]; int rb[30*N],top,tot; inline int newNode() { return top?rb[top--]:++tot; } inline void delNode(int x) { rb[++top]=x; ls[x]=rs[x]=sum[x]=0; } void modify(int&x,int l,int r,int pos,int val) { if(!x) x=newNode(); if(l==r) { sum[x]+=val; return;} int mid=(l+r)>>1; if(pos<=mid) modify(ls[x],l,mid,pos,val); else modify(rs[x],mid+1,r,pos,val); sum[x]=sum[ls[x]]+sum[rs[x]]; } int merge(int x,int y,int l,int r) { if(!x||!y) return x|y; int now=newNode(); if(l==r) sum[now]=sum[x]+sum[y]; else { int mid=(l+r)>>1; ls[now]=merge(ls[x],ls[y],l,mid); rs[now]=merge(rs[x],rs[y],mid+1,r); sum[now]=sum[ls[now]]+sum[rs[now]]; } delNode(x), delNode(y); return now; } int query(int x,int l,int r,int pos) { if(l==r) return sum[x]; int mid=(l+r)>>1; return (mid>=pos) ? query(ls[x],l,mid,pos) :query(rs[x],mid+1,r,pos); } } T1, T2; vector E[N]; vector G[N]; struct Player { int u,v,tim; } a[N]; int n,m,w[N],ans[N],dep[N],fa[N][22]; void pre(int x,int pa) { dep[x]=dep[fa[x][0]=pa]+1; for(int i=1; (1<