这大概是洛谷“深基4”题单里,让最多人第一次体会到“数组题也不会想当然”的一道题。P5724 求极差 / 最大跨度值 / 最大值和最小值的差,题面短得可怜:输入若干个整数,输出最大值与最小值的差。就这么一句话,本质上考察的却是一整套后续算法都会反复用到的基本功:读入数据、遍历维护最值、处理边界条件、避开类型和初始化的坑。
题目虽然简单,但它背后的“极差”概念,不只在 OJ 上出现。做数据分析要看一组数据的波动范围,写滑动窗口要动态维护一段区间的最大值和最小值,评审一段代码也要先看逻辑边界是否健壮。这个题目恰好把这些东西压缩在一个最小例子里,适合刚学完数组和循环的初学者拿来练手,也适合正在复习基础算法的同学快速自检——如果你不看题解,能不能一次写对,并且把每个边界情况都解释清楚。
1. 题目到底在考什么:从“求极差”看一类入门问题
1.1 极差是什么,为什么竞赛入门题都爱考它
极差(Range)在统计学里也叫全距,定义就是一组数据中最大值与最小值的差。它衡量的是这组数据的波动幅度,数值越大说明数据分布越分散,数值越小说明分布越集中。
这个名字在不同教材里叫法不太一样。有的叫“最大跨度值”,有的直接写成“最大值和最小值的差”,洛谷这道题的三个别名其实指向同一个东西。理解这一点很重要,因为 OJ 上很多题描述不换汤不换药,将来你还会碰到“求一个数组里最大元素和最小元素之差”的各种变体,核心都是同一套操作。
竞赛入门阶段拿极差当练习题,是因为它同时覆盖了两类基本能力:
- 对单个数组元素进行扫描的过程,包括循环、下标访问、分支判断。
- 处理“最值”问题的通用思路,即先有初始值,再不断比较更新。
不夸张地说,你后面接触到的动态规划、二分答案、滑动窗口,很多地方都要用到“维护当前最大值/最小值”这种朴素思想。只不过极差把这件事压缩到最简单,让你先把基本功打扎实。
1.2 三种常见解法的思路对比
既然要求最大值和最小值的差,直观上有三种做法。
第一种是扫描法:读入所有数据并同时记录最大值和最小值,最后输出差值。复杂度是 O(n),空间可以做到 O(1),这也是我认为最符合题目考察意图的解法。
第二种是排序法:把数组排好序后,最后一个元素减第一个元素。复杂度 O(n log n),虽然能过,但没有利用到这个问题的特殊性质,属于“杀鸡用牛刀”,在数据量大的时候会吃亏。
第三种是直接调用语言自带的求极值函数,例如 Python 的 min() 和 max(),C++ 的 std::minmax_element()。这种写法工程上很常见,可读性好,但如果是学习阶段,建议还是自己写一遍内部逻辑,否则很难积累代码直觉。
三种方法最关键的区别在于信息量。你需要的只是两个极值,而不是数据之间的顺序关系,所以排序其实多做了一大堆无用功。当你以后遇到“只求最大值”“只比大小”这类问题时,第一反应应该是扫描,而不是排序。这个判断力靠题目堆出来,这道 P5724 就是一个很好的起点。
| 解法 | 时间复杂度 | 额外空间 | 适合场景 | 缺点 |
|---|---|---|---|---|
| 扫描维护最值 | O(n) | O(1) | 只需要极值差 | 需要处理初始化 |
| 排序后取首尾 | O(n log n) | O(1) 或 O(n) | 后续还需要排序结果 | 多了无效计算 |
| 内置 min/max 函数 | O(n) | O(1) 或 O(n) | 工程代码,不易写错 | 初学者不易理解内部逻辑 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 不同语言的完整实现与关键细节
2.1 C++ 实现:从“标准答案”看初始化陷阱
我在带训练的时候,发现第一次写这道题的 C++ 选手,十有八九会在初始化上犯同一个错误。这里先给一个我觉得最稳妥的写法。
cpp复制#include <cstdio>
#include <algorithm>
int main() {
int n;
scanf("%d", &n);
int x;
scanf("%d", &x); // 先读第一个数
int maxv = x;
int minv = x;
for (int i = 1; i < n; ++i) {
scanf("%d", &x);
if (x > maxv) maxv = x;
if (x < minv) minv = x;
}
printf("%d\n", maxv - minv);
return 0;
}
这段代码最关键的地方在于,我没有把 maxv 初始化为 0,也没有把 minv 初始化为一个很大的数,而是直接把它设成第一个输入元素。为什么这样安全?因为如果全部输入都是负数,maxv = 0 就会导致答案错误,你以为最大值是 0,实际上它根本不在数据里。
当然也有另一套常见做法:
cpp复制int maxv = INT_MIN;
int minv = INT_MAX;
这也是对的,只要记得引入 <climits> 头文件。这两种方式各有拥趸,我个人更喜欢用第一个元素初始化,因为思路更自然,也不依赖宏定义,尤其是在数据范围不明确或可能出现更大值类型的时候。
2.2 Python 实现:先学会手动扫描,再谈简洁写法
如果是 Python 选手,这道题最直观的写法其实很短:
python复制n = int(input())
a = list(map(int, input().split()))
print(max(a) - min(a))
如果你是刚学编程,我建议还是先看下面的手动扫描版本,理解内部发生了什么:
python复制n = int(input())
a = list(map(int, input().split()))
maxv = a[0]
minv = a[0]
for x in a[1:]:
if x > maxv:
maxv = x
if x < minv:
minv = x
print(maxv - minv)
这里有个值得讨论的点:a[1:] 会复制一份新列表,对于这道题的数据量没什么影响,但如果你以后处理上百万个元素,这种切片会白白多占内存。更地道的写法是用索引循环,或者直接用 range(1, n)。写代码的时候,除了追求正确,还要下意识地考虑“数据大了怎么办”,这是从入门题开始就该养成的习惯。
Python 在处理超大输入的时候,input() 其实是比较慢的。如果以后刷到输入数据特别多的题,可以考虑用 sys.stdin.buffer.read().split() 一次性读入,再用 map 批量转成整数。这道题用不用无所谓,但早点知道这个技巧,后面会省很多事。
2.3 其他语言与工具环境的极值处理方式
如果你用 Java,思路一样,代码风格略有差异:
java复制int maxv = a[0];
int minv = a[0];
for (int i = 1; i < n; i++) {
maxv = Math.max(maxv, a[i]);
minv = Math.min(minv, a[i]);
}
System.out.println(maxv - minv);
而当你走出 OJ,在实际数据分析中拿到一组数据时,处理“最大值减最小值”这件事的工具又会变。比如在 Excel 里,就是 =MAX(A1:A100)-MIN(A1:A100);如果想按条件取最大,还会用到 MAXIFS。在 Stata 里,一条 summarize varname 就能看到极值,配合返回值的调用也能直接算极差。工具虽然不同,内在的“扫描比较”思想是完全一致的。这也是我建议你认真做这道题的原因:你学的不是某段代码,而是一种在任何语言、任何工具里都能迁移的数据处理逻辑。
3. 从“读题”到“AC”:完整过程还原与避坑点
3.1 读题和样例,教你如何确认自己没理解偏
先看洛谷 P5724 的样例输入:
code复制5
1 2 3 4 5
输出:
code复制4
这个样例太善良了,数据本身有序,最大值是 5,最小值是 1,差为 4,人眼一扫就能得到答案。但也正因为样例太简单,很多人写完代码,样例过了就急着交,结果测试点里藏着各种奇怪的边界情况,直接给你一个鲜红的 WA。
更稳的做法是提交前自己再造几组测试数据,手动把答案算出来,再和程序比对。比如:
- 输入
3,然后输入5 5 5,期望输出0。 - 输入
4,然后输入-1 -100 -99 -50,期望输出99(最大值 -1,最小值 -100)。 - 输入
2,然后输入2147483647 -2147483647,期望输出4294967294,这时候你就要考虑int是否放得下了。
如果你能把这些情况全部想清楚,再提交,那这题才算真正“读完了”。
3.2 输入格式里容易忽略的细节
这道题的输入格式是:
code复制n
a1 a2 ... an
第二个正整数 a1 a2 ... an 可能在同一个空格行里,也可能跨多行。比如这样也是合法输入:
code复制5
1 2
3 4 5
scanf、cin、input().split() 这类标准输入函数天然会跳过空白字符(空格、换行、制表符),所以不需要担心数据在不在同一行。但如果你用 getline 按行读,就要自己处理“一行可能装不下所有数据”的情况。
还有一个非常基础的坑:n 都可能为零吗?这道题题目里应该保证 n 至少为 1,但你要是自己写通用函数,最好先判断数组长度,避免对空数组取 a[0] 导致运行时错误。在没有明确说明的情况下,对每一个输入都保持警惕,是竞赛选手的基本素养。
3.3 数据范围与整型溢出风险
原题给的数据范围通常不会特别夸张,你直接用 int 大概率能过。但我想借这个问题讲清楚一个判断逻辑,而不是让你去死记这个题的范围。
int 在绝大多数 OJ 上是 32 位有符号整数,能表示的范围大约是 [-2147483648, 2147483647]。如果输入的数据里既有接近上限的正数,又有接近下限的负数,那么 maxv - minv 的结果就可能接近 2147483647 - (-2147483648),这个值已经超过 int 的上限了,会产生溢出。
一旦你意识到输入数据可能接近这个极端,就应该用 long long 或者直接输出时转成更大的类型。有些题目为了保证难度,会故意把数据范围卡在临界点附近,所以“看清楚数据范围再决定类型”的习惯,很可能在后面的考试里帮你避开一个大坑。
4. 实际问题排查:为什么你交上去还是 WA / RE / TLE
4.1 WA(答案错误)的三类高频原因
第一类就是初始化问题。最大值变量初始化为 0,但数据全是负数;最小值变量初始化为 0,但数据全是正数。这种错误样例不容易暴露,因为普通测试数据通常有正有负,恰好能把错误掩盖掉。
第二类是输出格式问题。有些同学会把样例的输出当成“带提示的”,自作聪明加了类似 printf("result=%d\n", maxv - minv) 的语句。OJ 的评测器只认约定的输出格式,多余字符都会被判定为 WA。
第三类是读入顺序错误。比如先循环读取数据,最后才读 n,或者把 n 也当成数据存进数组里,导致后面的比较范围不对。这种问题通常出现在你从别的题复制模板却没有改干净的情况下。
4.2 RE(运行时错误)和 TLE(超时)怎么排查
RE 最常见的原因是数组越界。声明 int a[100] 而输入 n=200,或者下标从 1 开始,却访问了 a[n],都会导致访问未知内存。这类问题的解决办法是:要么开一个足够大的静态数组,例如 const int MAXN = 100000 + 5;,要么直接用动态数组 vector<int> a(n);,让容器自己处理边界。
TLE 在这道题上不太可能发生,因为 O(n) 的扫描已经是最优时间复杂度。但如果同样思路放在其他题上超时了,就得检查是不是使用了过慢的输入输出方式,比如 C++ 的 cin 和 cout 在默认情况下要和 C 标准 I/O 同步,速度较慢;加上 ios::sync_with_stdio(false); 和 cin.tie(nullptr); 可以明显提升速度。Python 选手则要注意,input() 在百万级输入时会明显慢于 sys.stdin.buffer.read(),提前储备这些优化手段,能在后续刷题中省去不少排查时间。
4.3 手造边界数据,测试用例速查表
分享一个我自己常用的测试思路,就是给程序喂入一些“不友好”的输入,看它能不能扛住。这里整理一份可以直接用来检验的测试表:
| 测试场景 | 输入示例 | 期望输出 | 考察点 |
|---|---|---|---|
| 单元素 | 1 + 42 |
0 |
最大值与最小值相同 |
| 全负数 | 3 + -5 -2 -8 |
6 |
max 初始化不能为 0 |
| 全正数 | 3 + 5 2 8 |
6 |
min 初始化不能为 0 |
| 全部相等 | 4 + 7 7 7 7 |
0 |
极差为 0 的情况 |
| 极大数据 | 2 + 2147483647 -2147483647 |
4294967294 |
需要考虑 long long |
| 乱序数据 | 6 + 3 1 4 1 5 9 |
8 |
常规复杂情况 |
每个场景都很小,但组合起来能覆盖绝大多数出错的可能。平时做题养成自己造数据的习惯,比盲目提交等反馈要高效得多。
5. 极差问题的变体与延伸:从静态数组到滑动窗口
5.1 同一题换个问法:滑动窗口求最值
如果你觉得 P5724 太简单,那可以试试很常见的变体:给定一个数组和一个窗口大小 k,求每个长度为 k 的子数组中最大值和最小值的差。比如数组 1 3 -1 -3 5 3 6 7,k=3,第一个窗口 1 3 -1 的最大值是 3,最小值是 -1,极差是 4;第二个窗口 3 -1 -3 的极差是 6,以此类推。
如果对每个窗口都重新扫描一遍,总复杂度是 O(n·k),当数据和 k 都很大时就会超时。正确做法是用单调队列(双端队列维护一个单调递减或递增的索引序列),把每个窗口的最值查询降到 O(1) 均摊,整个问题做到 O(n)。这个技巧在热词里也经常出现,比如“滑动窗口的最小值”就是一类经典题。
我强烈建议学有余力的同学去动手实现一遍滑动窗口极值,把这道 P5724 当成预热。你会发现,静态数组求一次最值和动态滑窗求多次最值之间,差的不是一个循环,而是“如何维护候选值集合”的算法设计思想,这正是从入门走向进阶的重要分水岭。
5.2 极差在数据分析和工程里的实际意义
走出 OJ,极差这个概念也会频繁出现在数据报表里。例如统计某产品一个月的每日销量,极差反映销量波动范围;分析传感器温度数据,极差能快速判断采集值是否异常;就连 Excel 里几个简单的 MAX 和 MIN 函数组合,也是在计算极差。
还有一点值得注意:热词里出现过“已达到计算机的连接数最大值”这样一个表述。它和“最大值”这个词有点关联,但实际是另一类“资源上限”问题——某个服务进程占用连接数达到操作系统的限制,属于运维层面的故障。初学编程时容易看到带“最大值”的报错就往算法上想,其实要先分清问题发生的层次:是数据逻辑问题,还是系统资源问题,还是代码本身的边界问题。这就是所谓“定位问题的能力”,从一道简单题开始,你对“最大值”“最小值”“极差”这些词的敏感度才会慢慢培养起来。
5.3 如果题目升级:求第二大值、区间修改后的极差
再延伸一步,有些题会要求输出第二大的值、第二大与最小的差,或者在每次区间修改后重新查询极差。前者要你在扫描时同时维护 max1 和 max2,后者就要用线段树之类的数据结构来维护区间最值,甚至还要合并左右子树的信息。
这些题目看起来比 P5724 复杂很多,但底层仍然是“合并两个已知区间的最大值和最小值”这一操作。你现在理解了极差问题的合并本质,将来接触线段树时就会觉得它并不神秘,无非是把比较过程变成树形结构里的自底向上合并。这也解释了我为什么推荐在这道入门题上多花一点时间:它是后面一系列高级数据结构的微缩版,值得认真对待。
6. 做题之外的一些个人体会
说回 P5724 本身。我第一次刷这道题的时候,觉得它就是“白给”,写了个 max(0, x) 的初始化版本,结果在一个全负数的测试点上翻车了。后来带学弟学妹训练,发现同样的问题会以不同的形式反复出现,不是大家不会写循环,而是缺少对边界情况的敏感度。
从那以后我养成了一个习惯:每次看到一个简单的求最值题目,都会先在草稿纸上列出三到五个边界数据,把答案手算出来,再写代码。写完后不急着提交,先拿这些边界数据自测一遍。这个习惯陪我从入门级刷到了省选级别,虽然不能保证每次 AC,但至少能过滤掉大部分低级失误。
题目可以很简单,但对待它的方式可以不简单。把这套“读题、造数据、写代码、自测、提交”的流程吃透,比多刷一百道重复的简单题都管用。希望这篇记录能帮你在极差这道题上不只是拿到一个 Accepted,而是真正把数组最值问题理解透。
