## 线段树合并 ### 前置知识 1. 动态开点线段树 每个点左儿子和右儿子的节点编号不是确定的,只需要记录一下,剩下的和普通线段树相同, 2. 权值线段树 大部分线段树合并都是用权值线段树来做的。 简单说就是就是以权值为下标建立的线段树,每个点维护当前权值相关的量。 ### 过程 每次合并线段树 $A$ , $B$ 时,直接将$B$合并入 $A$ ,不需要关心 $A$ , $B$ 中哪棵树里节点多 我们考虑以下两种情况: 1. 如果 $A$ 和 $B$ 都非空,我们递归将 $B$ 的左儿子和 $A$ 的左儿子合并, $B$ 的右儿子和 $A$ 的右儿子合并,然后返回$A$节点 2. 如果 $A$ , $B$ 中有一个是空的,我们返回另一个非空的节点 ### 代码 ```cpp int merge(int a, int b, int L, int R) { if (!a) return b; if (!b) return a; if (L==R) { [按照需要合并] return a; } int mid=(L+R)>>1; l[a]=merge(l[a], l[b], L, mid); r[a]=merge(r[a], r[b], mid+1, R); push_up(a); return a; } ``` ### 复杂度 ? ~~空间复杂度:$O(n\log n)$~~ ~~时间复杂度:每个节点最多删除 $1$ 次,所以时间复杂度为 $O(n\log n)$~~ ### 例题 ##### [P3605 [USACO17JAN]Promotion Counting P](https://www.luogu.com.cn/problem/P3605) 给定一棵树,每个点有点权,求每一个点的子树内有多少点的点权比当前节点大。 ##### 分析 一看就可以用树状数组做,实现离散化,做一遍 dfs 每个节点的答案就是加入这个点的儿子后比他小的个数减去原来比他小的个数。 当然,线段树合并也可以做 还是需要离散化,每个节点开一棵动态开点线段树(只开有用的节点),还是在 dfs 的过程中,把当前节点和他的每一个儿子节点合并即可。 ##### 代码 (注:代码中的 add 函数没有用,可以删去) ```cpp #include using namespace std; int read() { int x=0, f=0; char c=getchar(); while (!isdigit(c)) f|=c=='-', c=getchar(); while (isdigit(c)) x=(x<<3)+(x<<1)+(c^48), c=getchar(); return f ? -x : x; } const int N=int(1e5+10)<<5; int n, a[N], b[N], lc[N], rc[N], tot, s[N], ans[N], root[N]; vector e[N]; void build(int &p, int L, int R, int x) { p=++tot; if (L==R) return s[p]=1, void(); int mid=L+R>>1; if (x<=mid) build(lc[p], L, mid, x); else build(rc[p], mid+1, R, x); s[p]=s[lc[p]]+s[rc[p]]; } void add(int p, int L, int R, int x) { if (!p) p=++tot; if (L==R) return s[p]++, void(); int mid=L+R>>1; if (x<=mid) add(lc[p], L, mid, x); else add(rc[p], mid+1, R, x); s[p]=s[lc[p]]+s[rc[p]]; } int merge(int a, int b, int L, int R) { if (!a || !b) return a|b; if (L==R) return s[a]+=s[b], a; int mid=L+R>>1; lc[a]=merge(lc[a], lc[b], L, mid); rc[a]=merge(rc[a], rc[b], mid+1, R); s[a]=s[lc[a]]+s[rc[a]]; return a; } int query(int p, int L, int R, int l, int r) { if (!p) return 0; if (l<=L && R<=r) return s[p]; int mid=L+R>>1, res=0; if (l<=mid) res+=query(lc[p], L, mid, l, r); if (r>mid) res+=query(rc[p], mid+1, R, l, r); return res; } void dfs(int x) { for (int y:e[x]) { dfs(y); root[x]=merge(root[x], root[y], 1, n); } ans[x]=query(root[x], 1, n, a[x]+1, n); } int main() { n=read(); for (int i=1; i<=n; i++) a[i]=b[i]=read(); sort(b+1, b+n+1); for (int i=1; i<=n; i++) a[i]=lower_bound(b+1, b+n+1, a[i])-b, build(root[i], 1, n, a[i]); for (int i=2; i<=n; i++) e[read()].push_back(i); dfs(1); for (int i=1; i<=n; i++) printf("%d\n", ans[i]); return 0; } ``` #### [P3224 [HNOI2012]永无乡](https://www.luogu.com.cn/problem/P3224) - 给定一个图,初始有一些边,每个点有点权 - 1. 加边 - 1. 查询一个点所在连通块中的第 $k$ 小点权 ##### 分析 这题就是一个启发式合并的过程 每个点所在连通块开个并查集,然后开个平衡树来维护权值 合并的时候把小的连通块的权值插入到大的里面 时间复杂度是 $O(n\log n)$ 我们这里讲解一下线段树合并的方法 与启发式合并类似线段树使用动态开点实现,维护一棵权值线段树 初始每棵线段树是一个只有某个叶子节点,以及到根路径上所有点的动态开点线段树,每次合并就好了! 时间复杂度 $O(n\log n)$ ##### 代码 ```cpp #include #include using namespace std; const int N=2e6+10; int n, m, cnt, rt[N], fa[N]; int l[N], r[N], id[N], s[N]; int read() { int x=0, f=0; char ch=getchar(); while (!isdigit(ch)) f|=ch=='-', ch=getchar(); while (isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48), ch=getchar(); return f?-x:x; } int find(int x) { return fa[x]==x ? x : fa[x]=find(fa[x]); } int merge(int a, int b, int L, int R) { if (!a || !b) return a+b; if (l==r) { s[a]+=s[b]; return a; } int mid=(L+R)>>1; l[a]=merge(l[a], l[b], L, mid); r[a]=merge(r[a], r[b], mid+1, R); s[a]=s[l[a]]+s[r[a]]; return a; } void add(int &p, int L, int R, int pos, int idx) { p=++cnt; if (L==R) return id[p]=idx, s[p]++, void(); int mid=L+R>>1; if (pos<=mid) add(l[p], L, mid, pos, idx); else add(r[p], mid+1, R, pos, idx); s[p]=s[l[p]]+s[r[p]]; } int ask(int p, int L, int R, int k) { if (s[p]>1; if (k<=s[l[p]]) return ask(l[p], L, mid, k); else return ask(r[p], mid+1, R, k-s[l[p]]); } int main() { n=read(), m=read(); for (int i=1; i<=n; i++) { fa[i]=i; add(rt[i], 1, n, read(), i); } for (int i=1; i<=m; i++) { int x=find(read()), y=find(read()); fa[y]=x; rt[x]=merge(rt[x], rt[y], 1, n); } int q=read(); while (q--) { char s[3]; scanf("%s", s); if (s[0]=='B') { int x=find(read()), y=find(read()); if (x==y) continue; fa[y]=x; rt[x]=merge(rt[x], rt[y], 1, n); } else { int x=find(read()), y=read(); int ans=ask(rt[x], 1, n, y); printf("%d\n", ans ? ans : -1); } } return 0; } ``` #### 其他好题 [CF600E Lomsat gelral](https://www.luogu.com.cn/problem/CF600E):很经典的一道题 ## 线段树分裂 先康一道模板题:[P5494 【模板】线段树分裂](https://www.luogu.com.cn/problem/P5494) ### 题意 给出一个可重集 $a$(编号为 $1$),它支持以下操作: `0 p x y`:将可重集 $p$ 中大于等于 $x$ 且小于等于 $y$ 的值放入一个新的可重集中(新可重集编号为从 $2$ 开始的正整数,是上一次产生的新可重集的编号$+1$)。 `1 p t`:将可重集 $t$ 中的数放入可重集 $p$,且清空可重集 $t$(数据保证在此后的操作中不会出现可重集 $t$)。 `2 p x q`:在 $p$ 这个可重集中加入 $x$ 个数字 $q$。 `3 p x y`:查询可重集 $p$ 中大于等于 $x$ 且小于等于 $y$ 的值的个数。 `4 p k`:查询在 $p$ 这个可重集中第 $k$ 小的数,不存在时输出 $-1$。 ### 分析 其他的都很简单,主要是分裂操作。 我们考虑把以 x 为根的节点分裂,前 $k$ 小在 $x$ ,后面的在 $y$ (一个新建的根) 令$x$左子树的为$v$ 如果 $vk$ ,右子树属于 $y$ ,递归左子树。 最后再更新每个结点权值即可。 #### 复杂度 因为每次分裂只会增加 $O(\log n)$ 的势能. 因为线段树每层只会访问到一个节点,最多将这一个节点变成两个,所以只会增加 $O(\log n)$ 个节点,其他操作复杂度均为 $O(\log n)$ ,总时间复杂度 $O((n+m)\log n)$ ### 代码 #### 分裂代码 ```cpp int split(int x, int y, int k) { y=++cnt; int v=s[l[x]]; if (vk) r[y]=r[x], r[x]=0, l[y]=split(l[x], l[y], k); s[y]=s[x]-k; s[x]=k; return y; } ``` #### 本题完整代码 ```cpp #include #include #define int long long using namespace std; const int N=6e6+10; int n, m, cnt, TT=1, rt[N], bac[N], tot; int l[N], r[N], s[N]; int read() { int x=0, f=0; char ch=getchar(); while (!isdigit(ch)) f|=ch=='-', ch=getchar(); while (isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48), ch=getchar(); return f?-x:x; } int newnode() { return cnt ? bac[cnt--] : ++tot; } void del(int p) { bac[++cnt]=p; l[p]=r[p]=s[p]=0; } int split(int x, int y, int k) { if (x==0) return 0; y=newnode(); int v=s[l[x]]; if (vk) r[y]=r[x], r[x]=0, l[y]=split(l[x], l[y], k); s[y]=s[x]-k; s[x]=k; return y; } int merge(int a, int b, int L, int R) { if (!a || !b) return a+b; if (L==R) { s[a]+=s[b]; return a; } int mid=(L+R)>>1; l[a]=merge(l[a], l[b], L, mid); r[a]=merge(r[a], r[b], mid+1, R); s[a]=s[l[a]]+s[r[a]]; del(b); return a; } void add(int &p, int L, int R, int pos, int v) { if (!p) p=newnode(); s[p]+=v; if (L==R) return void(); int mid=L+R>>1; if (pos<=mid) add(l[p], L, mid, pos, v); else add(r[p], mid+1, R, pos, v); } int ask(int p, int L, int R, int k) { if (L==R) return L; int mid=L+R>>1; if (k<=s[l[p]]) return ask(l[p], L, mid, k); else return ask(r[p], mid+1, R, k-s[l[p]]); } int query(int p, int ll, int rr, int L, int R) { if (R>1; return query(l[p], ll, mid, L, R)+query(r[p], mid+1, rr, L, R); } signed main() { n=read(), m=read(); for (int i=1; i<=n; i++) add(rt[1], 1, n, i, read()); while (m--) { int opt=read(); if (opt==0) { int p=read(), x=read(), y=read(); int k1=query(rt[p], 1, n, 1, y), k2=query(rt[p], 1, n, x, y), t=0; TT++, rt[TT]=split(rt[p], rt[TT], k1-k2); t=split(rt[TT], t, k2); rt[p]=merge(rt[p], t, 1, n); } if (opt==1) { int p=read(), t=read(); rt[p]=merge(rt[p], rt[t], 1, n); } if (opt==2) { int p=read(), x=read(), q=read(); add(rt[p], 1, n, q, x); } if (opt==3) { int p=read(), x=read(), y=read(); printf("%lld\n", query(rt[p], 1, n, x, y)); } if (opt==4) { int p=read(), k=read(); printf("%lld\n", s[rt[p]] Loading... ## 线段树合并 ### 前置知识 1. 动态开点线段树 每个点左儿子和右儿子的节点编号不是确定的,只需要记录一下,剩下的和普通线段树相同, 2. 权值线段树 大部分线段树合并都是用权值线段树来做的。 简单说就是就是以权值为下标建立的线段树,每个点维护当前权值相关的量。 ### 过程 每次合并线段树 $A$ , $B$ 时,直接将$B$合并入 $A$ ,不需要关心 $A$ , $B$ 中哪棵树里节点多 我们考虑以下两种情况: 1. 如果 $A$ 和 $B$ 都非空,我们递归将 $B$ 的左儿子和 $A$ 的左儿子合并, $B$ 的右儿子和 $A$ 的右儿子合并,然后返回$A$节点 2. 如果 $A$ , $B$ 中有一个是空的,我们返回另一个非空的节点 ### 代码 ```cpp int merge(int a, int b, int L, int R) { if (!a) return b; if (!b) return a; if (L==R) { [按照需要合并] return a; } int mid=(L+R)>>1; l[a]=merge(l[a], l[b], L, mid); r[a]=merge(r[a], r[b], mid+1, R); push_up(a); return a; } ``` ### 复杂度 ? ~~空间复杂度:$O(n\log n)$~~ ~~时间复杂度:每个节点最多删除 $1$ 次,所以时间复杂度为 $O(n\log n)$~~ ### 例题 ##### [P3605 [USACO17JAN]Promotion Counting P](https://www.luogu.com.cn/problem/P3605) 给定一棵树,每个点有点权,求每一个点的子树内有多少点的点权比当前节点大。 ##### 分析 一看就可以用树状数组做,实现离散化,做一遍 dfs 每个节点的答案就是加入这个点的儿子后比他小的个数减去原来比他小的个数。 当然,线段树合并也可以做 还是需要离散化,每个节点开一棵动态开点线段树(只开有用的节点),还是在 dfs 的过程中,把当前节点和他的每一个儿子节点合并即可。 ##### 代码 (注:代码中的 add 函数没有用,可以删去) ```cpp #include <bits/stdc++.h> using namespace std; int read() { int x=0, f=0; char c=getchar(); while (!isdigit(c)) f|=c=='-', c=getchar(); while (isdigit(c)) x=(x<<3)+(x<<1)+(c^48), c=getchar(); return f ? -x : x; } const int N=int(1e5+10)<<5; int n, a[N], b[N], lc[N], rc[N], tot, s[N], ans[N], root[N]; vector<int> e[N]; void build(int &p, int L, int R, int x) { p=++tot; if (L==R) return s[p]=1, void(); int mid=L+R>>1; if (x<=mid) build(lc[p], L, mid, x); else build(rc[p], mid+1, R, x); s[p]=s[lc[p]]+s[rc[p]]; } void add(int p, int L, int R, int x) { if (!p) p=++tot; if (L==R) return s[p]++, void(); int mid=L+R>>1; if (x<=mid) add(lc[p], L, mid, x); else add(rc[p], mid+1, R, x); s[p]=s[lc[p]]+s[rc[p]]; } int merge(int a, int b, int L, int R) { if (!a || !b) return a|b; if (L==R) return s[a]+=s[b], a; int mid=L+R>>1; lc[a]=merge(lc[a], lc[b], L, mid); rc[a]=merge(rc[a], rc[b], mid+1, R); s[a]=s[lc[a]]+s[rc[a]]; return a; } int query(int p, int L, int R, int l, int r) { if (!p) return 0; if (l<=L && R<=r) return s[p]; int mid=L+R>>1, res=0; if (l<=mid) res+=query(lc[p], L, mid, l, r); if (r>mid) res+=query(rc[p], mid+1, R, l, r); return res; } void dfs(int x) { for (int y:e[x]) { dfs(y); root[x]=merge(root[x], root[y], 1, n); } ans[x]=query(root[x], 1, n, a[x]+1, n); } int main() { n=read(); for (int i=1; i<=n; i++) a[i]=b[i]=read(); sort(b+1, b+n+1); for (int i=1; i<=n; i++) a[i]=lower_bound(b+1, b+n+1, a[i])-b, build(root[i], 1, n, a[i]); for (int i=2; i<=n; i++) e[read()].push_back(i); dfs(1); for (int i=1; i<=n; i++) printf("%d\n", ans[i]); return 0; } ``` #### [P3224 [HNOI2012]永无乡](https://www.luogu.com.cn/problem/P3224) - 给定一个图,初始有一些边,每个点有点权 - 1. 加边 - 1. 查询一个点所在连通块中的第 $k$ 小点权 ##### 分析 这题就是一个启发式合并的过程 每个点所在连通块开个并查集,然后开个平衡树来维护权值 合并的时候把小的连通块的权值插入到大的里面 时间复杂度是 $O(n\log n)$ 我们这里讲解一下线段树合并的方法 与启发式合并类似线段树使用动态开点实现,维护一棵权值线段树 初始每棵线段树是一个只有某个叶子节点,以及到根路径上所有点的动态开点线段树,每次合并就好了! 时间复杂度 $O(n\log n)$ ##### 代码 ```cpp #include <cstdio> #include <cctype> using namespace std; const int N=2e6+10; int n, m, cnt, rt[N], fa[N]; int l[N], r[N], id[N], s[N]; int read() { int x=0, f=0; char ch=getchar(); while (!isdigit(ch)) f|=ch=='-', ch=getchar(); while (isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48), ch=getchar(); return f?-x:x; } int find(int x) { return fa[x]==x ? x : fa[x]=find(fa[x]); } int merge(int a, int b, int L, int R) { if (!a || !b) return a+b; if (l==r) { s[a]+=s[b]; return a; } int mid=(L+R)>>1; l[a]=merge(l[a], l[b], L, mid); r[a]=merge(r[a], r[b], mid+1, R); s[a]=s[l[a]]+s[r[a]]; return a; } void add(int &p, int L, int R, int pos, int idx) { p=++cnt; if (L==R) return id[p]=idx, s[p]++, void(); int mid=L+R>>1; if (pos<=mid) add(l[p], L, mid, pos, idx); else add(r[p], mid+1, R, pos, idx); s[p]=s[l[p]]+s[r[p]]; } int ask(int p, int L, int R, int k) { if (s[p]<k) return 0; if (L==R) return id[p]; int mid=L+R>>1; if (k<=s[l[p]]) return ask(l[p], L, mid, k); else return ask(r[p], mid+1, R, k-s[l[p]]); } int main() { n=read(), m=read(); for (int i=1; i<=n; i++) { fa[i]=i; add(rt[i], 1, n, read(), i); } for (int i=1; i<=m; i++) { int x=find(read()), y=find(read()); fa[y]=x; rt[x]=merge(rt[x], rt[y], 1, n); } int q=read(); while (q--) { char s[3]; scanf("%s", s); if (s[0]=='B') { int x=find(read()), y=find(read()); if (x==y) continue; fa[y]=x; rt[x]=merge(rt[x], rt[y], 1, n); } else { int x=find(read()), y=read(); int ans=ask(rt[x], 1, n, y); printf("%d\n", ans ? ans : -1); } } return 0; } ``` #### 其他好题 [CF600E Lomsat gelral](https://www.luogu.com.cn/problem/CF600E):很经典的一道题 ## 线段树分裂 先康一道模板题:[P5494 【模板】线段树分裂](https://www.luogu.com.cn/problem/P5494) ### 题意 给出一个可重集 $a$(编号为 $1$),它支持以下操作: `0 p x y`:将可重集 $p$ 中大于等于 $x$ 且小于等于 $y$ 的值放入一个新的可重集中(新可重集编号为从 $2$ 开始的正整数,是上一次产生的新可重集的编号$+1$)。 `1 p t`:将可重集 $t$ 中的数放入可重集 $p$,且清空可重集 $t$(数据保证在此后的操作中不会出现可重集 $t$)。 `2 p x q`:在 $p$ 这个可重集中加入 $x$ 个数字 $q$。 `3 p x y`:查询可重集 $p$ 中大于等于 $x$ 且小于等于 $y$ 的值的个数。 `4 p k`:查询在 $p$ 这个可重集中第 $k$ 小的数,不存在时输出 $-1$。 ### 分析 其他的都很简单,主要是分裂操作。 我们考虑把以 x 为根的节点分裂,前 $k$ 小在 $x$ ,后面的在 $y$ (一个新建的根) 令$x$左子树的为$v$ 如果 $v<k$ ,左子树属于 $x$ ,只需要递归右子树, $k$ 变成 $k-v$ 。 如果 $v=k$ ,左子树属于 $x$ ,右子树属于 $y$ 即可。 如果 $v>k$ ,右子树属于 $y$ ,递归左子树。 最后再更新每个结点权值即可。 #### 复杂度 因为每次分裂只会增加 $O(\log n)$ 的势能. 因为线段树每层只会访问到一个节点,最多将这一个节点变成两个,所以只会增加 $O(\log n)$ 个节点,其他操作复杂度均为 $O(\log n)$ ,总时间复杂度 $O((n+m)\log n)$ ### 代码 #### 分裂代码 ```cpp int split(int x, int y, int k) { y=++cnt; int v=s[l[x]]; if (v<k) r[y]=split(r[x], r[y], k-v); if (v==k) r[y]=r[x], r[x]=0; if (v>k) r[y]=r[x], r[x]=0, l[y]=split(l[x], l[y], k); s[y]=s[x]-k; s[x]=k; return y; } ``` #### 本题完整代码 ```cpp #include <cstdio> #include <cctype> #define int long long using namespace std; const int N=6e6+10; int n, m, cnt, TT=1, rt[N], bac[N], tot; int l[N], r[N], s[N]; int read() { int x=0, f=0; char ch=getchar(); while (!isdigit(ch)) f|=ch=='-', ch=getchar(); while (isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48), ch=getchar(); return f?-x:x; } int newnode() { return cnt ? bac[cnt--] : ++tot; } void del(int p) { bac[++cnt]=p; l[p]=r[p]=s[p]=0; } int split(int x, int y, int k) { if (x==0) return 0; y=newnode(); int v=s[l[x]]; if (v<k) r[y]=split(r[x], r[y], k-v); if (v==k) r[y]=r[x], r[x]=0; if (v>k) r[y]=r[x], r[x]=0, l[y]=split(l[x], l[y], k); s[y]=s[x]-k; s[x]=k; return y; } int merge(int a, int b, int L, int R) { if (!a || !b) return a+b; if (L==R) { s[a]+=s[b]; return a; } int mid=(L+R)>>1; l[a]=merge(l[a], l[b], L, mid); r[a]=merge(r[a], r[b], mid+1, R); s[a]=s[l[a]]+s[r[a]]; del(b); return a; } void add(int &p, int L, int R, int pos, int v) { if (!p) p=newnode(); s[p]+=v; if (L==R) return void(); int mid=L+R>>1; if (pos<=mid) add(l[p], L, mid, pos, v); else add(r[p], mid+1, R, pos, v); } int ask(int p, int L, int R, int k) { if (L==R) return L; int mid=L+R>>1; if (k<=s[l[p]]) return ask(l[p], L, mid, k); else return ask(r[p], mid+1, R, k-s[l[p]]); } int query(int p, int ll, int rr, int L, int R) { if (R<ll || rr<L) return 0; if (L<=ll && rr<=R) return s[p]; int mid=ll+rr>>1; return query(l[p], ll, mid, L, R)+query(r[p], mid+1, rr, L, R); } signed main() { n=read(), m=read(); for (int i=1; i<=n; i++) add(rt[1], 1, n, i, read()); while (m--) { int opt=read(); if (opt==0) { int p=read(), x=read(), y=read(); int k1=query(rt[p], 1, n, 1, y), k2=query(rt[p], 1, n, x, y), t=0; TT++, rt[TT]=split(rt[p], rt[TT], k1-k2); t=split(rt[TT], t, k2); rt[p]=merge(rt[p], t, 1, n); } if (opt==1) { int p=read(), t=read(); rt[p]=merge(rt[p], rt[t], 1, n); } if (opt==2) { int p=read(), x=read(), q=read(); add(rt[p], 1, n, q, x); } if (opt==3) { int p=read(), x=read(), y=read(); printf("%lld\n", query(rt[p], 1, n, x, y)); } if (opt==4) { int p=read(), k=read(); printf("%lld\n", s[rt[p]]<k ? -1ll : ask(rt[p], 1, n, k)); } } return 0; } ``` 最后修改:2025 年 08 月 12 日 © 允许规范转载 赞 1 如果觉得我的文章对你有用,请随意赞赏