返回

线段树(动态开点)在解决 LeetCode 699 题中的妙用

后端

当算法问题与数据结构碰撞时,往往能迸发出精彩的火花。在 LeetCode 699 题《掉落的方块》中,线段树(动态开点)的巧妙运用就是如此。

699 题

给你一个大小为 n x n 的方格网,网格中每个格点要么为空,要么有一个方块。每个方块从网格的上方边缘掉落,如果它落到空格子中,则会停留在该格子中;否则,它会压在下方第一个不为空的格子上。

动态开点线段树的运用

要解决这个问题,我们需要一种数据结构来维护网格中方块的状态。线段树是一种完美的选择,因为它允许我们高效地更新和查询区间信息。

然而,在该问题中,网格的大小是动态的。方块的掉落会改变网格中非空的方格数,因此我们需要使用动态开点线段树。

动态开点线段树与普通线段树的不同之处在于,它允许在需要时动态创建新节点。这使我们能够在 O(log n) 时间内高效处理网格大小的变化。

两种动态开点线段树实现方式

动态开点线段树有两种常见的实现方式:

1. 标记传播法

此方法通过标记节点来表示需要创建的新节点。当需要更新包含标记的区间时,我们会递归地将标记向下传递,直到创建所有必要的节点。

2. 惰性传播法

此方法通过延迟更新来表示需要创建的新节点。当需要更新包含延迟更新的区间时,我们会立即创建所有必要的节点,然后执行实际的更新。

应用场景和示例代码

动态开点线段树在解决 LeetCode 699 题时,可以用来维护网格中每个列中非空方格的数量。当方块掉落时,我们可以使用线段树高效地更新相应的区间信息,从而得到方块的最终位置。

以下是使用标记传播法实现的动态开点线段树的部分示例代码:

struct SegmentTree {
    vector<int> tree;
    vector<int> lazy;
    int size;

    void init(int n) {
        size = 1 << (int)ceil(log2(n));
        tree.resize(2 * size);
        lazy.resize(2 * size);
    }

    void update_range(int l, int r, int val) {
        update_range(l, r, val, 1, 0, size - 1);
    }

    void update_range(int l, int r, int val, int node, int nl, int nr) {
        propagate(node, nl, nr);
        if (l <= nl && nr <= r) {
            lazy[node] = val;
            propagate(node, nl, nr);
            return;
        }
        if (r < nl || l > nr) {
            return;
        }
        int mid = (nl + nr) / 2;
        update_range(l, r, val, 2 * node, nl, mid);
        update_range(l, r, val, 2 * node + 1, mid + 1, nr);
        tree[node] = merge(tree[2 * node], tree[2 * node + 1]);
    }

    int query(int l, int r) {
        return query(l, r, 1, 0, size - 1);
    }

    int query(int l, int r, int node, int nl, int nr) {
        propagate(node, nl, nr);
        if (l <= nl && nr <= r) {
            return tree[node];
        }
        if (r < nl || l > nr) {
            return 0;
        }
        int mid = (nl + nr) / 2;
        return merge(query(l, r, 2 * node, nl, mid), query(l, r, 2 * node + 1, mid + 1, nr));
    }

private:
    void propagate(int node, int nl, int nr) {
        if (lazy[node] != 0) {
            tree[node] = lazy[node] * (nr - nl + 1);
            if (nl != nr) {
                lazy[2 * node] = lazy[node];
                lazy[2 * node + 1] = lazy[node];
            }
            lazy[node] = 0;
        }
    }

    int merge(int a, int b) {
        return a + b;
    }
};

总结

线段树(动态开点)在解决 LeetCode 699 题中发挥了至关重要的作用。其高效地处理动态网格和区间更新的能力,使我们能够优雅地解决这个问题。无论是标记传播法还是惰性传播法,动态开点线段树都是算法工具箱中宝贵的工具。