线段树动态开点,合并分裂三合一

前言:

本文的参考代码为权值线段树的合并/分裂,题目为 P5494 【模板】线段树分裂 - 洛谷

注意,阅读代码时请代入以下宏定义:

#define D(x) tre[x].dat
#define L(x) tre[x].l
#define R(x) tre[x].r

动态开点:

字面意思就是在需要查询的时候创建节点,避免了传统线段树每次都把空间挤满到 \(4N\) 的缺点,并且便于进行合并/分裂操作。

其本质逻辑也十分容易理解,我们只需记录一个 \(ncnt\) 作为总点数,在代表线段树结点的结构体里记录 \(l,r\) 即可。

但是,这样会在删除结点后,前面很多的结点没有重新用到,而后面的新结点一直从 \(ncnt\) 往后加,从而使空间被极大浪费。所以我们另外开一个 \(loop\) 数组记录已经删除的结点编号,在新开点的时候优先启用 \(loop\) 中的结点编号即可。

int ncnt = 0;
struct Tre
{
    int l, r;
    ll dat;
} tre[N << 2];
int loop[N], used = 0;
void newnode(int &id)
{
    id = used ? loop[used--] : ++ncnt;//优先使用已删除结点的编号
}
void del(int id)
{
    loop[++used] = id;//回收
    D(id) = L(id) = R(id) = 0;
}

void update(int id)
{
    D(id) = D(L(id)) + D(R(id));//维护区间和信息
}
void add(int &id, int l, int r, int x, ll va)
{
    if (!id)
        newnode(id);//没结点就开结点
    if (l == r)
    {
        D(id) += va;
        return;
    }
    int mid = (l + r) >> 1;
    if (x <= mid)
        add(L(id), l, mid, x, va);
    else
        add(R(id), mid + 1, r, x, va);
    update(id);
}

合并:

十分简单,就是从上到下把两个线段树的对应结点合并起来,遇见空结点就取另一棵树的非空结点,本质十分暴力。

int merge(int x, int y)//返回根结点的值
{
    if (!x || !y)
        return x + y;//有空结点时取非空结点
    D(x) += D(y);//合并信息
    L(x) = merge(L(x), L(y));
    R(x) = merge(R(x), R(y));
    //将左右儿子更改为合并后的结点
    del(y);//删除被合并掉的结点
    return x;
}//不难发现,在main()中调用时,函数的返回值即为合并后得到的树的根结点

分裂:

也一样容易理解。

设要保留维护的区间前 \(k\) 个,剩下的全部分裂出去,\(v\) 为左儿子结点维护的区间长度,那么可以分类讨论:

  • \(k>v\),直接递归分裂右子树,递归函数内 \(k\) 变为 \(k-v\)
  • \(k=v\),交换两棵树的右子树,即把右子树全部分裂出去。
  • \(k<v\),同上,右子树全分裂出去后递归分裂左子树,递归函数中 \(k\) 不变。
void split(int x, int &y, ll k)
{
    if (!x)
        return;//好理解,空结点不用分。
    newnode(y);//每次的y都是新结点
    
    ll v = D(L(x));
    if (k > v)
        split(R(x), R(y), k - v);
    else
        swap(R(x), R(y));
    if (k < v)
        split(L(x), L(y), k);
    
    D(y) = D(x) - k;
    D(x) = k;//更新信息
    return;
}

\(\text{Code:}\)

Click here

posted @ 2025-07-07 19:51  azaa414  阅读(15)  评论(1)    收藏  举报