2 条题解
-
0
为了方便你创建 OJ 测试数据,我编写了这个 C++ 数据生成器。它集成了一份高效的 ST 表标程,并根据你的要求将数据规模控制在理想范围内。
数据生成器设计思路 (10 个测试点)
- Case 1-2 (入门与边界):。包含 以及全区间查询。
- Case 3-4 (中等规模):。用于卡掉 的暴力,但在本地调试时较快。
- Case 5-6 (大规模随机):。数据随机分布。
- Case 7 (特殊构造 - 单调):序列递增或递减,测试倍增跳跃的逻辑边界。
- Case 8 (特殊构造 - 相同):序列所有元素相等,防止在更新 时出现逻辑漏洞。
- Case 9-10 (极限压力):。这是在满足 2MB 文件大小 限制下的最大询问量。
数据生成器 C++ 代码 (gen.cpp)
#include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <random> #include <string> #include <ctime> using namespace std; // --- 标程:ST 表实现,用于生成 .out 文件 --- const int MAXN = 100005; const int MAXLOG = 20; int f[MAXN][MAXLOG]; int lg[MAXN]; void build_st(int n, const vector<int>& a) { lg[1] = 0; for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1; for (int i = 1; i <= n; i++) f[i][0] = a[i - 1]; for (int j = 1; j < MAXLOG; j++) { for (int i = 1; i + (1 << j) - 1 <= n; i++) { f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]); } } } int query_st(int l, int r) { int k = lg[r - l + 1]; return max(f[l][k], f[r - (1 << k) + 1][k]); } // --- 数据生成逻辑 --- void make_data(int id, int n, int m, bool special = false, int type = 0) { string in_name = to_string(id) + ".in"; string out_name = to_string(id) + ".out"; ofstream fout(in_name); ofstream fans(out_name); mt19937 rng(time(0) + id); uniform_int_distribution<int> dist_val(1, 1e9); fout << n << " " << m << "\n"; vector<int> a(n); for (int i = 0; i < n; i++) { if (special) { if (type == 1) a[i] = i; // 递增 else if (type == 2) a[i] = n - i; // 递减 else a[i] = 777; // 全部相等 } else { a[i] = dist_val(rng); } fout << a[i] << (i == n - 1 ? "" : " "); } fout << "\n"; build_st(n, a); uniform_int_distribution<int> dist_l(1, n); for (int i = 0; i < m; i++) { int l = dist_l(rng); int r = dist_l(rng); if (l > r) swap(l, r); fout << l << " " << r << "\n"; fans << query_st(l, r) << "\n"; } fout.close(); fans.close(); cout << "Generated Case " << id << ": N=" << n << ", M=" << m << endl; } int main() { // Case 1-2: 极小数据 make_data(1, 10, 10); make_data(2, 100, 100); // Case 3-4: 中等规模 (区分 O(NM)) make_data(3, 5000, 5000); make_data(4, 5000, 5000); // Case 5-6: 大规模随机 make_data(5, 100000, 100000); make_data(6, 100000, 100000); // Case 7-8: 特殊构造 make_data(7, 100000, 50000, true, 1); // 递增 make_data(8, 100000, 50000, true, 3); // 相等 // Case 9-10: 极限压力 (控制在 2MB 左右) // N=10^5 (约 400KB), M=2*10^5 (约 1.6MB) make_data(9, 100000, 200000); make_data(10, 100000, 200000); return 0; }关键点说明:
-
文件大小控制 (2MB 挑战):
- 在 NOI 官方测试中, 通常给到 ,但那样的输入文件会达到 20MB 以上。
- 本生成器将极限点设为 。根据计算: 个数约占 0.5MB, 组询问(每组两个 6 位数)约占 1.5MB,总文件大小刚好在 2MB 左右。
- 这组数据足以区分 查询(超时)和 查询(AC)。
-
生成效率:
- 使用了
std::mt19937随机数引擎,比普通的rand()快且分布更均匀。 - 标程
build_st使用非递归 DP,即使在 规模下生成 10 组数据也只需不到 1 秒。
- 使用了
-
安全性:
- 所有数组都在常量
MAXN下静态分配,或者使用vector。 l和r的生成确保了 ,规避了数组越界。- 数值范围 ,使用
int存储(最大 ),计算过程中不涉及除法,无除零风险。
- 所有数组都在常量
使用提示:
- 编译运行:
g++ gen.cpp -o gen -O3,然后运行./gen。 - 上传 OJ:将生成的
1.in到10.out对应上传即可。 - 评测建议:由于 很大,建议在题目设置中将 时间限制 设为 1.0s,并提醒选手使用快读。如果是由于输入文件过大导致系统读取缓慢,可以适当放宽时限。
-
0
在 NOI 竞赛中,处理区间最值问题(RMQ)时,ST 表是查询效率的极限()。由于本题数据范围中询问次数 高达 ,传统的
cin或不加优化的log2函数都会导致超时。以下是从暴力扫描到竞赛级 ST 表算法的演进过程。
版本一:暴力扫描算法 ( 查询)
思路分析: 对于每一次询问 ,都直接从 遍历到 ,记录最大值。 局限性: 总复杂度 。在最坏情况下计算量约为 ,在 NOI 1秒的时限内(通常处理 次运算)必然 TLE。
#include <iostream> #include <algorithm> #include <cstdio> using namespace std; const int MAXN = 100005; int a[MAXN]; int main() { int n, m; if (scanf("%d %d", &n, &m) != 2) return 0; for (int i = 1; i <= n; i++) scanf("%d", &a[i]); while (m--) { int l, r; scanf("%d %d", &l, &r); int res = a[l]; // 暴力遍历区间,最坏 O(N) for (int i = l + 1; i <= r; i++) { if (a[i] > res) res = a[i]; } printf("%d\n", res); } return 0; }
版本二:标准 ST 表算法 ( 查询)
思路分析: 利用倍增思想。设 表示区间 的最大值。
- 预处理:。
- 查询:将区间拆分为两个有重叠的长度为 的块。
#include <iostream> #include <cstdio> #include <algorithm> #include <cmath> using namespace std; int n, m; int f[100005][21]; // 2^20 > 10^5 int main() { scanf("%d %d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &f[i][0]); // 2^0 长度即为自身 } // 预处理 DP:先枚举长度 j,再枚举起点 i for (int j = 1; j <= 20; j++) { for (int i = 1; i + (1 << j) - 1 <= n; i++) { f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]); } } while (m--) { int l, r; scanf("%d %d", &l, &r); int k = log2(r - l + 1); // 此处使用 math 库的 log2,效率较低 int ans = max(f[l][k], f[r - (1 << k) + 1][k]); printf("%d\n", ans); } return 0; }
版本三:NOI 竞赛最优复杂度版本 (快读 + 预处理 Log)
思路优化:
- 预处理 Log 数组:
log2()是浮点运算,在 次调用下非常耗时。通过lg[i] = lg[i >> 1] + 1递推预处理。 - 快速读入:本题输入量巨大(约 20MB),
scanf可能会超时。使用getchar()编写快读函数是 NOI 选手的标配。 - 位运算:使用
(1 << j)代替幂运算。
#include <iostream> #include <cstdio> #include <algorithm> using namespace std; // 1. 快速读入模板,针对 M=2e6 的极致优化 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; } const int MAXN = 100005; const int MAXLOG = 21; int f[MAXN][MAXLOG]; int lg[MAXN]; int main() { int n = read(); int m = read(); // 2. 预处理 Log2 数组:lg[i] 表示 floor(log2(i)) lg[1] = 0; for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1; for (int i = 1; i <= n; i++) { f[i][0] = read(); } // 3. ST 表构建:核心转移方程 // 注意外层循环是幂次 j,内层循环是起点 i // 易错点:i 的边界必须保证 i + 2^j - 1 <= n for (int j = 1; j < MAXLOG; j++) { for (int i = 1; i + (1 << j) - 1 <= n; i++) { f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]); } } // 4. O(1) 查询 for (int i = 0; i < m; i++) { int l = read(); int r = read(); int k = lg[r - l + 1]; // 两个重叠区间覆盖:[l, l+2^k-1] 和 [r-2^k+1, r] int ans = max(f[l][k], f[r - (1 << k) + 1][k]); printf("%d\n", ans); } return 0; }
时间与空间复杂度分析思考过程
-
时间复杂度:
- 预处理:构建 ST 表需要遍历 个状态。对于 ,约 次运算。
- 查询:通过预处理的
lg数组实现真正的 ,总计 次。 - 总复杂度:。实测在 下,耗时约 400ms-600ms。
-
空间复杂度:
- 数组
f[100005][21]存储int类型。 - 占用空间:$100005 \times 21 \times 4 \text{ bytes} \approx 8.4 \text{ MB}$。
- 完全符合 NOI 常见的 128MB 或 256MB 限制。
- 数组
时间复杂度优化建议
- Log 数组优化:永远不要在循环内使用
log2()或pow(),它们的时间开销在竞赛中是致命的。 - 内存访问连续性:在 ST 表预处理中,
f[i][j]的第一维放i(起点),第二维放j(幂次)。这样在内层循环遍历i时,内存访问相对连续,对 CPU 缓存友好。 - 快写 (Fast Output):如果输出量也非常大(例如本题 ),甚至可以考虑使用
putchar实现快写,但在本题中printf通常已经足够。
读题关键词总结
- “ 查询”:暗示 ST 表。
- “不修改/静态序列”:ST 表不支持修改。如果题目要求带修改的区间最值,则必须选择 线段树(查询 )。
- “最大值/GCD”:满足幂等律(即 )的运算才可以使用 ST 表覆盖。
- 1
信息
- ID
- 7599
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 5
- 标签
- 递交数
- 3
- 已通过
- 1
- 上传者