====== 线段树合并/分裂 ======
===== 算法简介 =====
一种快速合并、分裂线段树(一般为权值线段树)的算法,时空间复杂度 $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<