https://niumacode.com/training/121/problem/P1656
需要注意
在提取操作中检查优先级是否为最新,是为了解决“堆中存在旧版本数据”的问题,确保提取的是当前经验池中真实有效的高优先级经验。这一设计源于优先级队列(堆)的特性限制,具体原因如下:
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <sstream>
#include <algorithm>
using namespace std;
struct Experience {
int priority;
int id;
// 重载比较运算符,定义堆的排序规则
bool operator<(const Experience& other) const {
if (priority != other.priority) {
return priority < other.priority; // 优先级高的在前
}
return id > other.id; // ID小的在前
}
};
int main() {
int N;
cin >> N;
if (cin.fail() || N < 1 || N > 1e5) {
cout << "null" << endl;
return 0;
}
cin.ignore(); // 消耗掉换行符
vector<string> operations;
string line;
for (int i = 0; i < N; ++i) {
if (!getline(cin, line)) {
cout << "null" << endl;
return 0;
}
operations.push_back(line);
}
// 如果还有剩余输入,说明操作数不匹配
if (getline(cin, line) && !line.empty()) {
cout << "null" << endl;
return 0;
}
priority_queue<Experience> heap;
unordered_map<int, int> idToPriority; // 存储ID到优先级的映射
unordered_set<int> extracted; // 记录已提取的ID
bool hasExtract = false;
for (const string& op : operations) {
stringstream ss(op);
char type;
ss >> type;
if (type == '+') { // 插入操作
int id, priority;
ss >> id >> priority;
if (ss.fail()) {
cout << "null" << endl;
return 0;
}
idToPriority[id] = priority;
heap.push({priority, id});
} else if (type == '-') { // 提取操作
hasExtract = true;
int K;
ss >> K;
if (ss.fail()) {
cout << "null" << endl;
return 0;
}
vector<Experience> temp; // 临时存储弹出的元素
vector<int> result; // 存储最终结果
// 从堆中找到K个未被提取的元素
while (!heap.empty() && result.size() < K) {
Experience e = heap.top();
heap.pop();
temp.push_back(e);
// 检查是否已被提取且优先级是否最新
if (extracted.find(e.id) == extracted.end() &&
e.priority == idToPriority[e.id]) {
result.push_back(e.id);
}
}
// 将临时存储的元素重新放回堆中
for (const auto& e : temp) {
heap.push(e);
}
// 输出结果
if (result.size() < K) {
cout << -1 << endl;
} else {
// 按优先级降序,ID升序排序
sort(result.begin(), result.end(), [&](int a, int b) {
if (idToPriority[a] != idToPriority[b]) {
return idToPriority[a] > idToPriority[b];
}
return a < b;
});
for (int i = 0; i < result.size(); ++i) {
cout << result[i];
if (i != result.size() - 1) {
cout << " ";
}
}
cout << endl;
// 标记这些ID已被提取
for (int id : result) {
extracted.insert(id);
}
}
} else if (type == '=') { // 更新操作
int id, newPriority;
ss >> id >> newPriority;
if (ss.fail()) {
cout << "null" << endl;
return 0;
}
idToPriority[id] = newPriority;
heap.push({newPriority, id});
extracted.clear(); // 重置提取标记
} else {
cout << "null" << endl;
return 0;
}
}
// 如果没有提取操作,输出null
if (!hasExtract) {
cout << "null" << endl;
}
return 0;
}
一、核心问题:堆无法高效更新旧元素
std::priority_queue(堆)是一种“静态”数据结构:
- 当某个经验的优先级被更新(
update操作)时,堆中仍可能保留该经验的旧优先级版本(因为堆无法直接修改内部元素的值,也无法高效重新排序)。 - 例如:经验
id=3最初优先级为7,被加入堆中;之后通过update将其优先级改为20,堆中会新增一个(20, 3)的元素,但旧的(7, 3)元素仍在堆中。
此时,若提取时不检查优先级是否最新,可能会错误地提取到堆中残留的旧版本低优先级数据,导致结果错误。
二、具体场景示例
以用户提供的用例为例:
- 初始插入:
(1,5)、(2,10)、(3,7),堆中元素为(10,2)、(7,3)、(5,1)。 - 第一次提取
K=2:正常提取2(10)和3(7),并标记为已提取。 - 更新操作:
3的优先级改为20,堆中新增(20,3),此时堆中存在旧版本(7,3)和新版本(20,3)。 - 第二次提取
K=1:- 若不检查优先级是否最新,可能从堆中取出旧版本
(7,3)(但此时其真实优先级已更新为20),导致错误。 - 检查后会发现
(7,3)的优先级与哈希表中记录的20不匹配,判定为旧版本,跳过该元素,转而提取新版本(20,3)。
- 若不检查优先级是否最新,可能从堆中取出旧版本
三、本质:维护数据一致性
哈希表(idToPriority)存储的是当前最新的优先级,而堆中可能混合了新旧版本的优先级数据。提取时检查 e.priority == idToPriority[e.id],本质是:
- 用哈希表作为“真理源”,验证堆中元素是否为当前有效的版本。
- 过滤掉堆中残留的旧版本数据,确保提取的经验优先级是最新更新后的结果。
四、不检查会导致什么问题?
如果跳过优先级是否最新的检查,会出现两种错误:
- 提取到低优先级的旧版本:例如某经验已被更新为高优先级,但堆中旧版本的低优先级数据被错误提取,导致真实高优先级经验被遗漏。
- 优先级排序失真:堆的排序基于旧数据,提取结果的优先级顺序与实际最新优先级不符,违背“按当前优先级提取Top K”的需求。
总结
检查优先级是否最新,是为了解决堆无法高效更新旧元素的缺陷,通过哈希表与堆的“双重校验”,确保提取的是当前经验池中真实有效的高优先级数据。这一操作是维持数据一致性的关键,也是优先级经验回放机制正确性的保障。
比如在https://niumacode.com/problem/P1696中,需要用一个map或数组来记录有无遍历过,如果遍历过则在while循环中继续
浙公网安备 33010602011771号