把每次插入的 1..x 压成一个块,只维护块前缀删除量;第 z 个元素和最大值都转成块级查询。
OJ: luogu
题目 ID: P9588
难度:普及+/提高
标签:数据结构模拟队列二分
日期: 2026-06-20 13:55
题意
维护一个队列,支持四种操作:
- 在队尾依次加入
1,2,...,x - 弹出队头前
y个元素 - 查询第
z个元素 - 查询整个队列的最大值
需要对第 3、4 种操作输出答案。
思路
先看一个可以直接验证想法的朴素解:
cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long ignored_n;
int q;
cin >> ignored_n >> q;
deque<i64> que;
for (int i = 1; i <= q; i++) {
int op;
cin >> op;
if (op == 1) {
int x;
cin >> x;
for (int v = 1; v <= x; v++) {
que.push_back(v);
}
} else if (op == 2) {
int y;
cin >> y;
while (y--) que.pop_front();
} else if (op == 3) {
int z;
cin >> z;
cout << que[z - 1] << '\n';
} else {
i64 mx = 0;
for (int j = 0; j < (int)que.size(); j++) {
mx = max(mx, que[j]);
}
cout << mx << '\n';
}
}
return 0;
}本题最关键的是:每次插入的内容不是任意序列,而总是一个完整的连续块:
[1,2,3,...,x]
而删除操作又只会从整个队列前端删。
所以一个插入生成的块,后续只可能变成它自己的某个后缀:
[start+1, start+2, ..., len]
因此每个块只要维护两个量:
len:原始长度start:已经被弹掉了多少前缀
这样四种操作都能在块层面完成:
- 插入:直接追加新块
- 弹出:从头块开始删,删完整块就跳过,否则只改
start - 查询第
z个元素:按块剩余长度前缀和定位到所在块,再换算块内排名 - 查询最大值:某个块的最大值永远是
len,遍历剩余块取最大即可
这种做法避免了展开 sum x 级别的真实元素。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int MAXQ = 200000 + 5;
struct Block {
i64 len; // 原始长度 x
i64 start; // 当前已经弹掉了多少个前缀元素,剩下的是 [start+1 .. len]
i64 size() const {
return len - start;
}
i64 max_value() const {
return len;
}
i64 kth(i64 k) const {
return start + k;
}
};
int q;
vector<Block> blocks;
int head_ptr = 0;
i64 prefix_size[MAXQ];
void rebuild_prefix() {
i64 sum = 0;
for (int i = head_ptr; i < (int)blocks.size(); i++) {
sum += blocks[i].size();
prefix_size[i] = sum;
}
}
int find_block_by_rank(i64 z) {
int left = head_ptr;
int right = (int)blocks.size() - 1;
int ans = right;
while (left <= right) {
int mid = (left + right) >> 1;
if (prefix_size[mid] >= z) {
ans = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long ignored_n;
cin >> ignored_n >> q;
for (int i = 1; i <= q; i++) {
int op;
cin >> op;
if (op == 1) {
i64 x;
cin >> x;
blocks.push_back({x, 0});
} else if (op == 2) {
i64 y;
cin >> y;
while (y > 0) {
i64 cur_size = blocks[head_ptr].size();
if (cur_size <= y) {
y -= cur_size;
head_ptr++;
} else {
blocks[head_ptr].start += y;
y = 0;
}
}
} else if (op == 3) {
i64 z;
cin >> z;
rebuild_prefix();
int idx = find_block_by_rank(z);
i64 prev = (idx == head_ptr ? 0 : prefix_size[idx - 1]);
i64 inside_rank = z - prev;
cout << blocks[idx].kth(inside_rank) << '\n';
} else {
i64 answer = 0;
for (int j = head_ptr; j < (int)blocks.size(); j++) {
if (blocks[j].size() > 0) {
answer = max(answer, blocks[j].max_value());
}
}
cout << answer << '\n';
}
}
return 0;
}复杂度
块数最多等于插入次数,所以不超过 q。
当前实现中:
- 插入、弹出均摊
- 查询第
z个元素、查询最大值为
总体空间复杂度
总结
这题的核心不是维护普通队列,而是先识别出:
- 队列始终由若干个
1..x的块拼接而成
一旦把它压缩成块结构,所有操作都会自然很多。