线段树分裂合并
线段树分裂合并
我先接触的是线段树合并所以先讲线段树合并。
首先,用来合并的线段树必须是动态开点的。线段树合并所做的事就是合并两棵动态开点线段树的信息,对于两棵动态开点线段树,可能会存在一些公共节点,我们所要做的就是合并这些节点的信息,然后把其他节点的信息继承。理清思路之后,剩下的事就是。设初始信息的个数是n,值域是m,对于每一个初始信息一次这样的操作是\log m的,而每个信息只会被合并一次,所以一般的线段树合并是O(n \log m)的。非常好理解,直接上模板题:[POI2011]ROT-Tree Rotations。
这题就是对每一个叶节点点建一棵权值线段树;对于每个非叶节点,先计算左右儿子交换前后的逆序对数取较小值加入答案,再合并左右儿子的线段树成为这个结点的线段树。用权值线段树的目的是快速求逆序对,线段树合并的目的是快速合并左右儿子信息。
//written by newbiechd
#include <cstdio>
#include <cctype>
#define R register
#define I inline
#define B 1000000
#define L long long
using namespace std;
const int N = 200003;
char buf[B], *p1, *p2;
I char gc() { return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, B, stdin), p1 == p2) ? EOF : *p1++; }
I int rd() {
R int f = 0;
R char c = gc();
while (c < 48 || c > 57)
c = gc();
while (c > 47 && c < 58)
f = f * 10 + (c ^ 48), c = gc();
return f;
}
int rt[N], n, E, T;
L a, b, ans;
struct segtree { int v, p, q; }e[N << 5];
I L min(L x, L y) { return x < y ? x : y; }
void insert(int &k, int l, int r, int x) {
if (!k)
k = ++T;
++e[k].v;
if (l == r)
return ;
R int m = l + r >> 1;
if (x <= m)
insert(e[k].p, l, m, x);
else
insert(e[k].q, m + 1, r, x);
}
int merge(int k, int t, int l, int r) {
if (!k)
return t;
if (!t)
return k;
e[k].v += e[t].v;
if (l == r)
return k;
R int m = l + r >> 1;
a += 1ll * e[e[k].q].v * e[e[t].p].v, b += 1ll * e[e[t].q].v * e[e[k].p].v;
e[k].p = merge(e[k].p, e[t].p, l, m), e[k].q = merge(e[k].q, e[t].q, m + 1, r);
return k;
}
void solve(int &k) {
R int x = rd();
if (x) {
k = ++E, insert(rt[E], 1, n, x);
return ;
}
R int p, q;
solve(p), solve(q), a = b = 0, merge(rt[p], rt[q], 1, n), ans += min(a, b), k = p;
}
int main() {
R int k;
n = rd(), solve(k), printf("%lld", ans);
return 0;
}
放几道习题:
[Vani有约会]雨天的尾巴 题解(我当时貌似在这篇题解里又把动态开点线段树和线段树合并讲了一遍)
有时候,我们不仅需要把多棵线段树的信息合并,在有些情况下还需要把一颗线段树的信息分裂成几棵,这时候就要用到线段树分裂了。下面讲讲线段树分裂。
在形式上,线段树分裂可以看作线段树合并的逆操作,事实上线段树分裂并不是还原线段树合并之前的信息,结合下面的例题理解:[HEOI2016/TJOI2016]排序,这题是要求多次取一个全排列的一段子串升序或降序排序,最后求一个位置的权值。
有一种二分答案然后把原问题转化为01序列排序的方法,复杂度O(n (log n) ^ 2),这里不做过多介绍。
线段树分裂的方法可以支持最后查询一个区间,下面就按照最后查询l到r的一个区间来介绍。
首先考虑对一个排列可以桶排序,我们想象每次把题目要修改的位置的权值全部放到桶中就完成了排序,假设一开始每个位置有一个桶,放的是这个位置的权值:这时要取出一段区间排序,就要想办法把这段区间的桶合并成一个,顺序/倒序遍历的结果就是升序/降序排列的结果;有时候需要排序的区间的端点在某些已经排好序了的区间中,即现在需要排序的某些元素在有其他元素的桶中,这时就要我们想办法取出这些元素,即把这个桶分裂;最后查询的时候,我们就取出l到r这段区间(这道题就是q这个点)内的桶合并,同样的,对于含有端点的桶,如果还含有不在这段区间内的元素,就要把这个桶分裂。
我们觉得桶没法快速支持合并和分裂操作,所以我们就用动态开点的权值线段树来实现。其实把上面这段话中的“桶”全部换成“权值线段树”这道题就做完了。
我们已经会线段树合并了,但是还不会线段树分裂。现在看看这题,事实上我们只需要取出一段已经排好序的序列的前k大/前k小,并把这些部分放到一棵新的权值线段树上。那么我们直接在线段树上做一个取出前k大/前k小的操作就行了,唯一的不同就是把这些部分取出来时另开一颗线段树记录这些信息,想法非常自然,可以结合代码理解。
UPD:以前的写法太憨了,前来更新一波代码。
// Written by newbiechd
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <set>
#include <vector>
const int BUFFER_SIZE = 1 << 25 | 1;
struct InputOutputStream {
char ibuf[BUFFER_SIZE], obuf[BUFFER_SIZE], *s, *oh;
InputOutputStream() : s(ibuf), oh(obuf) {
#ifndef ONLINE_JUDGE
freopen("a.in", "r", stdin);
// freopen("a.out", "w", stdout);
#endif
ibuf[fread(ibuf, 1, BUFFER_SIZE, stdin)] = '\0';
}
~InputOutputStream() { fwrite(obuf, 1, oh - obuf, stdout); }
template <typename T>
inline InputOutputStream &operator>>(T &x) {
while (!isdigit(*s)) ++s;
for (x = 0; isdigit(*s); ++s)
x = x * 10 + (*s ^ '0');
return *this;
}
template <typename T>
inline InputOutputStream &operator<<(T x) {
static char buf[23];
register char *top = buf;
if (x) {
for (register int t; x; )
t = x / 10, *top++ = x - t * 10 + 48, x = t;
while (top != buf)
*oh++ = *--top;
}
else
*oh++ = '0';
*oh++ = ' ';
return *this;
}
inline void endl() { *oh++ = '\n'; }
}IO;
template <typename lhs, typename rhs>
inline lhs min(const lhs &x, const rhs &y) { return x < y ? x : y; }
template <typename lhs, typename rhs>
inline lhs max(const lhs &x, const rhs &y) { return x > y ? x : y; }
const int maxN = 100003;
int n, m;
int rt[maxN];
bool rise[maxN];
const int maxT = 5000003;
#define mid ((l + r) >> 1)
struct SegmentTree {
int lSon[maxT], rSon[maxT], siz[maxT], cntNode;
void insert(int &k, int l, int r, int x) {
if (!k)
k = ++cntNode;
++siz[k];
if (l == r)
return ;
x <= mid ? insert(lSon[k], l, mid, x) : insert(rSon[k], mid + 1, r, x);
}
int query(int k, int l = 1, int r = n) {
if (l == r)
return l;
return siz[lSon[k]] ? query(lSon[k], l, mid) : query(rSon[k], mid + 1, r);
}
int merge(int k, int t) {
if (!k || !t)
return k | t;
siz[k] += siz[t];
lSon[k] = merge(lSon[k], lSon[t]);
rSon[k] = merge(rSon[k], rSon[t]);
return k;
}
void split(int &k, int t, int d, bool rise) {
if (d == siz[t])
return ;
k = ++cntNode, siz[k] = siz[t] - d, siz[t] = d;
if (rise) {
if (d <= siz[lSon[t]])
split(lSon[k], lSon[t], d, rise), rSon[k] = rSon[t], rSon[t] = 0;
else
split(rSon[k], rSon[t], d - siz[lSon[t]], rise);
}
else {
if (d <= siz[rSon[t]])
split(rSon[k], rSon[t], d, rise), lSon[k] = lSon[t], lSon[t] = 0;
else
split(lSon[k], lSon[t], d - siz[rSon[t]], rise);
}
}
}tree;
#undef mid
std::set<int> st;
inline std::set<int>::iterator split(int x) {
std::set<int>::iterator it = st.lower_bound(x);
if (*it == x)
return it;
--it, tree.split(rt[x], rt[*it], x - *it, rise[x] = rise[*it]);
return st.insert(x).first;
}
int main() {
IO >> n >> m;
for (int i = 1; i <= n; ++i) {
int x;
IO >> x, tree.insert(rt[i], 1, n, x), st.insert(i);
}
st.insert(n + 1);
for (int i = 1; i <= m; ++i) {
int op, l, r;
IO >> op >> l >> r, op ^= 1;
std::set<int>::iterator L = split(l), R = split(r + 1), it;
for (it = ++L; it != R; ++it)
tree.merge(rt[l], rt[*it]);
rise[l] = op, st.erase(L, R);
}
int Q;
IO >> Q, split(Q), split(Q + 1), IO << tree.query(rt[Q]);
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步