2 条题解
-
0
针对“设计前中后队列”,制作 OJ 数据时需要特别注意奇偶长度切换时的平衡维护。由于操作涉及中间位置,我们需要构造大量在 临界点反复跳动的数据,以及大规模随机操作来验证 的效率。
以下是为你准备的数据生成器,采用 C++14 标准,逻辑严密且不会产生递归或除零异常。
一、 数据生成器代码 (C++14)
该程序整合了标准解法逻辑,通过非递归的迭代方式生成
1.in到10.out共 20 个文件。#include <iostream> #include <fstream> #include <vector> #include <string> #include <deque> #include <random> #include <ctime> #include <algorithm> using namespace std; // ================= 标准标程逻辑 (用于生成 .out) ================= class StandardQueue { deque<int> left, right; void balance() { if (left.size() > right.size()) { right.push_front(left.back()); left.pop_back(); } else if (right.size() > left.size() + 1) { left.push_back(right.front()); right.pop_front(); } } public: void pushFront(int val) { left.push_front(val); balance(); } void pushBack(int val) { right.push_back(val); balance(); } void pushMiddle(int val) { if (left.size() < right.size()) left.push_back(val); else right.push_front(val); balance(); } int popFront() { if (right.empty()) return -1; int res; if (left.empty()) { res = right.front(); right.pop_front(); } else { res = left.front(); left.pop_front(); } balance(); return res; } int popMiddle() { if (right.empty()) return -1; int res; if (left.size() == right.size()) { res = left.back(); left.pop_back(); } else { res = right.front(); right.pop_front(); } balance(); return res; } int popBack() { if (right.empty()) return -1; int res = right.back(); right.pop_back(); balance(); return res; } }; // ================= 数据构造逻辑 ================= struct Query { string op; int val; }; void make_test(int id, const vector<Query>& queries) { ofstream fout(to_string(id) + ".in"); ofstream fans(to_string(id) + ".out"); fout << queries.size() << "\n"; StandardQueue sq; for (const auto& q : queries) { if (q.op == "pushFront") { fout << q.op << " " << q.val << "\n"; sq.pushFront(q.val); } else if (q.op == "pushMiddle") { fout << q.op << " " << q.val << "\n"; sq.pushMiddle(q.val); } else if (q.op == "pushBack") { fout << q.op << " " << q.val << "\n"; sq.pushBack(q.val); } else if (q.op == "popFront") { fout << q.op << "\n"; fans << sq.popFront() << "\n"; } else if (q.op == "popMiddle") { fout << q.op << "\n"; fans << sq.popMiddle() << "\n"; } else if (q.op == "popBack") { fout << q.op << "\n"; fans << sq.popBack() << "\n"; } } cout << "Case " << id << " generated." << endl; } int main() { mt19937 rng(time(0)); string ops[] = {"pushFront", "pushMiddle", "pushBack", "popFront", "popMiddle", "popBack"}; for (int i = 1; i <= 10; ++i) { vector<Query> qs; int Q = 1000; // 对应题目规模上限 if (i == 1) { // 样例 qs = {{"pushFront", 1}, {"pushBack", 2}, {"pushMiddle", 3}, {"pushMiddle", 4}, {"popFront", 0}, {"popMiddle", 0}, {"popMiddle", 0}, {"popBack", 0}, {"popFront", 0}}; } else if (i == 2) { // 只有前部和后部操作 for(int j=0; j<Q; ++j) { int type = rng() % 4; if(type == 0) qs.push_back({"pushFront", (int)rng()%100}); else if(type == 1) qs.push_back({"pushBack", (int)rng()%100}); else if(type == 2) qs.push_back({"popFront", 0}); else qs.push_back({"popBack", 0}); } } else if (i == 3) { // 密集的中点操作 for(int j=0; j<Q; ++j) { if(rng() % 2 == 0) qs.push_back({"pushMiddle", (int)rng()%100}); else qs.push_back({"popMiddle", 0}); } } else if (i == 4) { // 空队列弹出测试 for(int j=0; j<10; ++j) qs.push_back({"popFront", 0}); for(int j=0; j<10; ++j) qs.push_back({"popMiddle", 0}); for(int j=0; j<10; ++j) qs.push_back({"popBack", 0}); } else if (i <= 6) { // 频繁在长度 1 和 2 之间切换 (测试平衡临界点) for(int j=0; j<Q/2; ++j) { qs.push_back({"pushMiddle", (int)rng()%100}); qs.push_back({"popMiddle", 0}); } } else { // 全随机压力测试 for(int j=0; j<Q; ++j) { int type = rng() % 6; if(type < 3) qs.push_back({ops[type], (int)(rng() % 1000000)}); else qs.push_back({ops[type], 0}); } } make_test(i, qs); } return 0; }
二、 测试点设计思路(创建 OJ 必读)
作为教练,为了防止选手通过简单的
vector::insert这种 的暴力做法混分,我们需要在数据中针对性地分布不同类型的压力:测试点 数据特征 考查意图 1 样例数据 验证基础逻辑及题目要求的中点定义。 2 仅首尾操作 检查 pushFront和popBack是否能正常配合,不影响中点结构。3 纯中点压力 考查 pushMiddle和popMiddle在 和 队列间搬运元素的准确性。4 空队列边界 检查在没有元素时是否能正确输出 -1而不崩溃。5-6 临界跳变 高频错点:当队列总数在 1, 2, 3 之间变化时,中点位置的指针/索引逻辑最容易出错。 7-10 综合随机 满负荷 次操作(可根据需要修改代码中的 为更高),考察均摊 的性能。
三、 考场避坑建议
- 关于
std::deque的坑: 虽然std::deque的首尾操作是 ,但它的内存分配机制比std::vector复杂。在极端内存受限题目中(如本题给 256MB 很充裕,但如果只给 2MB),可能需要选手手写循环队列。 - 平衡规则的唯一性:
告诉选手:一定要定死一个规则(比如:右边永远不比左边少,且最多多 1 个)。不要在多个函数里写不同的平衡判断,统一封装成一个
balance()函数是最安全的,可以避免逻辑冲突导致的Runtime Error。 - 防止除零异常:
虽然本题逻辑不涉及除法,但在生成
val时,如果选手代码有自定义的哈希或取模操作,可能会因val=0出错。本生成器生成的val均为正整数,避开了此类隐患。 - 非递归安全: 本题的所有操作都是线性指令,生成器和标程均无递归调用,完全不存在爆栈风险。
这份生成器生成的
.in和.out格式严格对齐题目描述,你可以直接使用它们快速搭建评测环境。加油! - 关于
-
0
你好,同学。这道题在 LeetCode 上属于中等难度,但在 NOI 竞赛中,它是考查 “结构平衡维护” 和 “分治思想” 的入门级好题。
为了实现所有操作的均摊 时间复杂度,我们不能使用单一的
std::vector(因为中间插入是 ),而是应当使用两个std::deque(双端队列)来分别维护队列的前半部分和后半部分。以下是基于 C++14 标准 的满分题解代码。
一、 设计前中后队列 标准题解 (C++14)
#include <iostream> #include <deque> #include <string> using namespace std; /** * 核心逻辑:双端队列平衡法 * 1. 使用 left_q 维护前半部分,right_q 维护后半部分。 * 2. 核心平衡条件:left_q.size() <= right_q.size() 且 right_q.size() <= left_q.size() + 1。 * 也就是说,如果有奇数个元素,多出来的那个始终放在 right_q。 */ class FrontMiddleBackQueue { private: deque<int> left_q, right_q; // 辅助函数:重新调整两个队列,使其符合平衡条件 void balance() { if (left_q.size() > right_q.size()) { // left 过长,将末尾移到 right 的头部 right_q.push_front(left_q.back()); left_q.pop_back(); } else if (right_q.size() > left_q.size() + 1) { // right 过长,将头部移到 left 的末尾 left_q.push_back(right_q.front()); right_q.pop_front(); } } public: void pushFront(int val) { left_q.push_front(val); balance(); } void pushMiddle(int val) { // 根据平衡规则: // 如果长度相等,新元素放进 right 的头部 // 如果 right 比 left 多一个,新元素放进 left 的末尾 if (left_q.size() < right_q.size()) { left_q.push_back(val); } else { right_q.push_front(val); } // 注意:这种逻辑本身就维持了平衡,但调用 balance() 更加保险且易于理解 balance(); } void pushBack(int val) { right_q.push_back(val); balance(); } int popFront() { if (left_q.empty() && right_q.empty()) return -1; int res; if (left_q.empty()) { // 只有一个元素的情况,一定在 right_q res = right_q.front(); right_q.pop_front(); } else { res = left_q.front(); left_q.pop_front(); } balance(); return res; } int popMiddle() { if (left_q.empty() && right_q.empty()) return -1; int res; // 中点定义:n 个元素弹出 (n-1)/2。 // 若 n=2 (1,1), (2-1)/2 = 0,即 left 的末尾。 // 若 n=3 (1,2), (3-1)/2 = 1,即 right 的头部。 if (left_q.size() == right_q.size()) { res = left_q.back(); left_q.pop_back(); } else { res = right_q.front(); right_q.pop_front(); } balance(); return res; } int popBack() { if (right_q.empty()) return -1; int res = right_q.back(); right_q.pop_back(); balance(); return res; } }; int main() { // NOI 竞赛加速:处理大量操作时的必备优化 ios::sync_with_stdio(false); cin.tie(0); FrontMiddleBackQueue q; int Q_count; if (!(cin >> Q_count)) return 0; while (Q_count--) { string op; cin >> op; if (op == "pushFront") { int val; cin >> val; q.pushFront(val); } else if (op == "pushMiddle") { int val; cin >> val; q.pushMiddle(val); } else if (op == "pushBack") { int val; cin >> val; q.pushBack(val); } else if (op == "popFront") { cout << q.popFront() << "\n"; } else if (op == "popMiddle") { cout << q.popMiddle() << "\n"; } else if (op == "popBack") { cout << q.popBack() << "\n"; } } return 0; }
二、 复杂度分析思考过程
1. 时间复杂度分析
- 各操作性能:
std::deque的push_front,push_back,pop_front,pop_back均为 操作。 - 平衡逻辑:
balance()函数内部仅包含一次跨队列的移动,复杂度为 。 - 总结:所有 6 种操作的时间复杂度均为 均摊 。即使总操作数 达到 ,也能在秒级内完成。
2. 空间复杂度分析
- 使用两个队列存储所有的元素,空间复杂度为 。在 NOI 的 256MB 内存限制下,存储几十万个整数完全没有压力。
三、 考场避坑与优化建议
1. 为什么不直接用
std::vector?虽然这道题 的规模下, 的
vector::insert也能过,但在真正的 NOI 考场上,数据量往往是 级别。此时必须通过**“双结构平衡”**(类似对顶堆维护中位数)的思路将复杂度降为 。2. 易错点:中点判定
题目对中点的定义在“总数为偶数”时非常敏感:
- 插入时: 个元素,插入位置 。
- 弹出时: 个元素,弹出位置 。
- 避坑指南:在草稿纸上模拟 的情况。通过将多出来的元素固定放在右侧(
right_q),你可以发现:- 时,弹出 号位对应
left_q.back()。 - 时,弹出 号位对应
right_q.front()。
- 时,弹出 号位对应
3. 内存优化建议
如果 极大且内存限制极严,可以考虑使用静态数组模拟双向链表。链表的插入删除是天然 的,只需要通过一个随插入移动的“中点指针”来维护位置即可。但在有 STL 可用的情况下,
deque是最高效的选择。教练寄语: 这道题是数据结构设计中的经典。它告诉我们:面对复杂的操作(如中点操作),可以通过拆分(双队列)和维护某种特定的平衡性质,将原本线性的复杂度转化为常数级。 这种“平衡维护”的技巧在学习“对顶堆”和“Splay 树”时会反复用到。去实现它吧!
- 各操作性能:
- 1
信息
- ID
- 19377
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者