动态开点线段树

前言

  • 本文要求读者对线段树较为熟悉,并能理解左闭右开的区间。
  • 动态开点线段树的写法非常多,读者应选择自己喜欢的方式去实现。

理解动态开点

动态开点的含义就是,不使用就不开空间,一使用就开空间。

为什么会存在不使用的情况呢?

因为线段树的一次操作只会使用到很小一部分结点——
具体地,假设整棵线段树的区间长度为

,那么单次操作只需要

个结点的信息。

下面以修改操作为例来分析:

单点修改

单点修改就是修改某一个叶子结点的值,而后更新它所有祖先结点的信息(因为都包含这个单点),例:

修改 [0, 8) 线段树中的单点 3

因此需要更新信息的结点为:从根结点到该叶子结点这条路径上所有的结点,最多

个结点。

区间修改

区间修改需要更新的结点数量可以逐层来分析,这里给出一个实例便于理解:

修改 [0, 8) 线段树中的 [1, 7) 区间

线段树每一层中需要更新的结点:

  • 要么包含左端点,这种每层最多 1 个,如
  • 要么包含右端点,这种每层最多 1 个,如
  • 要么在左右端点之间,如
  • ,这种每层最多 2 个,否则必能合并,如

    因此每层最多更新四个结点,而线段树的层数为

    ,因此区间修改需要更新的结点数量最多为

    小结

    因为单次操作只需要操作

    个结点,所以在操作的同时,将要用的结点的空间开出来就好了,从而对于单次操作,其最坏空间复杂度与时间复杂度相同。

    可见动态开点线段树本质上依然是一棵完整的线段树,只是没使用过的结点不占空间而已。

    如何动态开点

    其实只要会写普通的线段树,稍作修改就是动态开点的线段树啦。

    这里引入力扣上一道典范模板题:掉落的方块 (Falling Squares) - 力扣 (LeetCode)

    简要题意:
    从高空依次丢若干个正方形的方块,每个方块底边与

    轴平行竖直下落。
    当方块碰到地面或者碰到其他方块时停下(左右边缘的触碰不能算),停下之后才会丢出下一个方块。
    每次丢下一个方块并停下来后,你需要求出目前方块堆叠出的最高高度。

    样例示意图(原题图片)

    样例输入:

    样例输出:

    样例解释:
    共有 3 个方块,如第一个方块

    表示左边缘坐标 left = 1,边长 side_length = 2 的方块。
    第 1 个方块掉落后,最高的堆叠由方块 1 组成,堆叠的最高高度为 2 。
    第 2 个方块掉落后,最高的堆叠由方块 1 和 2 组成,堆叠的最高高度为 5 。
    第 3 个方块掉落后,最高的堆叠仍然由方块 1 和 2 组成,堆叠的最高高度为 5 。
    因此,返回 [2, 5, 5] 作为答案。

    怎么做呢?


    每个方块都有个区间 [left, left + side_length),那么该区间最大高度 h 就是停止时下边缘的高度。
    那么当这个方块落下来之后,这个区间的高度就会变成 h + side_length

    每次丢下方块后的最高高度就是整个区间的最大值。

    也就是区间赋值+区间最值啦。

    首先定义结点:
    动态开点的左、右孩子下标无法根据当前下标计算,所以用变量 lc, rc 分别记录左、右孩子的下标。
    此外跟普通线段树类似,最高高度用 height 来记录,区间赋值需要懒标记 lazy

    using int64 = long long;
    struct node {
        int lc = -1;
        int rc = -1;
        int64 height = 0;
        int64 lazy = 0;
    } tr[TREE_SIZE];
    

    这里用 -1 表示不存在(还没有为其分配空间)。

    那么线段树的空间大小 TREE_SIZE 应该开多大呢?

    由前面的分析可知,每次区间操作最坏情况需要开的空间是

    ,其中

    是区间长度。而又因每个方块只操作一个区间,因此最坏情况下需要开的空间的一个上界是:方块数量乘以单次操作最坏情况下需要开的空间。

    constexpr static int N = (int) 1e8 + (int) 1e6 + 1; // 区间长度
    constexpr static int logN = 27;                     // log2(N)上取整
    constexpr static int M = 1000;                      // 方块个数(也是区间操作的次数)
    constexpr static int TREE_SIZE = M * 4 * logN + 10;      // 树的大小
    

    由于在结点要用时才会给它分配空间,因此定义函数 check(int &u) 来完成这一操作:

    void check(int &u) {
        if (u == -1) {  // 如果还没有为结点 u 分配空间
            u = ++ tot; // 为之分配一个下标 tot
            tr[u].lc = tr[u].rc = -1;      // 左右孩子仍待分配
            tr[u].height = tr[u].lazy = 0; // 初始化
        }
    }
    

    注意到 check 函数传的是引用类型 int &,这意味着函数内可以修改传过来的变量,这也意味着如果某个函数需要在其内部调用 check,那就也要传引用。

    其他操作与普通的线段树十分类似,只要在有需要的时候调用 check 函数分配空间即可。

    有哪些地方需要呢,这里给出一个参考答案(可能并不完整):

    在递归调用子树、下传懒标记、为结点赋值时需要调用 check 函数。

    // down(u, h) : 将结点 u 所表示区间的高度赋值为 h
    void down(int &u, int64 h) {
        check(u);
        tr[u].height = h;
        tr[u].lazy = h;
    }
    
    // 将区间 [lo, hi) 赋值为 val
    void range_assign(int &u, int l, int r, int lo, int hi, int64 val) {
        check(u);
        // ...和普通线段树完全一样,故省略...
    }
    
    // 求区间 [lo, hi) 的最大值
    int64 range_max(int &u, int l, int r, int lo, int hi) {
        check(u);
        // ...和普通线段树完全一样,故省略...
    }
    

    最后,push_up 时需要判断孩子结点是否存在:

    void push_up(int u) {
        if (tr[u].lc != -1) {
            tr[u].height = std::max(tr[u].height, tr[tr[u].lc].height);
        }
        if (tr[u].rc != -1) {
            tr[u].height = std::max(tr[u].height, tr[tr[u].rc].height);
        }
    }
    

    最后的建议是,根结点的下标也定义一个变量,初始化为 -1 表示未分配空间。

    然后就像普通线段树那样直接调用就好啦——

    int root = -1;   // 注:调用递归函数需要使用变量 root,不能直接用常量,因为常量不能作为 int& 引用传入
    std::vector<int> ans;
    for (auto &square : positions) {
        int left = square[0];
        int side_length = square[1];
        int right = left + side_length;
        int64 h = range_max(root, 0, N, left, right);
        range_assign(root, 0, N, left, right, h + side_length);
        ans.push_back(tr[root].height);
    }
    

    完整代码

    class Solution {
    public:
    
        constexpr static int N = (int) 1e8 + (int) 1e6 + 1;
        constexpr static int logN = 27;
        constexpr static int M = 1000;
        constexpr static int TREE_SIZE = M * 4 * logN + 10;
    
        using int64 = long long;
    
        struct node {
            int lc = -1;
            int rc = -1;
            int64 height = 0;
            int64 lazy = 0;
        } tr[TREE_SIZE];
    
        int tot = 0;
    
        void check(int &u) {
            if (u == -1) {
                u = ++ tot;
                tr[u].lc = tr[u].rc = -1;
                tr[u].height = tr[u].lazy = 0;
            }
        }
    
        void down(int &u, int64 h) {
            check(u);
            tr[u].height = h;
            tr[u].lazy = h;
        }
    
        void push_down(int u) {
            if (tr[u].lazy != 0) {
                int h = tr[u].lazy;
                tr[u].lazy = 0;
                down(tr[u].lc, h);
                down(tr[u].rc, h);
            }
        }
    
        void push_up(int u) {
            if (tr[u].lc != -1) {
                tr[u].height = std::max(tr[u].height, tr[tr[u].lc].height);
            }
            if (tr[u].rc != -1) {
                tr[u].height = std::max(tr[u].height, tr[tr[u].rc].height);
            }
        }
    
        void range_assign(int &u, int l, int r, int lo, int hi, int64 val) {
            check(u);
            if (lo <= l and r <= hi) {
                down(u, val);
            } else if (r <= lo or hi <= l) {
                // pass
            } else {
                push_down(u);
                int mid = l + (r - l) / 2;
                range_assign(tr[u].lc, l, mid, lo, hi, val);
                range_assign(tr[u].rc, mid, r, lo, hi, val);
                push_up(u);
            }
        }
    
        int64 range_max(int &u, int l, int r, int lo, int hi) {
            check(u);
            if (lo <= l and r <= hi) {
                return tr[u].height;
            } else if (r <= lo or hi <= l) {
                return 0ll;
            } else {
                push_down(u);
                int mid = l + (r - l) / 2;
                int64 res = std::max(range_max(tr[u].lc, l, mid, lo, hi), range_max(tr[u].rc, mid, r, lo, hi));
                push_up(u);
                return res;
            }
        }
    
        vector<int> fallingSquares(vector<vector<int>>& positions) {
            vector<int> ans;
            int root = -1;   // 注:调用递归函数需要使用 root,不能直接用 0,因为 0 不能作为 int& 引用传入
            for (auto &square : positions) {
                int left = square[0];
                int side_length = square[1];
                int right = left + side_length;
                int64 h = range_max(root, 0, N, left, right);
                range_assign(root, 0, N, left, right, h + side_length);
                ans.push_back(tr[root].height);
            }
            return ans;
        }
    };
    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值