洛谷P1801 黑匣子 C++双堆解法

185 阅读5分钟

黑匣子问题题解

问题描述

Black Box(黑匣子) 是一种原始的数据库,能够存储一个整数数组以及一个特别的变量 ii。最开始时,Black Box 是空的,i=0i=0。Black Box 需要处理一系列的命令:

  • ADD(x):将元素 xx 放入 Black Box;
  • GET:将 ii11,然后输出 Black Box 中第 ii 小的数。

ii 小的数是指将 Black Box 中的所有数从小到大排序后的第 ii 个数。

给定两个整数数组:

  1. a1,a2,,ama_1,a_2,\cdots,a_m:需要依次放入 Black Box 的元素;
  2. u1,u2,,unu_1,u_2,\cdots,u_n:表示在第 uiu_i 个元素被放入 Black Box 后,执行一次 GET 操作。

我们的目标是根据命令序列,输出每次 GET 操作得到的数。

解题思路

这道题的核心在于如何高效地维护一个动态集合,使得在每次 GET 操作时能够快速获取当前集合中的第 ii 小的数。

关键挑战

  • 动态插入元素:需要高效地将新元素插入到集合中。
  • 获取第 ii 小的数:需要在动态集合中快速获取第 ii 小的数。

数据结构选择

为了满足上述需求,我们可以采用双堆(最大堆和最小堆)的数据结构:

  • 最大堆(maxHeap):存储当前集合中较小的一部分元素(比第 ii 小的数还要小的元素)。
  • 最小堆(minHeap):存储当前集合中较大的元素(第 ii 小的数以及比它更大的元素)。

通过调整两个堆中的元素,我们可以在对数时间内维护和获取第 ii 小的数。

算法设计

初始化

  • processed:表示已经处理的元素数量,初始为 0。
  • maxHeap:空的最大堆,用于存储较小的元素。
  • minHeap:空的最小堆,用于存储较大的元素。

操作流程

1. 添加元素 ADD(x)
  • 步骤 1:将新元素 x 放入 maxHeap
  • 步骤 2:将 maxHeap 中的最大元素移动到 minHeap,确保 minHeap 中始终包含当前的第 ii 小的元素以及比它更大的元素。
  • 步骤 3:更新 processed,表示已经处理的元素数量增加。
2. 获取第 ii 小的数 GET
  • 步骤 1:从 minHeap 中取出堆顶元素,即当前的第 ii 小的数。
  • 步骤 2:为了下一次查询的正确性,将该元素移动回 maxHeap

维护堆的性质

通过上述操作,我们确保了以下性质:

  • maxHeap 中的元素数量为 k-1,存储了当前集合中最小的 k-1 个元素。
  • minHeap 中的元素数量为 N - (k-1),存储了第 ii 小的元素以及更大的元素。

这样,在每次需要获取第 ii 小的数时,只需查看 minHeap 的堆顶元素即可。

代码实现

下面我们详细解析代码的实现。

1. 类 BlackBox 的定义

class BlackBox {
   private:
    int processed = 0;  // 已经处理的元素个数
    priority_queue<int> maxHeap;                             // 最大堆,存储较小的元素
    priority_queue<int, vector<int>, greater<int>> minHeap;  // 最小堆,存储较大的元素

   public:
    void add(int val);
    int get();
    int getProcessed() const;
};

2. 添加元素的实现

void BlackBox::add(int val) {
    processed++;
    maxHeap.push(val);             // 将新元素加入最大堆
    minHeap.push(maxHeap.top());   // 将最大堆的堆顶元素移动到最小堆
    maxHeap.pop();                 // 移除最大堆的堆顶元素
}
  • 解释
    • 新元素首先加入 maxHeap,保证了 maxHeap 中包含当前所有较小的元素。
    • 然后,将 maxHeap 中最大的元素(即堆顶)移动到 minHeap,使得 minHeap 包含当前的第 ii 小的元素及更大的元素。

3. 获取第 ii 小的数的实现

int BlackBox::get() {
    int result = minHeap.top();     // 获取最小堆的堆顶元素,即第 i 小的数
    maxHeap.push(result);           // 为了维护堆的平衡,将该元素移回最大堆
    minHeap.pop();                  // 从最小堆中移除该元素
    return result;
}
  • 解释
    • 直接获取 minHeap 的堆顶元素,即为当前的第 ii 小的数。
    • 为了确保下一次查询的正确性,我们需要将该元素移回 maxHeap,恢复堆的状态。

4. 主函数的实现

int main() {
    int m, n;
    cin >> m >> n;

    vector<int> a(m);
    for (int i = 0; i < m; i++) {
        cin >> a[i];
    }

    vector<int> u(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> u[i];
    }

    BlackBox box;

    for (int i = 1; i <= n; i++) {
        // 添加新元素直到达到查询的位置
        while (box.getProcessed() < u[i]) {
            box.add(a[box.getProcessed()]);
        }
        // 输出第 i 小的数
        cout << box.get() << "\n";
    }
}
  • 解释
    • 读取输入的元素序列 a 和查询序列 u
    • 对于每次查询 u[i],我们需要确保已经处理了足够的元素,即 processed >= u[i]
    • 调用 box.get() 获取当前的第 ii 小的数并输出。

复杂度分析

  • 单次插入操作 add()
    • 时间复杂度为 O(logk)O(\log k),其中 kk 为当前堆的大小。
  • 单次查询操作 get()
    • 时间复杂度为 O(logk)O(\log k)
  • 总体时间复杂度
    • 对于 mm 个插入和 nn 个查询,总时间复杂度为 O((m+n)logk)O((m + n) \log k)
    • 由于 kk 最多为 nn,因此总时间复杂度为 O((m+n)logn)O((m + n) \log n)

总结

通过使用最大堆和最小堆,我们成功地设计了一个高效的算法,能够在对数时间内维护动态集合并获取第 ii 小的数。

  • 优势

    • 适合处理动态数据,插入和查询效率高。
    • 代码简洁,易于理解和实现。
  • 注意事项

    • 需要正确维护堆之间的平衡,确保每次操作后堆的状态正确。
    • 在实现过程中,要注意堆的性质以及元素的移动方向。

示例演示

以示例输入为例:

7 4
3 1 -4 2 8 -1000 2
1 2 6 6
  • 第 1 次查询
    • 添加元素 3maxHeap:空,minHeap[3]
    • 输出 3
  • 第 2 次查询
    • 添加元素 1maxHeap[1]minHeap[3]
    • 输出 3
  • 第 3 次查询
    • 依次添加元素 -428-1000,调整堆的状态。
    • 输出 1
  • 第 4 次查询
    • 不需添加新元素,直接输出 2

结论

本题通过巧妙地利用堆的数据结构,实现了高效的插入和查询操作。通过维护两个堆,我们能够在每次 GET 操作时快速获取当前集合的第 ii 小的数,满足了题目的要求。