385.小c的mex查询 | 豆包MarsCode AI刷题

32 阅读5分钟

好的,下面提供一份更加详细的解题思路及分析,特别是对于线段树的构建、更新、查询过程的具体细节,希望能完整呈现这个问题的解决方案。

问题背景

在这道题目中,我们需要处理若干个区间更新操作,以及在每次更新后查询集合的 mex(最小未出现的非负整数)。具体而言,在给定的范围 [l, r] 内,将所有整数视为在一个集合中添加,并在每个操作后返回当前集合的 mex 值。

mex 的定义

mex 是一个集合的最小非负整数值,简单来说,mex 是存在于非负整数集合中,最小的那个没有出现在给定集合中的数字。例如:

  • 对于集合 {0, 1, 2}mex3
  • 对于集合 {1, 2, 3}mex0
  • 对于空集合 {}mex0

数据结构选择

如果我们用一个数组标记已经出现的数字,那么用线段树维护整个数组,每次修改等于给整个区间的位置加1,此时的mex即为数组中第一个值为0的位置。

由于我们需要频繁地进行区间更新并查询 mex,使用线段树(Segment Tree)是一个合适的选择。线段树能够在 O(log n) 的时间复杂度内处理更新和查询,其灵活的特性支持我们管理动态变化的区间数据。

算法步骤

1. 初始化线段树

我们初始化一个线段树类 segtree,其构造函数接受一个参数 n,用于确定我们处理的最大值范围(这里设定为10000,以覆盖可能的需求)。线段树需要两个主要的数组:

  • tree:用于存储线段树节点的信息。
  • lazy:用于实现懒惰更新,以减少频繁的重复计算。
struct segtree {
    vector<int> tree; // 存储每个节点的mex值
    vector<int> lazy; // 懒惰更新标记
    int n;

    segtree(int n) {
        tree.resize((n + 10) << 2, 0); // 分配树的空间
        lazy.resize((n + 10) << 2, 0); // 分配懒惰标记的空间
        this->n = n;
    }
};

2. 更新操作

更新操作需要处理的是对区间 [l, r] 中所有整数的加入。这意味着在这个区间内,每个节点都要在其对应的 tree 中反映这些数的增加。我们需要实现几个辅助方法。

  • 懒惰传播(Push Down):当我们更新某个节点时,如果它有懒惰标记,我们需要将这个标记传播到其子节点。这是为了确保所有子节点维护正确的数据。懒惰标记可以帮助我们避免在每次更新时对所有涉及的节点进行显式更新操作,从而节省时间。

  • 更新(Update):这个方法更新线段树节点,增加对应区间的数字计数,并适当更新懒惰标记。

void pushdown(int id, int l, int r) {
    if (!lazy[id]) return; // 如果没有懒惰更新,返回
    int mid = (l + r) >> 1; // 找到中点
    tree[id << 1] += lazy[id]; // 左子树懒惰更新
    tree[id << 1 | 1] += lazy[id]; // 右子树懒惰更新
    lazy[id << 1] += lazy[id]; // 更新左子树的懒惰标记
    lazy[id << 1 | 1] += lazy[id]; // 更新右子树的懒惰标记
    lazy[id] = 0; // 清空当前节点的懒惰标记
}

void update(int id, int l, int r, int s, int t) {
    if (s <= l && r <= t) { // 如果当前区间完全在更新范围内
        tree[id]++; // 更新树节点
        lazy[id]++; // 更新懒惰标记
        return;
    }
    pushdown(id, l, r); // 传播懒惰操作
    int mid = (l + r) >> 1; // 找到中点
    if (s <= mid) update(id << 1, l, mid, s, t); // 更新左子树
    if (t > mid) update(id << 1 | 1, mid + 1, r, s, t); // 更新右子树
    pushup(id); // 从下往上更新当前树节点的值
}

3. 查询操作

查询操作用于获取当前 mex 值。我们可以从根节点开始检查,利用 findfirst 函数从顶层遍历至底层,找到第一个未出现的非负整数(即 mex)。

int findfirst(int id, int l, int r) {
    if (l == r) return l; // 如果区间只剩下一个元素,返回这个元素
    int mid = (l + r) >> 1;
    if (tree[id << 1] == 0) return findfirst(id << 1, l, mid); // 左子树的mex
    else return findfirst(id << 1 | 1, mid + 1, r); // 右子树的mex
}

int query() {
    return findfirst(1, 0, n); // 从根节点开始查询
}

4. 主函数和整合操作

solution 函数内,我们处理多个查询。每次处理更新时,调用 update 方法更新区间,然后使用 query 方法获取当前的 mex 值,最后将结果存入数组中。

vector<int> solution(int q, vector<vector<int>> queries) {
    segtree t(10000); // 初始化线段树
    vector<int> ans(q); // 存储每次查询结果

    for (int i = 0; i < q; i++) {
        t.update(queries[i][0], queries[i][1]); // 更新当前区间
        ans[i] = t.query(); // 查询当前区间的mex
    }
    return ans; // 返回结果数组
}

复杂度分析

  • 时间复杂度

    • 每次更新操作需要 O(log n),因为我们使用懒惰更新方法来优化。
    • 每次查询 mex 的时间复杂度同样为 O(log n)
    • 总体复杂度为 O(q log n),其中 q 为查询次数。
  • 空间复杂度

    • 线段树的空间复杂度为 O(n),加上辅助的懒惰标记数组,整体空间复杂度为 O(n)

示例解析

假设我们的输入是:

solution(4, {{1, 3}, {7, 8}, {0, 5}, {3, 6}});
  1. 第一次更新 [1, 3]:此时集合中有 {1, 2, 3}mex0
  2. 第二次更新 [7, 8]:此时集合中有 {1, 2, 3, 7, 8}mex 仍然是 0
  3. 第三次更新 [0, 5]:此时集合中有 {0, 1, 2, 3, 4, 5, 7, 8}mex 现在变为 6
  4. 第四次更新 [3, 6]:此时集合中有 {0, 1, 2, 3, 4, 5, 6, 7, 8},最终 mex9

最终输出为 {0, 0, 6, 9}

总结

通过使用线段树管理动态区间数据,我们能有效处理更新和查询操作,确保在每一步都能快速找到集合的 mex 值。这种灵活的数据结构不仅适用于本题,还对其他类似的动态区间查询问题提供了解决思路。掌握这类算法对于提高解决竞赛问题的能力有很大帮助。