黑匣子问题题解
问题描述
Black Box(黑匣子) 是一种原始的数据库,能够存储一个整数数组以及一个特别的变量 。最开始时,Black Box 是空的,。Black Box 需要处理一系列的命令:
ADD(x):将元素 放入 Black Box;GET:将 加 ,然后输出 Black Box 中第 小的数。
第 小的数是指将 Black Box 中的所有数从小到大排序后的第 个数。
给定两个整数数组:
- :需要依次放入 Black Box 的元素;
- :表示在第 个元素被放入 Black Box 后,执行一次
GET操作。
我们的目标是根据命令序列,输出每次 GET 操作得到的数。
解题思路
这道题的核心在于如何高效地维护一个动态集合,使得在每次 GET 操作时能够快速获取当前集合中的第 小的数。
关键挑战
- 动态插入元素:需要高效地将新元素插入到集合中。
- 获取第 小的数:需要在动态集合中快速获取第 小的数。
数据结构选择
为了满足上述需求,我们可以采用双堆(最大堆和最小堆)的数据结构:
- 最大堆(maxHeap):存储当前集合中较小的一部分元素(比第 小的数还要小的元素)。
- 最小堆(minHeap):存储当前集合中较大的元素(第 小的数以及比它更大的元素)。
通过调整两个堆中的元素,我们可以在对数时间内维护和获取第 小的数。
算法设计
初始化
- processed:表示已经处理的元素数量,初始为 0。
- maxHeap:空的最大堆,用于存储较小的元素。
- minHeap:空的最小堆,用于存储较大的元素。
操作流程
1. 添加元素 ADD(x)
- 步骤 1:将新元素
x放入maxHeap。 - 步骤 2:将
maxHeap中的最大元素移动到minHeap,确保minHeap中始终包含当前的第 小的元素以及比它更大的元素。 - 步骤 3:更新
processed,表示已经处理的元素数量增加。
2. 获取第 小的数 GET
- 步骤 1:从
minHeap中取出堆顶元素,即当前的第 小的数。 - 步骤 2:为了下一次查询的正确性,将该元素移动回
maxHeap。
维护堆的性质
通过上述操作,我们确保了以下性质:
- maxHeap 中的元素数量为
k-1,存储了当前集合中最小的k-1个元素。 - minHeap 中的元素数量为
N - (k-1),存储了第 小的元素以及更大的元素。
这样,在每次需要获取第 小的数时,只需查看 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包含当前的第 小的元素及更大的元素。
- 新元素首先加入
3. 获取第 小的数的实现
int BlackBox::get() {
int result = minHeap.top(); // 获取最小堆的堆顶元素,即第 i 小的数
maxHeap.push(result); // 为了维护堆的平衡,将该元素移回最大堆
minHeap.pop(); // 从最小堆中移除该元素
return result;
}
- 解释:
- 直接获取
minHeap的堆顶元素,即为当前的第 小的数。 - 为了确保下一次查询的正确性,我们需要将该元素移回
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()获取当前的第 小的数并输出。
- 读取输入的元素序列
复杂度分析
- 单次插入操作
add():- 时间复杂度为 ,其中 为当前堆的大小。
- 单次查询操作
get():- 时间复杂度为 。
- 总体时间复杂度:
- 对于 个插入和 个查询,总时间复杂度为 。
- 由于 最多为 ,因此总时间复杂度为 。
总结
通过使用最大堆和最小堆,我们成功地设计了一个高效的算法,能够在对数时间内维护动态集合并获取第 小的数。
-
优势:
- 适合处理动态数据,插入和查询效率高。
- 代码简洁,易于理解和实现。
-
注意事项:
- 需要正确维护堆之间的平衡,确保每次操作后堆的状态正确。
- 在实现过程中,要注意堆的性质以及元素的移动方向。
示例演示
以示例输入为例:
7 4
3 1 -4 2 8 -1000 2
1 2 6 6
- 第 1 次查询:
- 添加元素
3,maxHeap:空,minHeap:[3]。 - 输出
3。
- 添加元素
- 第 2 次查询:
- 添加元素
1,maxHeap:[1],minHeap:[3]。 - 输出
3。
- 添加元素
- 第 3 次查询:
- 依次添加元素
-4、2、8、-1000,调整堆的状态。 - 输出
1。
- 依次添加元素
- 第 4 次查询:
- 不需添加新元素,直接输出
2。
- 不需添加新元素,直接输出
结论
本题通过巧妙地利用堆的数据结构,实现了高效的插入和查询操作。通过维护两个堆,我们能够在每次 GET 操作时快速获取当前集合的第 小的数,满足了题目的要求。