线段树分裂合并
Posted cj-chd
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线段树分裂合并相关的知识,希望对你有一定的参考价值。
线段树分裂合并
我先接触的是线段树合并所以先讲线段树合并。
首先,用来合并的线段树必须是动态开点的。线段树合并所做的事就是合并两棵动态开点线段树的信息,对于两棵动态开点线段树,可能会存在一些公共节点,我们所要做的就是合并这些节点的信息,然后把其他节点的信息继承。理清思路之后,剩下的事就是。设初始信息的个数是(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)小的操作就行了,唯一的不同就是把这些部分取出来时另开一颗线段树记录这些信息,想法非常自然,可以结合代码理解。
什么?你不知道怎么判断一个点是不是在已经排好序的区间里?
那你可能要向全国的珂学家谢罪了:直接开个set,里面的结点是区间,剩下的不用我说了吧。
具体实现的时候写了个内存回收。
//written by newbiechd
#include <cstdio>
#include <cctype>
#include <set>
#define R register
#define I inline
#define B 1000000
using namespace std;
const int N = 100003;
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 sta[N << 5], top, n;
struct segtree { int p, q, s; }e[N << 5];
struct node { int l, r, k, t; };
set <node> s;
I int operator < (node x, node y) { return x.r ^ y.r ? x.r < y.r : x.l < y.l; }
I int newnode() {
R int t = sta[top--];
e[t] = (segtree){0, 0, 0};
return t;
}
void insert(int &k, int l, int r, int x) {
if (!k)
k = newnode();
++e[k].s;
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);
}
void split(int &k, int t, int l, int r, int x) {
if (!x)
return ;
if (!k)
k = newnode();
e[k].s += x, e[t].s -= x;
if (l == r)
return ;
R int m = l + r >> 1, y = e[e[t].p].s;
if (x < y)
split(e[k].p, e[t].p, l, m, x);
else {
e[k].p = e[t].p, e[t].p = 0;
split(e[k].q, e[t].q, m + 1, r, x - y);
}
}
int merge(int k, int t) {
if (!k || !t)
return k | t;
e[k].p = merge(e[k].p, e[t].p), e[k].q = merge(e[k].q, e[t].q);
e[k].s += e[t].s, sta[++top] = t;
return k;
}
int kth(int k, int l, int r, int x) {
if (l == r)
return l;
R int m = l + r >> 1;
if (e[e[k].p].s >= x)
return kth(e[k].p, l, m, x);
else
return kth(e[k].q, m + 1, r, x - e[e[k].p].s);
}
I int nsplit(int l, int r) {
set <node>::iterator it = s.lower_bound((node){0, l, 0, 0});
if ((*it).l ^ l) {
node t = *it;
R int k = 0;
s.erase(it);
if (t.t) {
split(k, t.k, 1, n, t.r - l + 1);
s.insert((node){t.l, l - 1, t.k, 1});
s.insert((node){l, t.r, k, 1});
}
else {
split(k, t.k, 1, n, l - t.l);
s.insert((node){t.l, l - 1, k, 0});
s.insert((node){l, t.r, t.k, 0});
}
}
it = s.lower_bound((node){0, r, 0, 0});
if ((*it).r ^ r) {
node t = *it;
R int k = 0;
s.erase(it);
if (t.t) {
split(k, t.k, 1, n, t.r - r);
s.insert((node){t.l, r, t.k, 1});
s.insert((node){r + 1, t.r, k, 1});
}
else {
split(k, t.k, 1, n, r - t.l + 1);
s.insert((node){t.l, r, k, 0});
s.insert((node){r + 1, t.r, t.k, 0});
}
}
R int o = 0;
while (1) {
it = s.lower_bound((node){0, l, 0, 0});
if (it == s.end() || (*it).l > r)
break;
node t = *it;
s.erase(it);
o = merge(o, t.k);
}
return o;
}
int main() {
R int m, i, x, y, opt, Q;
n = rd(), m = rd();
for (i = (N << 5) - 1; i; --i)
sta[++top] = i;
for (i = 1; i <= n; ++i)
x = rd(), y = 0, insert(y, 1, n, x), s.insert((node){i, i, y, 0});
while (m--) {
opt = rd(), x = rd(), y = rd(), i = nsplit(x, y);
s.insert((node){x, y, i, opt});
}
Q = rd(), i = nsplit(Q, Q), printf("%d", kth(i, 1, n, 1));
return 0;
}
以上是关于线段树分裂合并的主要内容,如果未能解决你的问题,请参考以下文章