「笔记」线段树合并/分裂
胡扯几句
众所周知,学习一个算法最快的方法是把代码贺一遍,如果仍然不会,就再贺一遍(
线段树合并/分裂操作的是权值线段树。
所以要会熟练的运用主席树。
线段树合并一般出现在需要把两棵权值线段树的信息整合在一起的情况。
同理,线段树分裂就是将一棵权值线段树的信息拆成两部分处理。
线段树合并
线段树合并的目的是把两棵权值线段树合并,一个朴素的想法是把一个树中的元素一个一个塞进另一个树中,复杂度是 \(O(n \log n)\) 的。
这个复杂度并不是十分理想,考虑将对应结点相加。
即从根节点递归相加,遇到空节点停止。虽然,它的最坏复杂度是 \(O(n \log n)\),(当合并对象是一个满二叉树时),但是它在大多数情况下跑的是飞快的。
这里给出核心代码
void Merge(int &x, int y, int l, int r) {
if(!x || !y) { x = x + y; return ;} // 遇到空节点返回
if(l == r) { cnt[x] += cnt[y]; maxm[x] = x; return ; }
int mid = (l + r) >> 1;
Merge(lson[x], lson[y], l, mid), Merge(rson[x], rson[y], mid + 1, r); // 递归合并
// Del(y);
Push_up(x); // 回溯时进行合并
return ;
}
线段树分裂
操作目的和它的名字一样,把一棵权值线段树分裂成两颗权值线段树。
一般情况下是按照元素个数分裂。
按照一下规则:
设要保留权值线段树的前 \(k\) 个元素,将其他元素分裂出去,设 \(siz\) 表示左子树大小。
- 如果 \(k < siz\),此时分裂的部分在左子树内,递归到左子树分裂。注意此时应将右子树全部分裂出去。
- 如果 \(k = siz\),直接把左子树和右子树分裂。
- 如果 \(k > siz\),此时递归右子树分裂。
递归重复上述过程,遇到空节点结束。
每次分裂都要新建一个结点,作为新树的根。
回溯的时候进行所有更新操作。
当然如果想按权值分裂的话可以先求出前面一段权值内有多少元素,在按照元素个数进行分裂。
求一段区间内的元素个数是主席树的基本操作了,这里不赘述。
详细操作看代码。
void Split(int now_, int &y, int k) { // 把 以 now_ 为根的权值线段树中的前 k 个值保留,其他的分裂出去
if(!now_) return ;
y = NewNode();
int siz = tr[ls].sum;
if(siz < k) Split(rs, tr[y].rson, k - siz); // 递归右子树分裂
else swap(rs, tr[y].rson); // 把右子树全部分裂出去
if(siz > k) Split(ls, tr[y].lson, k); // 递归左子树分裂
tr[y].sum = tr[now_].sum - k; // 分裂出一部分
tr[now_].sum = k; // 更新原来的
}
如果还不懂可以结合下面几道例题加深印象。
例题
P4556 [Vani有约会]雨天的尾巴 /【模板】线段树合并
给你一棵树,每次操作选择两个点,在这两个点及其简单路径上的点上发一次 \(z\) 类型的救济粮。最后询问每个点发放最多的是哪种救济粮。
考虑把救济粮种类放在权值线段树上维护,对每个点建立一棵权值线段树。
每个点已经在维护一棵权值线段树了,也就不方便进行区间修改。
考虑树上差分,修改 \(u,v,lca,fath_{lca}\) 这四个点。
最后 \(dfs\) 回溯时将子树的信息合并到根节点,直接找区间最大即可。
这里的信息合并就用到了上面讲的线段树合并。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
const int Max = 1e5;
struct edge {
int to, nxt;
}e[MAXN << 1];
int head[MAXN], num_edge = 1;
int n, m;
int root[MAXN], ans[MAXN];
int read(){
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
namespace Hjt {
#define ls lson[now_]
#define rs rson[now_]
int lson[MAXN << 6], rson[MAXN << 6], cnt[MAXN << 6], maxm[MAXN << 6], pre[MAXN << 6], node_num;
// int bin[MAXN << 6], top = 0;
void Push_up(int now_) { maxm[now_] = cnt[maxm[ls]] >= cnt[maxm[rs]] ? maxm[ls] : maxm[rs]; }
// void Del(int now_) { ls = rs = cnt[now_] = maxm[now_] = pre[now_] = 0, bin[++top] = now_;}
void Insert(int &now_, int l, int r, int val_, int k) {
// if(!now_) now_ = top ? bin[top--] : ++node_num;
if(!now_) now_ = ++node_num;
if(l == r) {
if(l != val_) return ;
cnt[now_] += k;
maxm[now_] = now_;
pre[now_] = l;
return ;
}
int mid = (l + r) >> 1;
if(val_ <= mid) Insert(ls, l, mid, val_, k);
else Insert(rs, mid + 1, r, val_, k);
Push_up(now_);
}
int Query(int now_, int l, int r) { return maxm[now_]; }
void Merge(int &x, int y, int l, int r) {
if(!x || !y) { x = x + y; return ;}
if(l == r) { cnt[x] += cnt[y]; maxm[x] = x; return ; }
int mid = (l + r) >> 1;
Merge(lson[x], lson[y], l, mid), Merge(rson[x], rson[y], mid + 1, r);
// Del(y); //删除操作,可以节省空间。但这到题没有新建结点也没有删除结点的操作,加上就是画蛇添足了。
Push_up(x);
return ;
}
}
namespace Cut {
int dfn[MAXN], top[MAXN], fath[MAXN], siz[MAXN], son[MAXN], dep[MAXN];
void add_edge(int from, int to) { e[++num_edge] = (edge){to, head[from]}, head[from] = num_edge; }
void dfs(int u, int fa) {
dep[u] = dep[fa] + 1, fath[u] = fa, siz[u] = 1;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(v == fa) continue;
dfs(v, u);
siz[u] += siz[v];
if(siz[son[u]] < siz[v]) son[u] = v;
}
}
void dfs2(int u, int tp) {
top[u] = tp;
if(son[u]) dfs2(son[u], tp);
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(v == fath[u] || v == son[u]) continue;
dfs2(v, v);
}
}
int Get_Lca(int x, int y) {
while(top[x] != top[y]) dep[top[x]] < dep[top[y]] ? y = fath[top[y]] : x = fath[top[x]];
return dep[x] < dep[y] ? x : y;
}
}
void Dfs(int u, int fa) {
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(v == fa) continue;
Dfs(v, u);
Hjt::Merge(root[u], root[v], 1, Max);
}
ans[u] = Hjt::Query(root[u], 1, Max);
}
int main()
{
n = read(), m = read();
for(int i = 1, u, v; i < n; ++i) u = read(), v = read(), Cut::add_edge(u, v), Cut::add_edge(v, u);
Cut::dfs(1, 0), Cut::dfs2(1, 1);
for(int i = 1, u, v, z; i <= m; ++i) {
u = read(), v = read(), z = read();
int Lca = Cut::Get_Lca(u, v);
Hjt::Insert(root[u], 1, Max, z, 1);
Hjt::Insert(root[v], 1, Max, z, 1);
Hjt::Insert(root[Lca], 1, Max, z, -1);
if(Cut::fath[Lca]) Hjt::Insert(root[Cut::fath[Lca]], 1, Max, z, -1);
}
Dfs(1, 0);
for(int i = 1; i <= n; ++i) {
if(Hjt::cnt[ans[i]]) printf("%d\n", Hjt::pre[ans[i]]);
else puts("0");
}
return 0;
}
P5494【模板】线段树分裂
这道模板题合并和分裂都涉及到了,可以同时作为他们的模板来写。
- 操作 \(0\):先把中间的分裂出去,在合并两边的;
- 操作 \(1\):赤裸裸的合并!
- 操作 \(2\):单点修改;
- 操作 \(3\):查询一段值域内的数的个数;
- 操作 \(4\):查询第 \(k\) 小。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 2e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
int n, m;
int root[MAXN], rt = 1;
int read(){
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
namespace Hjt {
#define ls tr[now_].lson
#define rs tr[now_].rson
struct Tree {
int lson, rson, sum;
}tr[MAXN << 6];
int node_num, bin[MAXN << 6], top = 0;
int NewNode() { return top ? bin[top--] : ++ node_num; }
void Delete(int now_) { bin[++top] = now_, ls = rs = tr[now_].sum = 0; }
void Insert(int &now_, int l, int r, int pos, int val_) {
if(!now_) now_ = NewNode();
tr[now_].sum += val_;
if(l == r) return ;
int mid = (l + r) >> 1;
if(pos <= mid) Insert(ls, l, mid, pos, val_);
else Insert(rs, mid + 1, r, pos, val_);
}
int QuerySiz(int now_, int l, int r, int L, int R) {
if(L <= l && r <= R) return tr[now_].sum;
int mid = (l + r) >> 1;
int ans = 0;
if(mid >= L) ans += QuerySiz(ls, l, mid, L, R);
if(mid < R) ans += QuerySiz(rs, mid + 1, r, L, R);
return ans;
}
void Split(int now_, int &y, int k) { // 把 以 now_ 为根的权值线段树中的前 k 个值保留,其他的分裂出去
if(!now_) return ;
y = NewNode();
int siz = tr[ls].sum;
if(siz < k) Split(rs, tr[y].rson, k - siz); // 递归右子树分裂
else swap(rs, tr[y].rson); // 把右子树全部分裂出去
if(siz > k) Split(ls, tr[y].lson, k); // 递归左子树分裂
tr[y].sum = tr[now_].sum - k; // 分裂出一部分
tr[now_].sum = k; // 更新原来的
}
int Merge(int now_, int y, int l, int r) {
if(!now_ || !y) return now_ + y;
if(l == r) return now_;
tr[now_].sum += tr[y].sum;
int mid = (l + r) >> 1;
ls = Merge(ls, tr[y].lson, l, mid);
rs = Merge(rs, tr[y].rson, mid + 1, r);
Delete(y);
return now_;
}
int QueryKth(int now_, int l, int r, int k) {
if(l == r) return l;
int mid = (l + r) >> 1;
int siz = tr[ls].sum;
if(k <= siz) return QueryKth(ls, l, mid, k);
else return QueryKth(rs, mid + 1, r, k - siz);
}
}
signed main()
{
n = read(), m = read();
for(int i = 1, x; i <= n; ++i) Hjt::Insert(root[1], 1, n, i, read());
for(int i = 1, opt, p, x, y; i <= m; ++i) {
opt = read();
if(opt == 0) {
p = read(), x = read(), y = read();
int siz1 = Hjt::QuerySiz(root[p], 1, n, 1, y);
int siz2 = Hjt::QuerySiz(root[p], 1, n, x, y);
int tmp = 0;
Hjt::Split(root[p], root[++rt], siz1 - siz2);
Hjt::Split(root[rt], tmp, siz2);
root[p] = Hjt::Merge(root[p], tmp, 1, n);
} else if(opt == 1) {
p = read(), x = read();
root[p] = Hjt::Merge(root[p], root[x], 1, n);
} else if(opt == 2) {
p = read(), x = read(), y = read();
Hjt::Insert(root[p], 1, n, y, x);
} else if(opt == 3) {
p = read(), x = read(), y = read();
printf("%lld\n", Hjt::QuerySiz(root[p], 1, n, x, y));
} else {
p = read(), x = read();
if(Hjt::tr[root[p]].sum < x) puts("-1");
else printf("%lld\n", Hjt::QueryKth(root[p], 1, n, x));
}
}
return 0;
}
其他线段树合并例题
P3605 [USACO17JAN]Promotion Counting P
P3224 [HNOI2012]永无乡
P3521 [POI2011]ROT-Tree Rotations
P1600 [NOIP2016 提高组] 天天爱跑步