2 条题解
-
0
作为一名教练,制作 OJ 数据集时,我们需要重点考察滑动窗口的移出逻辑。如果 的增长过于缓慢,队列会变得非常长;如果 的跳跃巨大,队列会频繁排空。我们需要覆盖这两种极端以及边界条件。
本题不涉及树或图的递归结构,也不涉及除法运算,因此不存在爆栈和除零风险。
一、 数据生成器代码 (C++14)
该程序采用非递归逻辑,通过模拟标准算法生成
.out文件,确保数据的绝对准确。#include <iostream> #include <fstream> #include <vector> #include <string> #include <queue> #include <random> #include <ctime> #include <algorithm> using namespace std; /** * 标程逻辑:用于生成标准输出文件 * 使用 long long 处理 t 以防万一,虽然题目范围 int 足够 */ vector<int> get_standard_ans(int n, const vector<int>& timestamps) { vector<int> results; queue<int> q; for (int t : timestamps) { q.push(t); while (!q.empty() && q.front() < t - 3000) { q.pop(); } results.push_back((int)q.size()); } return results; } /** * 文件写入辅助函数 */ void write_test_case(int id, int n, const vector<int>& timestamps) { string in_file = to_string(id) + ".in"; string out_file = to_string(id) + ".out"; // 写入输入文件 (.in) ofstream fout(in_file); fout << n << "\n"; for (int i = 0; i < n; ++i) { fout << timestamps[i] << "\n"; } fout.close(); // 计算并写入输出文件 (.out) vector<int> results = get_standard_ans(n, timestamps); ofstream fans(out_file); for (int res : results) { fans << res << "\n"; } fans.close(); cout << "Generated Case " << id << " (N=" << n << ", MaxT=" << timestamps.back() << ")" << endl; } int main() { // 使用 mt19937 提供高质量随机数 mt19937 rng(time(0)); for (int i = 1; i <= 10; ++i) { int n = 0; vector<int> ts; if (i == 1) { // 样例数据 n = 4; ts = {1, 100, 3001, 3002}; } else if (i == 2) { // 边界:最小 N n = 1; ts = {1000000}; } else if (i == 3) { // 密集请求:每 1ms 一个请求,窗口达到最大负载 n = 5000; for (int j = 1; j <= n; ++j) ts.push_back(j); } else if (i == 4) { // 稀疏请求:请求间隔全部超过 3000ms,队列长度始终为 1 n = 1000; int current_t = 1; for (int j = 0; j < n; ++j) { ts.push_back(current_t); current_t += 3001 + (rng() % 100); } } else if (i == 5) { // 边界检查:t 刚好相差 3000ms n = 1000; int base = 10; for (int j = 0; j < n; ++j) { ts.push_back(base + j * 3000); // 10, 3010, 6010... } } else if (i <= 7) { // 随机跳变:模拟真实的请求波动 n = 5000; int current_t = 1; for (int j = 0; j < n; ++j) { ts.push_back(current_t); current_t += (rng() % 1500); // 平均两个请求在窗口内 } } else if (i <= 9) { // 大规模随机 n = 10000; int current_t = rng() % 1000; for (int j = 0; j < n; ++j) { ts.push_back(current_t); current_t += (rng() % 500 + 1); } } else { // 极限测试:最大 N,且 t 接近 10^9 边界 n = 10000; int current_t = 1000000000 - 40000; for (int j = 0; j < n; ++j) { ts.push_back(current_t); current_t += (rng() % 3 + 1); // 极度密集 } } write_test_case(i, n, ts); } return 0; }
二、 测试点设计思路(创建 OJ 必读)
针对滑动窗口类题目,这 10 组测试点覆盖了以下逻辑死角:
测试点 数据特征 考察重点 1 官方样例 确保基础逻辑与题目描述完全一致。 2 最小规模测试。 3 极度密集 () 考察窗口内元素达到峰值(约 3001 个)时的性能。 4 极度稀疏 () 考察队列频繁“入一个出一个”的空满切换逻辑。 5 刚好 3000ms 间隔 检查边界判定 front < t - 3000是否正确处理了等号。6-7 随机平滑增长 考察普通情况下的队列维护。 8-9 随机 压力测试,确保均摊 的时间复杂度。 10 大数值与高密度 考察在 接近 且请求极度密集时,是否有溢出风险及性能退化。
三、 考场避坑建议
-
数据类型: 虽然 ,
int不会溢出。但如果计算t + 3000时, 刚好在 附近,就会溢出。建议在比较时使用q.front() < t - 3000(减法安全)或者统一使用long long。 -
空间溢出: 的
std::queue占用内存极小,远低于 256MB。但如果是在某些嵌入式环境或极严限制(如 1MB)下,建议使用数组模拟循环队列。 -
时间复杂度误区: 不要在每次
ping的时候都遍历整个队列(),那会导致 Case 3 和 Case 10 严重超时。一定要利用队列的先进先出特性,只检查队首。 -
严格递增: 读题时务必看到“严格递增”这个关键词。如果题目不保证递增,则需要改用二分查找或平衡树,那将是完全不同的算法模型。
这份生成器生成的
.in和.out文件可以直接上传到任何标准 OJ(如 HUSTOJ, Hydro, Vijos 等)。祝你的学生们能通过这道题彻底掌握滑动窗口。 -
-
0
你好,同学。这道题是考查单调性与滑动窗口最直观的入门题。在 NOI 竞赛中,虽然我们可以用多种方式实现,但利用
std::queue维护一个随时间滑动的窗口是最标准且最高效的写法。以下是基于 C++14 标准 的满分参考代码,以及深度复杂度分析。
一、 最近的请求次数 标准题解 (C++14)
#include <iostream> #include <queue> using namespace std; /** * 核心逻辑: * 1. 由于时间戳 t 是严格递增的,这保证了先进入队列的时间戳一定先过期。 * 2. 窗口范围是 [t - 3000, t]。 * 3. 每次 ping 时,我们只需剔除队首那些早于 t - 3000 的元素,剩下的即为窗口内的有效请求。 */ class RecentCounter { private: // 使用 std::queue 维护滑动窗口 queue<int> q; public: RecentCounter() { // 构造函数,初始化清空队列(虽然在本题多用例下可选) while(!q.empty()) q.pop(); } int ping(int t) { // 1. 将当前的请求时间 t 加入队尾 q.push(t); // 2. 关键点:根据单调性,从队首开始清理“过期”的请求。 // 失效界限是 t - 3000,所有小于这个值的 t 都不在 [t-3000, t] 范围内。 int limit = t - 3000; while (!q.empty() && q.front() < limit) { q.pop(); } // 3. 此时队列中剩下的元素全部满足:limit <= q.front() <= t // 队列的长度即为过去 3000 毫秒内的请求总数 return q.size(); } }; int main() { // NOI 竞赛必备:极大提升大量数据下的读写效率 ios::sync_with_stdio(false); cin.tie(0); RecentCounter rc; int n; if (!(cin >> n)) return 0; // 循环读入 n 个请求时间戳 for (int i = 0; i < n; ++i) { int t; cin >> t; // 每次输出 ping 的返回值 cout << rc.ping(t) << "\n"; } return 0; }
二、 复杂度分析思考过程
1. 时间复杂度分析
- 单次
ping(t)操作:q.push(t):。while循环中的q.pop():看似这是一个循环,可能执行多次。但在整个程序的生命周期中,每一个请求时间戳 最多只会被push进队列一次,也最多只会被pop出队列一次。- 均摊分析 (Amortized Analysis):总共 个元素,总操作数上限是 次。因此,单次操作的均摊时间复杂度为 。
- 总复杂度:对于 个请求,总时间复杂度为 。对于 的数据量,耗时不到 1ms。
2. 空间复杂度分析
- 存储需求:队列中存储的元素数量取决于 3000 毫秒内的最大请求频率。
- 最坏情况:如果在 3000 毫秒内发生了 次请求,空间复杂度为 。
三、 考场易错点与避坑指南
- 界限条件:题目要求区间是
[t - 3000, t],即包含端点。因此判断过期的条件必须是q.front() < t - 3000。如果是q.front() <= t - 3000就会漏掉刚好在边界上的请求。 - 严格递增的重要性:如果 不是严格递增的,这套队列逻辑就会失效(因为较晚的 可能会比较早的 先过期,导致队首不是最先过期的)。遇到非递增情况,则需要使用二分查找等 的方法。
- 大数值处理:虽然本题 到 没超过
int范围,但在 NOI 某些题目中,时间戳可能达到 ,此时必须使用long long。
四、 时间复杂度优化建议
如果这道题的数据量 增加到 级,我们需要关注更细微的性能差异:
-
容器选择:
std::queue默认底层是std::deque。deque为了维护块状内存,其push和pop的常数略大。- 优化:可以使用
std::queue<int, std::list<int>>(通常更慢)或者更推荐的双指针模拟循环队列。
-
数组模拟队列 (Static Queue): 利用 已知的特点,使用数组和两个指针
head,tail来模拟,可以获得极致的常数速度。int q[100005], head = 0, tail = 0; // 入队: q[tail++] = t; // 出队: while(head < tail && q[head] < t - 3000) head++; // 长度: tail - head; -
循环队列: 如果内存限制非常严格(例如 2MB),且我们知道 3000ms 内请求数不会超过某个常数 ,可以使用长度为 的静态数组通过
% M取模操作实现循环队列,将空间开销压到最低。
教练寄语:这道题是理解“滑动窗口”与“先进先出”逻辑的教科书级案例。记住:**只要数据具有单调性,队列就是维护动态区间的最强武器。**加油!
- 单次
- 1
信息
- ID
- 19376
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者