先给大家交个底:我是一名在高校里带过好几轮数据结构课程实训的“老学长”,这几年陪着几百个学生刷过“头歌实践教学平台”上的各种关卡。如果说哪个题目看着简单、背后却最能暴露基本功,我第一个想到的就是这道“玉米地”。
“头歌数据结构课程实践——玉米地”在平台上属于二维数组和矩阵遍历方向的经典题目。单看题目描述,不过就是给你一块 N 行 M 列的玉米地,每个格子有对应的玉米产量,让你算总产量、找最高产的位置、或者统计某条对角线上的数据。听起来像就是套几个 for 循环的事,但它本质上考的是三件事:你能不能把现实问题抽象成二维数组模型,能不能把行列索引和边界条件理清楚,以及能不能写出能在评测机上稳定通过的代码。适合谁看?正在头歌上被二维数组关卡卡住的同学、期末复习数据结构想要补基础的同学,以及那些明明代码能跑、却死活过不了评测的朋友。
这篇文章我会按自己带学生的思路来拆:先从题目设计逻辑讲起,再给一套可以直接“抄作业”的建模方案和完整代码,然后把我踩过的坑、学生最容易犯的错都列出来,最后聊聊从“玉米地”出发还能延伸到哪些算法。文章里的示例题目设定是我基于这类实训的常见版本整理的,具体以你在头歌平台上实际看到的题目描述为准,但思路和代码是通用的。
1. 先别急着写代码:这道“玉米地”到底在考什么
1.1 从题目名字看数据结构考点
“玉米地”这个名字起得挺有迷惑性,第一次听还以为要计算农业产量,实际上它就是把二维数组换了个生活化的外衣。数据结构课程里,线性表讲完就要讲数组,数组讲完就得上难度,这时候拿一个“玉米地”当载体,让你把每行每列的产量存进矩阵,本质上训练的就是二维数组的建模能力。
我一般会跟学生这么类比:把玉米地想象成 Excel 表格,行号是第几垄地,列号是第几棵苗,交叉点上的单元格就是产量数据。题目让你做的所有事情——求和、找最大、算对角线——都是在跟这张“表格”打交道。你只要能把现实坐标映射到a[i][j]上,这道题就成功了一半。
还有一点容易被忽略:这类题目往往不是单独考察数组,它会把输入输出格式、多组测试数据、数组越界这些边界问题全塞进来。换句话说,题目的一半是“数学建模”,另一半是“健壮性编码”。很多同学代码逻辑写得挺顺,结果栽在 scanf 格式上,或者栽在数组开小了,这都是我没有提前跟他强调“评测机不吃这一套”的锅。
1.2 输入输出是隐藏的“半道题”
头歌这类平台和学校里的纸质作业最大的区别是,它有严格的评测逻辑。你的程序读什么格式的数据、输出什么格式的结果,必须和题目约定完全一致,多个空格、少个换行都可能被判错。就以“玉米地”来说,常见版本会给你这样的输入约定:
第一行输入两个整数 N 和 M,表示玉米地有 N 行 M 列。接下来 N 行,每行包含 M 个整数,表示每个格子的玉米产量。
然后输出要求可能是:
输出一个整数,表示整块玉米地的玉米总产量。
看起来很简单对吧?但我见过太多学生在这儿翻车。有些人读完 N 和 M 之后,用 scanf 读数据时不注意空格和换行,导致数据读串位;有些人输出的时候多了个\n,或者少了个\n,评测直接给判错。所以拿到题目之后,第一件事不是写代码,而是把输入输出要求逐字读一遍,尤其是“输出的格式”和“边界范围”。
1.3 为什么这类题会被放在课程实践里
你要是以为“玉米地”只是一道过场题,那就太小看课程设计了。我在备课的时候喜欢把实训题按“考点链路”串起来看:前几关练线性表和链表,后面开始练栈和队列,“玉米地”正好卡在“数组”和“后续复杂算法”中间。它的任务是让你把二维数组的几个固定套路练熟——行优先遍历、列优先遍历、按对角线遍历、边界判断。
这些套路为什么重要?因为后面你学图的邻接矩阵存储、学动态规划的二维状态表、学图像处理里的卷积操作,全部是在二维数组上做文章。如果“玉米地”这种基础遍历都要想半天,后面那些题根本没法做。所以遇到这道题,别只求过关,最好把每种遍历方式都自己动手写一遍,这是值回票价的地方。
2. 玉米地的建模方案:二维数组的几个关键选择
2.1 选对存储结构:静态数组、动态数组还是直接开大
“玉米地”这种题的数据范围通常不会太夸张,常见的是 N、M 在 100 到 1000 之间。这种规模下,静态二维数组是最省心的方案。C 语言里你直接写int farm[1005][1005];,把上限稍微开大一点,就能覆盖绝大多数测试点。
这里我特别想强调一个习惯:数组大小别刚好卡着题目给的上限开。题目说 N ≤ 1000,你就开[1000][1000],万一平台数据里有边界值、或者你循环里不小心多算了一个索引,马上就越界,而且是那种毫无提示的越界。我习惯的做法是“上限 + 5”,比如开[1005][1005],多出来的几个格子不吃亏,但能避免很多匪夷所思的报错。
有些同学学得比较新,想用 C99 的变长数组,或者用 vector 动态分配。说实话,在刷题场景里这是给自己找麻烦。变长数组在部分评测环境下可能不支持,动态分配还要记得释放,纯属增加出错概率。先老老实实用静态数组把题目过了,有余力再折腾其他写法。
2.2 遍历顺序:行优先、列优先和“隐藏要求”
遍历二维数组是最基础的操作,但遍历方式不同,代码写起来天差地别。默认情况下我们用行优先,就是外层循环控制行 i,内层循环控制列 j:
for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { // 处理 farm[i][j] } }这个顺序符合我们读表格的直觉:一行一行往下读。但如果题目突然让你“按列统计”,或者“求每一列的最大值”,你要立刻能反应过来,把内外层循环对调,或者在内层循环里调整访问方式。还有一个容易出错的点:按行优先读入的数据,你要按行优先处理,不要一会儿行一会儿列,把索引全部搞混。
除了这两种,有时候“玉米地”会加一个环形遍历或者螺旋遍历的小问。比如让你从外圈到内圈绕圈统计产量。这种题看着花哨,其实核心还是四个边界变量(top、bottom、left、right)的控制,当我把这道题当课堂练习讲时,会让学生先写行优先版,再逼自己写螺旋版,两道题一起练,边界感就出来了。
2.3 索引从 0 开始还是从 1 开始,必须一开始就定死
这是新手最痛苦的地方。C 语言数组下标默认从 0 开始,但很多题目描述里会说“第 1 行第 1 列”,要是你不加转换,直接拿题目给的“第 1 行”去访问a[1],那你实际上读的是第二行。我有一次在课堂上专门做了个统计,三分之一的学生在这上面翻过车。
我的建议是:无论题目用 1 开始还是 0 开始描述,你写代码时统一用 0 开始。读入时把行列坐标减一,或者干脆读入的时候就直接映射好。比如题目说输入“第 i 行的第 j 个数”,你就存在farm[i-1][j-1]。这样后续所有循环、判断都统一用 0 开始,不容易乱。
关键是这个转换一定要在一开始就定好,不要写到一半发现算错了再回头改。我自己的习惯是在代码注释里写清楚:“farm 数组下标从 0 开始,但题目输入从 1 开始,读入时减一”。这个小注释在调试时能救你一命。
3. 完整实现:从读入玉米地到输出统计结果
3.1 把题目需求拆成三个小功能
为了讲清楚整道题的实现思路,我在这里定义一个典型的“玉米地”任务版本。假设题目要求:
- 输入 N、M,再输入 N 行 M 列的玉米产量数据;
- 计算整块地的总产量;
- 找出产量最高的格子,输出它的产量值和坐标(行、列);
- 计算从左上角到右下角的主对角线产量之和。
这三个任务刚好覆盖了最基础的二维数组遍历、求最值和特殊路径访问。下面我直接给一份完整的 C 语言参考实现,并逐段说明为什么这么写。
3.2 参考代码与逐步讲解
#include <stdio.h> int main() { int n, m; int farm[1005][1005]; // 读入行数和列数 scanf("%d %d", &n, &m); // 读入玉米地数据,同时累加总产量 long long total = 0; int maxValue = -1; int maxRow = -1, maxCol = -1; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { scanf("%d", &farm[i][j]); total += farm[i][j]; // 边读入边找最大值,省得再遍历一遍 if (farm[i][j] > maxValue) { maxValue = farm[i][j]; maxRow = i; maxCol = j; } } } // 输出总产量 printf("%lld\n", total); // 输出最高产位置,注意坐标转换回题目描述里的“第几行第几列” printf("%d %d %d\n", maxValue, maxRow + 1, maxCol + 1); // 计算主对角线产量之和 long long diagSum = 0; int minDim = n < m ? n : m; for (int k = 0; k < minDim; k++) { diagSum += farm[k][k]; } printf("%lld\n", diagSum); return 0; }这段代码里有两个细节我要单独拎出来讲。第一个是total和diagSum用了long long而不是int。很多同学在头歌上跑“玉米地”时结果不对,不是逻辑错了,而是产量累加时 int 溢出。假设 N 和 M 都是 1000,每个格子的产量是 100000,总产量就到了 10^11,明显超过 int 的 21 亿上限。这个坑我在实训中反复强调:涉及到累加和,数据范围没把握就直接上 long long,反正不差那点内存。
第二个细节是不管求最大值还是求对角线,我都把计算嵌在或者紧接着读入循环之后,没有额外开三层循环。有些同学会先把数据存好,再用一个独立的双重循环重头扫一遍找最大值,不能说错,但代码冗余,而且在 N、M 变大时浪费时间。边读边算既简洁又高效,这也是评测代码的一个好习惯。
3.3 测试用例设计与验证方法
写完代码别急着提交,先自己造几组测试数据在本地跑一遍。我给学生的一个固定流程是:先跑小数据,再跑边界数据,最后跑极端数据。举个例子,如果题目给了 N=2, M=3 的样例:
2 3 1 2 3 4 5 6期望输出应该是:
21 6 2 3 12第一行 21 是 1+2+3+4+5+6;第二行是最大值 6,位于第 2 行第 3 列;第三行是主对角线 1+5=6,但因为列数只有 3、行数只有 2,副对角线只能取到 k=0 和 k=1,也就是 a[0][0] 和 a[1][1],结果是 6。为什么不是 6+4? 因为主对角线是从左上到右下,不是从右上到左下。
我建议你至少测三组:一组是题目给的样例,一组是 N=1、M=1 的极端小数据,一组是全 0 数据。极端小数据能帮你发现坐标输出有没有问题,全 0 数据能帮你确认最大值初始值设置没毛病。我在代码里把maxValue初始化为 -1,就是为了避免全 0 时最大值初始为 0 导致判断失效。这一招是从“找最小值初始化为极大值、找最大值初始化为极小值”的套路里来的。
4. 我在“玉米地”上踩过的坑:调试实录与排查技巧
4.1 数组越界:肉眼看不出的“幽灵错误”
带实训这几年,“玉米地”这道题里最神出鬼没的错误就是数组越界。有一次一个学生代码逻辑完全正确,一提交就是“运行时错误”。我帮他一行行看,发现他在循环里写了for (int j = 0; j <= m; j++),多了一个等号。就这一个字符,数组最后一个格子后面那个位置被写入了数据,C 语言不会报错,但可能把其他变量给冲掉了,程序后续行为完全不可预测。
这种错误难就难在它不一定会稳定复现。有时候你本地跑是好的,提交到头歌上就崩;有时候小数据是对的,大数据崩。排查手法其实很笨但很有效:跑大点儿的数据,比如 N=1000、M=1000,全填随机数,如果程序异常退出,十有八九是越界。你也可以开着编译器警告(比如 gcc 的-fsanitize=address),它能直接告诉你越界的位置,实训时遇到“运行时错误”我一般先让学生用这招。
4.2 scanf 的格式问题:换行空格都是细节
第二个高频翻车点是 scanf 的格式。有些人喜欢在%d后面带空格,比如scanf("%d ", &n),这会让程序在读完 n 之后继续吃掉输入流里的空白字符,导致第一行数据读取错位。还有人在循环里用scanf("%d%d", &farm[i][j])把两个数据当一次读,但如果题目输入是用空格分隔的,这样写没问题;可万一数据是用逗号分隔,你就要改成匹配逗号的格式。总之,面向评测机写输入代码时,能用最朴素的scanf("%d", &x)就用最朴素的,别给自己加戏。
另外,处理多组测试数据时,如果题目没说要读到 EOF,那就别自己写while(scanf(...) != EOF)死循环。头歌的评测通常是单组数据,你只要老老实实读一次就行。这个坑看着低级,但我在批改学生作业时确实见过不少人栽在这。
4.3 多组数据的变量重置问题
有些版本的“玉米地”题目会加难,比如让你处理 T 组数据,每组都是一个新的玉米地。这时候最容易出错的是变量没有重置。total算完一组后忘了清零,直接累加下一组,结果输出翻了好几倍。maxValue也是同样的问题,上一组最大值没重置,下一组数据全比它小,你输出的还是旧值。
我的习惯是:把每一组数据的处理逻辑封装成一个独立的函数,函数内部的局部变量每次调用都会重新初始化,天然避免“残留数据”问题。如果不想写函数,那就要特别注意循环开头把变量置位。我经常和学生开玩笑说,变量重置这关过不了的人,后面学链表时删除结点、遍历链表都会栽跟头,因为本质上都是“状态没有及时清空”的问题。
4.4 常见错误速查表
为了方便同学们自查,我把这道题最常见的错误整理成了表格。你在头歌上提交不过的时候,拿这张表一项项对,大概率能找到原因。
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
| 答案偏大,恰好是大样本时 | int 累加溢出 | total 改用 long long |
| 输出结果差一行或多一行 | 输出格式多了/少了换行 | 严格按题目约定 printf,最后也补\n |
| 运行时错误,小数据正常 | 数组越界,循环条件多了等号 | 检查所有<=,数组开大 5 |
| 最大值一直是 0 | 初始化值设成了 0,而数据全为负 | maxValue 初始化为负数 |
| 坐标输出比正确答案大 1 | 忘了把 0 起始下标转成 1 起始行号 | 输出时 row + 1, col + 1 |
| 读入错位,一片乱码 | scanf 格式串里多了空格 | 用最朴素%d格式 |
这个表我打印过贴在实验室墙上,效果比反复口头提醒要好得多。如果你把表里每一项都在代码里核了一遍还查不出来,那就把代码放一放,喝口水,回来看一眼循环边界,很多时候是眼花了。
5. 从“玉米地”延伸出去:这些能力后续能用在哪些地方
5.1 二维数组是很多“看起来很难”题目的地基
“玉米地”本身不难,但它练的能力是所有二维数据问题的地基。头歌数据结构课程的后续关卡里,有大量题目都是“玉米地”的变体:比如求一个矩阵的转置,就是行列交换后输出;比如求两个矩阵的乘积,就是三层循环加索引对齐;再比如图像翻转、旋转,本质上还是搞清楚变换前后的坐标映射关系。
我带过一个学生,在“玉米地”上花了整整一个晚上,反复研究螺旋遍历和斜向遍历。结果后面学到图的邻接矩阵存储时,他几乎是全班最快理解“从顶点 i 到顶点 j 有没有边,就看 matrix[i][j] 是否为 1”这个映射关系的人。因为他已经在“玉米地”里把二维数组索引玩明白了。这不夸张,学习数据结构很大的一个瓶颈就是“抽象不出来”,而二维数组恰恰是帮助建立抽象能力的第一道关卡。
5.2 如果题目升级,你该怎么思考
把“玉米地”往难了改,改法还挺多的,而且每个方向都对应一个后续要学的经典算法。如果题目改成“每走一步只能向上下左右移动,求从左上角到右下角采集的最多玉米数量”,这就是动态规划,状态转移方程要从二维表格的某个方向推过来。如果改成“找产量大于某个阈值、且上下左右相连的最大玉米块”,这就变成了 Flood Fill,用 BFS 或 DFS 做连通块搜索。如果改成“每个格子有通过代价,求最低代价路径”,那就是最短路问题。
我在实训课上有个保留节目:让学生把“玉米地”里找最大值的代码改一改,变成“求每一行的最大值”,再改成“求每个 3×3 小区域的最大值”,最后改成“对整个矩阵做上下翻转”。每一步改动都不大,但每一步都在训练同一个核心能力——二维坐标的变换和边界控制。等你把这一系列变体都写顺了,后面听 BFS、DP 这些概念时会轻松很多。
5.3 如何在头歌平台上高效刷这类基础题
最后聊聊刷题节奏。头歌这种平台的好处是关卡之间是递进的,坏处是有些人纯粹为了拿分,遇到卡住的题就翻答案或者直接抄。我建议大家至少做到以下三步:第一,题目看明白之后,先在草稿纸上画一个 3×3 的小矩阵,手工推一遍输出,确认自己的思路;第二,写代码之后一定要自己造测试数据,别只依赖题目样例;第三,通过了之后,试着改一版不同的遍历顺序,或者把功能封装成函数,再提交一次,看能不能过。
这三步做完,一道基础题你至少顶别人做三道。我在实际带课过程中发现,凡是愿意在“玉米地”这种简单题上多折腾几版的学生,后面学图论、学排序、学树,普遍比只求过关的人更稳。数据结构这门课就是这样,真正的分水岭不在于你刷了多少难题,而在于这些基础题你有没有真正吃透。
写在最后的一点体会
我到现在给学生讲“玉米地”时,还经常想起自己当年第一次在头歌上做这道题的经历。当时我也觉得这就是个套两层循环的题,代码写完一交就过了,没当回事。后来老师课堂上提了一个问题——如果玉米地很大,怎么在内存里存下整块数据、又怎么用最少的遍历拿到想要的结果——我才发现自己只会照着模板写,根本不懂为什么这么写。从那以后我刷题就养成了一个习惯:每道题过了之后,会问自己三个问题,我处理的是什么样的数据结构、数据是怎么存储的、我的遍历方式能不能更优。
如果你现在正卡在头歌的“玉米地”上,别急躁。这道题说白了就是二维数组的“九九乘法表”,你把行、列、边界、累加这几个点吃透,后面的路会顺很多。最后分享一个小技巧:提交之前,把代码里所有<=都扫一遍改成<试试,把int换成long long试试,很多莫名其妙的错误,根源就是你从来没怀疑过这两个地方。祝你在头歌上顺利过关,也希望这篇拆解不只是帮你拿到这关的分数,更帮你把二维数组这道地基打得结实一点。