2 条题解
-
0
在 NOI 竞赛中,一个优秀的测试数据包需要包含各种极端边界条件(Corner Cases)。对于单调栈题目,我们需要特别构造递增序列、递减序列以及大量重复元素的序列来测试程序的鲁棒性。
以下代码是一个集成了“数据生成”与“标准算法”的自动化脚本。运行该程序后,它会在当前目录下自动生成
1.in/1.out到10.in/10.out共 10 组测试点。【数据生成器 C++ 代码】
#include <iostream> #include <fstream> #include <vector> #include <stack> #include <string> #include <random> #include <algorithm> using namespace std; // --- 标准答案算法部分 (O(n) 单调栈) --- void solve(int n, const vector<int>& T, vector<int>& ans) { ans.assign(n, 0); static int stk[100005]; // 使用静态数组模拟栈,避免重复分配内存 int top = 0; for (int i = 0; i < n; ++i) { while (top > 0 && T[i] > T[stk[top]]) { int prev = stk[top--]; ans[prev] = i - prev; } stk[++top] = i; } } // --- 数据生成辅助函数 --- void writeInput(int caseIdx, int n, const vector<int>& T) { string fileName = to_string(caseIdx) + ".in"; ofstream fout(fileName); fout << n << "\n"; for (int i = 0; i < n; ++i) { fout << T[i] << (i == n - 1 ? "" : " "); } fout << endl; fout.close(); } void writeOutput(int caseIdx, int n, const vector<int>& ans) { string fileName = to_string(caseIdx) + ".out"; ofstream fout(fileName); for (int i = 0; i < n; ++i) { fout << ans[i] << (i == n - 1 ? "" : " "); } fout << endl; fout.close(); } // --- 主生成逻辑 --- int main() { // 使用随机数引擎 C++11/14 标准 mt19937 rng(1337); auto getRand = [&](int minV, int maxV) { return uniform_int_distribution<int>(minV, maxV)(rng); }; for (int i = 1; i <= 10; ++i) { int n; vector<int> T; // --- 分点构造策略 --- if (i == 1) { // 样例数据 n = 8; T = {73, 74, 75, 71, 69, 72, 76, 73}; } else if (i == 2) { // 极小规模边界 n = 1; T = {30}; } else if (i == 3) { // 严格递增序列 (所有 ans 均为 1, 除了最后一个) n = 1000; for (int j = 0; j < n; ++j) T.push_back(30 + (j % 70)); } else if (i == 4) { // 严格递减序列 (所有 ans 均为 0) n = 1000; for (int j = 0; j < n; ++j) T.push_back(100 - (j % 70)); } else if (i == 5) { // 全部相同温度 (所有 ans 均为 0) n = 5000; T.assign(n, 50); } else if (i == 6) { // 锯齿形序列 (70, 30, 70, 30...) n = 10000; for (int j = 0; j < n; ++j) T.push_back(j % 2 == 0 ? 70 : 30); } else if (i >= 7 && i <= 9) { // 大规模随机数据 n = 100000; for (int j = 0; j < n; ++j) T.push_back(getRand(30, 100)); } else { // 极限挑战:只有最后一天是最高温 (测试单调栈最长驻留情况) n = 100000; for (int j = 0; j < n - 1; ++j) T.push_back(30); T.push_back(100); } // --- 执行标程并输出 --- vector<int> ans; solve(n, T, ans); writeInput(i, n, T); writeOutput(i, n, ans); cout << "Generated Test Case " << i << ": n = " << n << endl; } return 0; }
测试点分布说明(OJ 构建参考):
测试点 规模 数据特征说明 考察目的 1 8 题目样例 基础逻辑验证 2 1 最小 边界处理 3 严格递增 栈内元素快速弹出的情况 4 严格递减 栈内元素始终不弹出,最终全 0 情况 5 全部相等 温度“不高于”不触发弹出,考察判断条件 6 锯齿形 频繁入栈出栈的切换逻辑 7-9 随机分布 综合性能与大压力测试 10 前 天极低,末天极高 模拟单调栈在最后时刻处理大量积压数据的极端情况
技术要点提示:
-
非递归性: 本题的标准解法(单调栈)本质是迭代的(
for+while),不存在递归调用,因此天然不存在爆栈风险。在生成器中,我们也使用了非递归的solve函数。 -
生成效率: 对于 的数据,生成 10 组的时间复杂度为 。在普通现代电脑上,运行此生成器脚本只需不到 1 秒即可完成所有
.in和.out的写入。 -
随机性保证: 使用了
mt19937随机数引擎,它比传统的rand()拥有更长的周期和更好的分布特性,是 NOI 竞赛中生成高随机度数据的推荐做法。 -
内存安全: 虽然
std::vector很方便,但在solve函数中,我们使用了static修饰的数组或预分配空间的vector。在处理 以上更高级别数据时,应尽量避免在循环内反复申请和释放内存。
-
-
0
你好,同学。作为教练,我需要提醒你,NOIP/NOI 竞赛中,代码的稳健性(Robustness)与执行效率同样重要。
这道题是单调栈的教科书式应用。在实现时,我们通常会为了追求更高的效率,使用**手写栈(Array-based Stack)**来代替
std::stack,并使用较快的输入输出方式。以下是符合 NOI 竞赛要求的标准代码示例。
【标准题解代码】
#include <cstdio> #include <iostream> using namespace std; // 常量定义,10^5 的数据量,数组开略大一点防止越界 const int MAXN = 100005; int t[MAXN]; // 存储每日气温 int ans[MAXN]; // 存储结果 int stk[MAXN]; // 手写栈:存储温度数组的下标 int top = 0; // 栈顶指针,top=0 表示栈为空 int main() { int n; // 使用 scanf 提高读入效率,在 10^5 级别数据下比 cin 快 if (scanf("%d", &n) != 1) return 0; for (int i = 0; i < n; i++) { scanf("%d", &t[i]); } // 单调栈核心逻辑 for (int i = 0; i < n; i++) { /* * 易错点:这里必须是 while 而不是 if。 * 因为当前这一天的温度可能同时高于栈顶好几天的温度。 * 关键点:栈内存储的是下标,这样才能计算天数差距。 */ while (top > 0 && t[i] > t[stk[top]]) { // 如果当前温度比栈顶温度高,说明找到了栈顶元素“下一个更高温” int prev_index = stk[top]; ans[prev_index] = i - prev_index; // 计算日期差 top--; // 出栈 } /* * 维护单调性:将当前下标压入栈中。 * 此时栈内下标对应的温度从栈底到栈顶一定是单调不增的。 */ stk[++top] = i; } // 题目要求:如果之后都没有更高气温,则为 0。 // 由于全局数组 ans 初始化即为 0,所以不需要额外处理栈内剩余元素。 // 输出结果,注意 NOI 格式要求空格隔开,末尾无多余空格或有换行均可 for (int i = 0; i < n; i++) { printf("%d%c", ans[i], i == n - 1 ? '\n' : ' '); } return 0; }
二、 复杂度分析思考过程
在 NOI 考场上,写完代码后必须进行严格的复杂度确认。
1. 时间复杂度:
- 分析:虽然代码中有一个
while循环嵌套在for循环内部,但这并不意味着它是 。 - 思考过程:请看每一个元素。每个下标 最多只会被执行一次入栈操作(
stk[++top] = i),并且一旦出栈(top--),它就再也不会进入逻辑了。 - 结论:入栈总次数为 ,出栈总次数最多为 。总的操作步数与 成线性关系。
2. 空间复杂度:
- 分析:我们开启了
t[],ans[],stk[]三个长度为 的数组。 - 思考过程: 个
int类型数据,每个int占 4 字节,总共约占用 字节 MB。 - 结论:这远远低于题目给出的 256MB 限制,空间非常充裕。
三、 时间复杂度优化建议
虽然 已经是理论最优解,但在实际竞赛中,若遇到更严苛的时间限制(例如 数据量且限时 0.2s),可以考虑以下优化:
-
快读(Fast I/O):
scanf虽然比cin快,但在极端数据下,使用getchar()手写的read()函数效率更高。inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); } return x * f; } -
避免频繁入栈出栈的对象创建: 在 C++14 中,使用
std::stack会有一定的对象维护开销。如上文所示,使用原生数组模拟栈,直接操作指针top,是竞赛中最极致的写法。 -
倒序遍历优化: 其实这道题也可以从右往左遍历。从右往左遍历时,栈内维护的是“右侧可能比我高的候选者”。这种思考方式在处理“左侧第一个比我高”和“右侧第一个比我高”同时存在的问题时(如:最大矩形面积)更加统一。
四、 考场注意事项
- 初始化:如果这道题有多组测试数据,记得在每组数据开始前清空
top = 0。 - 边界:下标是从 0 开始还是从 1 开始?在计算
i - prev_index时要统一标准。 - 数据量:本题温度最高 100,天数最高 ,结果不会溢出
int。如果天数是 (虽然不可能),则需要考虑long long。
- 分析:虽然代码中有一个
- 1
信息
- ID
- 19363
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者