「MXOI Round 2」队列

GitHub跳转原题关系图返回列表

把每次插入的 1..x 压成一个块,只维护块前缀删除量;第 z 个元素和最大值都转成块级查询。

OJ: luogu

题目 ID: P9588

难度:普及+/提高

标签:数据结构模拟队列二分

日期: 2026-06-20 13:55

题意

维护一个队列,支持四种操作:

  1. 在队尾依次加入 1,2,...,x
  2. 弹出队头前 y 个元素
  3. 查询第 z 个元素
  4. 查询整个队列的最大值

需要对第 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:已经被弹掉了多少前缀

这样四种操作都能在块层面完成:

  1. 插入:直接追加新块
  2. 弹出:从头块开始删,删完整块就跳过,否则只改 start
  3. 查询第 z 个元素:按块剩余长度前缀和定位到所在块,再换算块内排名
  4. 查询最大值:某个块的最大值永远是 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

当前实现中:

  • 插入、弹出均摊 O(1)O(1)
  • 查询第 z 个元素、查询最大值为 O(块数)O(块数)

总体空间复杂度 O(q)O(q)

总结

这题的核心不是维护普通队列,而是先识别出:

  • 队列始终由若干个 1..x 的块拼接而成

一旦把它压缩成块结构,所有操作都会自然很多。