☰
哈工大SSE第33题:5×5矩阵鞍点C语言实现详解
2026/10/10 20:48:51 网站建设 项目流程

如果你正在刷哈工大 SSE 这套 C 语言编程练习,到第 33 题附近,大概率会撞上一个老熟脸:求 5×5 矩阵的鞍点。这道题说难不难,说简单也不简单,很多刚学完二维数组的同学会卡在这一题上。作为一个给 SSE 写过代码、也给学弟学妹改过代码的人,我今天把这道题彻底讲透。从题目定义、算法思路,到完整代码、评测平台上的隐藏坑,一路写到底。

这里先说明一下:哈工大的 SSE 是程序设计基础课程配套的在线实验评测系统,题目按编号排列,练的是语法、分支、循环、数组、指针这些基本功。第 33 题最常见的形态,就是给一个 5 行 5 列的整数矩阵,让你找出它的鞍点并输出。这个 SSE 和前端那边常说的 Server-Sent Events 不是一回事,别搜资料搜岔了。鞍点这个名字听着玄幻,定义其实很直白:在一个二维矩阵里,某个元素如果同时满足“在它所在的那一行是最大的”和“在它所在的那一列是最小的”,这个位置就叫鞍点。你想象一下马鞍的形状:沿着马背方向是拱起的,横着是下凹的。矩阵里的鞍点也一样,行方向上是峰值,列方向上是谷值。

如果你正在刷哈工大 SSE 这套 C 语言编程练习,到第 33 题附近,大概率会撞上一个老熟脸:求 5×5 矩阵的鞍点。这道题说难不难,说简单也不简单,很多刚学完二维数组的同学会卡在这一题上。作为一个给 SSE 写过代码、也给学弟学妹改过代码的人,我今天把这道题彻底讲透。从题目定义、算法思路,到完整代码、评测平台上的隐藏坑,一路写到底。

1. 先搞清楚题目在问什么:5×5矩阵鞍点的定义

1.1 哈工大 SSE 的练习 33 到底在考什么

哈工大 SSE 的全称一般被大家直接叫做“哈工大程序设计实验系统”,是给学生提交代码、自动评测的在线练习平台。它的题号不是随便排的,基本按照 C 语言知识点的推进顺序来:从最开始的 printf、scanf,到分支 if-else、循环 for/while,再到数组、指针、结构体、文件。练习 33 这个位置,通常出现在二维数组章节之后,属于“数组综合应用”级别的题目。也就是说,做这道题之前,你应该已经掌握了一维数组的基本操作、二维数组的遍历方式,以及循环嵌套的写法。如果这些基础还不牢,建议先回头刷几道一维数组的题再回来。

练习 33 的题面描述往往非常简洁,核心就一句话:给定一个 5 行 5 列的整数矩阵,求它的鞍点。输出格式一般类似“输出鞍点所在行号、列号以及元素值”,找不到时输出“not found”之类的提示。不同年份、不同版本的 SSE 题目在细节上可能有差异,比如行列号从 0 开始还是从 1 开始,找不到时输出“not found”还是“no saddle point”。这些细节直接决定能不能一次通过评测,后面我会专门拿出来讲。

1.2 鞍点的数学定义与直观理解

在开始写代码之前,务必把定义吃透。一个 5×5 的矩阵可以写成 int matrix[5][5],其中第一个下标是行,第二个下标是列。某个位置 matrix[i][j] 成为鞍点,必须同时满足两个条件:

  • 在它的第 i 行上,matrix[i][j] 是这一行所有元素中的最大值;
  • 在它的第 j 列上,matrix[i][j] 是这一列所有元素中的最小值。

注意“同时”两个字,只满足一个不算鞍点。例如矩阵:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

第 0 行的最大值是 5,位置在 matrix[0][4]。再看第 4 列,元素是 5、10、15、20、25,最小值为 5。因此 matrix[0][4] 满足“行最大、列最小”,它就是鞍点。程序应该输出它的行号 0、列号 4、值 5。这个例子很适合用来验证代码逻辑,我建议你先在脑子里把整个过程过一遍。

1.3 新手最容易踩的坑:把“行最大列最小”写反

我给学弟学妹改代码的时候,发现最常见的错误不是语法,而是方向搞反。有些人会把鞍点定义写成“行最小列最大”,还有些人会把行和列的下标搞混,在比较时写成 matrix[j][i] 而不是 matrix[i][j]。这两种错误在语法上完全没问题,编译器不会报错,但结果完全错误。

为什么容易搞反?因为“鞍点”这个名词在日常生活中并不常见,大家只能死记定义。我的记忆技巧是:把“鞍”字和“马鞍”绑在一起——马鞍沿着马背方向是隆起的,所以行方向上是“峰”;横向是垂下去的,所以列方向上是“谷”。理解了这个几何意象,就不容易记反了。如果你觉得不够,还有一个更朴素的土办法:直接背下这道题的标准说法,练习 33 考的是“行最大、列最小”,每次动手写代码前先在注释里写一遍定义,再开始写。

2. 算法选型:暴力三重循环还是预处理

2.1 最直白的暴力解法:逐个元素扫描行列

拿到这道题,第一反应是直接暴力。对矩阵中的每个元素 matrix[i][j],都去扫描它所在的第 i 行,看它是不是最大值;再扫描第 j 列,看它是不是最小值。如果两个条件都满足,就输出。这个思路没有任何技巧,代码写起来也直接:

for (i = 0; i < 5; i++) { for (j = 0; j < 5; j++) { int isRowMax = 1, isColMin = 1; for (k = 0; k < 5; k++) { if (matrix[i][k] > matrix[i][j]) { isRowMax = 0; } } for (k = 0; k < 5; k++) { if (matrix[k][j] < matrix[i][j]) { isColMin = 0; } } if (isRowMax && isColMin) { printf("%d %d %d\n", i, j, matrix[i][j]); } } }

暴力法的时间复杂度是 O(n) 的三次方,因为最外层 25 个位置,每个位置要扫描 5 个行元素和 5 个列元素。矩阵规模固定在 5×5 时,总操作量是 25×10=250 次,对计算机来说完全不值一提。所以从“能不能通过评测”的角度看,暴力法已经可以提交了。但它有一个隐藏问题:代码里嵌套了三层循环,初学者在变量初始化位置、循环边界上很容易写错。比如有人会把 isRowMax 初始化的位置放错,导致所有元素都被判定成行最大值。

2.2 更清晰的预处理思路:先记录每行最大值和每列最小值

我教新手时更推荐另一种做法,也是我认为面试和课程设计里更“有章法”的写法:先扫描一遍矩阵,把每一行的最大值、每一列的最小值分别记下来,然后再扫描第二遍,判断每个元素是否同时等于它所在行的最大值和所在列的最小值。

这个过程你可以理解为:先把“谁是行老大、谁是列老小”的名单列好,第二次直接查名单。具体需要两个一维辅助数组:

  • rowMax[5]:rowMax[i] 记录第 i 行的最大值;
  • colMin[5]:colMin[j] 记录第 j 列的最小值。

第一遍遍历矩阵时更新这两个数组,第二遍遍历矩阵时判断 matrix[i][j] == rowMax[i] && matrix[i][j] == colMin[j]。这种预处理法的复杂度是 O(n)。对 5×5 矩阵来说,第一遍和第二遍各 25 次操作,比暴力法少了一半多。更重要的是,代码结构非常清楚:读入阶段、预处理阶段、判定阶段,三个阶段各自独立,后期想加功能或者改逻辑都很方便。

2.3 为什么必须使用 limits.h 里的 INT_MAX 和 INT_MIN

很多同学第一次写这段代码时,会随手把 rowMax 初始化成 0,把 colMin 初始化成 99999。这在小规模测试下可能碰巧正确,但存在两个隐患。

隐患一:矩阵元素可能是负数。如果所有元素都是负数,比如 -1 到 -25,那么“每行最大值”会比 0 小。把 rowMax 初始化为 0 后,所有比较都变成matrix[i][j] > 0,结果每行最大值都算成 0,最后找不到鞍点或者给出错误答案。隐患二:矩阵元素可能超过你随便写的大数 99999。虽然在课程练习里不太可能出现这种极端输入,但养成严谨的习惯总没错。

正确做法是引入 limits.h 头文件,使用 INT_MIN 和 INT_MAX。INT_MIN 是当前编译环境下 int 类型能表示的最小值,INT_MAX 是最大值。求每行最大值时,把 rowMax 初始化为 INT_MIN,这样任何正常的 int 元素都能在第一次比较时把它替换掉;求每列最小值时,把 colMin 初始化为 INT_MAX,道理同理。这也是我在前面提到的“为什么练习 33 相关的热词里会出现 limits.h”的原因——题目本身不直接考这个头文件,但评测用例可能会包含负数,不处理好就会栽跟头。

3. 完整实现:从读入矩阵到输出鞍点

3.1 头文件选择与宏定义

先搭出代码的基本框架。头文件只需要两个:

#include <stdio.h> #include <limits.h>

stdio.h 负责 scanf 和 printf,limits.h 提供 INT_MAX、INT_MIN。矩阵规模是固定的 5×5,所以我习惯用宏定义把它写成常量,这样后面改起来方便:

#define ROW 5 #define COL 5

有人会问,为什么不直接写数字 5?因为代码里多处用到 5,一旦题目改成 4×4 或 6×6,宏定义只需要改一行,而直接写数字则需要全文搜索替换,还容易漏改。这个问题在后续扩展成任意 N×M 矩阵时尤其明显,我会在第 5 节详细展开。另外注意,C 语言标准里 main 函数的返回类型要写成 int,末尾加上 return 0;有些 SSE 评测环境对返回值有要求,不写 return 0 可能导致编译警告甚至评测异常,别在这种地方丢分。

3.2 声明变量与初始化辅助数组

代码主体的第一步是声明变量。我建议把所有变量集中在函数开头声明,这样既符合 C89 的老规矩,也能规避部分平台编译器对“变量声明必须位于语句之前”的限制。核心变量如下:

int matrix[ROW][COL]; int rowMax[ROW]; int colMin[COL]; int i, j; int found = 0;

found 用来标记是否找到了至少一个鞍点,初值为 0。接下来初始化 rowMax 和 colMin:

for (i = 0; i < ROW; i++) { rowMax[i] = INT_MIN; } for (j = 0; j < COL; j++) { colMin[j] = INT_MAX; }

注意这是两个独立的 for 循环,分别遍历行和列。有些同学会写成双重循环来初始化,那就把步骤搞复杂了,完全没有必要。初始化完成后,就可以读入矩阵并同时更新这两个辅助数组。

3.3 读入矩阵的同时更新行最大值与列最小值

读入这 25 个整数,最简单的做法是两层 for 循环嵌套。scanf 的%d格式会自动跳过空白字符,所以输入数据无论是空格分隔、换行分隔还是多个空格混合,都能正确读入:

for (i = 0; i < ROW; i++) { for (j = 0; j < COL; j++) { scanf("%d", &matrix[i][j]); if (matrix[i][j] > rowMax[i]) { rowMax[i] = matrix[i][j]; } if (matrix[i][j] < colMin[j]) { colMin[j] = matrix[i][j]; } } }

这段代码的关键点在于:读入每个元素后马上顺手做两次比较。不需要先把 25 个数全部存完,再单独写两个循环去更新 rowMax 和 colMin,那样多了一轮遍历,逻辑上也更绕。边读边更新的思维在竞赛代码里很常见,核心思想是“数据到手,能算就算”,避免重复遍历同一批数据。但要注意,matrix 数组本身必须完整存下来,因为第二遍判定鞍点时还要用到每一个原始值。

3.4 第二遍扫描:判定鞍点并输出

辅助数组准备好了,接下来就是最核心的判定阶段。再次遍历整个矩阵,检查每个元素是否同时满足两个条件:

for (i = 0; i < ROW; i++) { for (j = 0; j < COL; j++) { if (matrix[i][j] == rowMax[i] && matrix[i][j] == colMin[j]) { printf("%d %d %d\n", i, j, matrix[i][j]); found = 1; } } } if (!found) { printf("not found\n"); }

这段代码里没有用“大于等于”或“小于等于”,而是直接用等号判断。这是因为 rowMax[i] 本来就是第 i 行的最大值,colMin[j] 本来就是第 j 列的最小值。一个元素想成为鞍点,它的值必须同时等于这两个数。等号条件天然支持“多个元素并列最大或并列最小”的情况,具体原因我会在第 4 节里细说。

3.5 完整代码整理

把上面的代码拼起来,就是一份可以直接提交到 SSE 的完整版本:

#include <stdio.h> #include <limits.h> #define ROW 5 #define COL 5 int main(void) { int matrix[ROW][COL]; int rowMax[ROW]; int colMin[COL]; int i, j; int found = 0; for (i = 0; i < ROW; i++) { rowMax[i] = INT_MIN; } for (j = 0; j < COL; j++) { colMin[j] = INT_MAX; } for (i = 0; i < ROW; i++) { for (j = 0; j < COL; j++) { scanf("%d", &matrix[i][j]); if (matrix[i][j] > rowMax[i]) { rowMax[i] = matrix[i][j]; } if (matrix[i][j] < colMin[j]) { colMin[j] = matrix[i][j]; } } } for (i = 0; i < ROW; i++) { for (j = 0; j < COL; j++) { if (matrix[i][j] == rowMax[i] && matrix[i][j] == colMin[j]) { printf("%d %d %d\n", i, j, matrix[i][j]); found = 1; } } } if (!found) { printf("not found\n"); } return 0; }

这份代码已经在多个场景下实测过,输入上面那个递增矩阵时输出是“0 4 5”,完全符合预期。如果题目要求只输出第一个鞍点,就在 printf 之后加上“跳出两层循环”的处理,最简单的办法是使用 goto 语句,或者把判定写在函数里配合 return。不过 SSE 练习 33 的常见题面是输出所有鞍点,所以这份代码没有做提前退出。

4. SSE评测下的常见问题与调试实录

4.1 输出格式与行列编号:看清原题再动手

SSE 是机器自动评测,输出字符串必须和标准答案完全一致,多一个空格、少一个换行都会判错。练习 33 的题面对于“行列号从 0 开始还是从 1 开始”通常有明确说明。如果题面写的是“行号、列号从 0 开始”,那我的代码里直接输出 i 和 j 就正确;如果题面要求从 1 开始,你需要输出 i+1 和 j+1。再比如“找不到鞍点”时的提示语,有的题面要求输出“not found”,有的是“no saddle point”,还有的是“NO”。这些字符串都要严格按题面来,不能凭感觉。

我的建议是:提交前先本地运行几个样例,包括一个能找到鞍点的样例和一个找不到鞍点的样例,人工检查输出结果是否和题面示例一字不差。这个习惯能帮你过滤掉大部分格式问题,避免在评测平台上反复试错消耗提交次数。

4.2 初始化变量时的“灵异”错误:辅助数组没初始化全

这是我改代码时遇到最多的错误。有些同学知道要用 rowMax 和 colMin,但只给 rowMax 写了初始化循环,colMin 直接用默认值,或者两个数组都用= {0}初始化。用 0 初始化 colMin 一旦遇到正数矩阵,所有 colMin[j] 都保持为 0,最终判定结果必然错误。还有同学把初始化循环写进了读入循环里面,导致每读一个元素就重置一次辅助数组,最后 rowMax 里存的是每行最后一个元素、colMin 里存的是每列最后一个元素。这些错误在逻辑上非常隐蔽,但结果一跑就现原形。

排查方法也很简单:在更新辅助数组的循环结束后,打印一遍 rowMax 和 colMin,看是不是和手工算的一致。如果第 0 行的最大值是 5,rowMax[0] 就必须是 5。很多看似“玄学”的出错,其实就是这种低级的初始化位置问题。

4.3 矩阵元素全相同或极端取值时怎么办

当一个矩阵的所有元素都相同时,比如全 0 矩阵,每一个位置既是所在行的最大值,也是所在列的最小值。按我的代码逻辑,25 个位置都会输出,这是符合“所有鞍点”语义的。如果题面要求的是“只输出一个鞍点”,那么需要找到后立即停止。极端情况还包括矩阵元素本身就是 INT_MIN 或 INT_MAX,由于我们初始化时用的就是这两个极限值,比较结果是正确的;如果你偷懒手动写了一个 -999999 作为初始最小值,而输入里恰好有 -1000000,就会出错。这就是为什么我一直强调用 limits.h。

4.4 读入数据时的空白字符陷阱

scanf 的%d会自动跳过空格、Tab 和换行,这是它最省心的地方。但有同学会用 getchar 配合循环逐字符读取整数,这样处理多位数时会非常痛苦。比如输入“123 45 67”,getchar 会把每个字符分开,你需要自己处理数字拼接和负数符号,稍不注意就出错。我的建议很简单:老老实实使用 scanf("%d", &matrix[i][j]),它能把所有空白字符的细节全部屏蔽掉。只有当题目故意把输入格式改成逗号分隔、并且在评测时真的用逗号时,才需要特殊处理,但 SSE 这类课程练习通常不会这么折腾人。

4.5 编译警告和平台差异:SSE 不只检查结果

在线评测系统通常不是只运行你的程序等结果,编译时还会开启一些警告选项。比如变量声明后未使用,某些环境会给出警告。虽然警告不一定判错,但代码风格不好很容易在某些严格配置下出问题。练习 33 这种程度的题目,注意三点就能稳过:第一,所有变量在函数开头声明,避免在循环体内临时声明导致 C89 兼容问题;第二,main 函数返回 int 并且写 return 0;第三,不要出现未使用的变量。很多同学喜欢声明一个变量用了两下就改掉,最后忘了删,这习惯在课程作业里没什么,到了项目里会被同事嫌弃。

4.6 调试技巧清单:一个用例定位所有问题

我在实际调试时经常用一个测试矩阵同时检查多种情况。比如:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

这个矩阵唯一鞍点是 (0, 4, 5)。如果你的代码输出结果不是“0 4 5”,要么是行最大/列最小方向反了,要么是下标统计错误。再换一个无鞍点矩阵:

5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1

这里每行最大值出现在第 0 行,每列最小值出现在第 4 行,没有任何元素同时满足两个条件,程序应该输出 not found。这两个用例一个覆盖“有鞍点”,一个覆盖“无鞍点”,结合起来能定位绝大多数逻辑错误。

5. 进阶扩展:从固定5×5到任意矩阵

5.1 把宏定义改成读入的行数列数

练习 33 要求固定 5×5,但很多同学会想练得更深一些,把程序改成处理任意 N×M 矩阵。最简单的改法是先读入行数和列数,再定义矩阵。C99 标准支持变长数组,也就是可以用变量指定数组大小:

int row, col; scanf("%d %d", &row, &col); int matrix[row][col];

但要注意,部分在线评测环境可能默认使用 C89,不支持这种写法。更稳妥的做法是用 malloc 动态分配内存,但初学者可能还没学到指针。我的建议是:如果只是自己练习,可以先尝试变长数组;如果是提交到严格的老平台,还是老老实实把 ROW 和 COL 定义成宏,或者考虑用一维数组模拟二维访问。由于练习 33 明确写的是 5×5,这个问题并不影响提交,纯属延伸思考。

5.2 变体题目:求“行最小列最大”的鞍点

我曾见过某些版本把鞍点定义改成“行最小值、列最大值”,或者两种方向混在一起出题。改起来很简单,只需要把 rowMax 改成 rowMin,把 colMin 改成 colMax,比较符号反过来。具体到代码上,一行一列就能搞定:

if (matrix[i][j] < rowMin[i]) { rowMin[i] = matrix[i][j]; } if (matrix[i][j] > colMax[j]) { colMax[j] = matrix[i][j]; }

判定时把条件改成matrix[i][j] == rowMin[i] && matrix[i][j] == colMax[j]即可。做题前先读题、确认方向,比任何代码技巧都重要。

5.3 多组输入时的处理套路

有些扩展题目会在一份输入里包含多个矩阵,要求分别输出每个矩阵的鞍点。此时最外层的结构通常是:

while (scanf("%d %d", &row, &col) == 2) { // 读入一个矩阵并处理 }

注意每一组数据处理完后,found 必须重置为 0,rowMax 和 colMin 也必须重新初始化。我见过不少人在循环外只初始化一次,导致第二组矩阵直接复用第一组的结果。这个错误的隐蔽性很高,因为第一组数据可能正确,第二组就开始乱输出。建议把整个处理逻辑封装成一个函数,每组数据调用一次,内部自己初始化,能有效避免这类状态残留问题。

个人体会

我自己第一次做这道题时,用的就是暴力三重循环,当时觉得把每个元素都扫一遍很符合直觉。后来给学弟学妹讲题,才发现预处理法更适合用来理解二维数组的“行视角”和“列视角”。一个刚学完二维数组的人,能独立写出 rowMax 和 colMin 两个辅助数组并知道为什么初始化要用 INT_MIN 和 INT_MAX,说明他基本已经跨过了“循环套循环容易晕”的那道坎。如果你在 SSE 练习 33 上卡了比较久,不要急着怀疑自己智商,把 rowMax 和 colMin 在草稿纸上画出来,跟着几组数据手动推演一遍,思路很快就能理顺。这个题放在二维数组章节的末尾,就是为了检验你有没有真正建立起“按行思考”和“按列思考”的独立视角,代码本身反而不是最难的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询