
1. 项目概述从一道题看二维数组遍历的核心思维最近在带学生刷《信息学奥赛一本通》的题目翻到第1120题“同行列对角线的格”发现不少刚接触二维数组和坐标计算的同学在这里容易卡壳。这道题本身逻辑并不复杂但它像一块极好的“试金石”能清晰检验你是否真正理解了数组下标、行列关系以及方向向量的运用。题目要求是给定一个n×n的方格矩阵行列从1开始编号再给定一个位置(i, j)要求输出所有与(i, j)同行、同列、以及在同一对角线上的格子坐标。乍一看这不就是几个循环的事情吗但实际编码时很多细节需要厘清对角线的方向有两条主对角线和副对角线坐标变化的规律是什么如何确保输出的坐标不越界在1到n的范围内输出的顺序又该如何安排这些问题恰恰是初学者从“看懂题目”到“写出健壮代码”的关键跨越。我常跟学生说信息学竞赛的题目尤其是基础题其价值往往不在于算法有多高深而在于它能否逼迫你形成严谨、无歧义的逻辑思维。这道1120题就是一个典型的例子。接下来我将结合这道题拆解二维数组遍历与坐标推算的完整思路并分享一些在调试和代码优化上的实操心得。无论你是正在备赛的学生还是希望巩固C二维数组知识的开发者相信这篇详细的拆解都能给你带来直接的帮助。2. 核心需求解析与解题思路确立拿到题目第一步不是急着写代码而是彻底理解需求并把它转化为清晰的、可执行的逻辑步骤。我们先把题目要求翻译成更具体的编程任务。2.1 问题定义与输入输出规格题目明确给出了一个n×n的方格矩阵行列编号均从1开始。这意味着我们在用数组思维处理时需要做一个“心理映射”题目中的坐标(i, j)对应我们思维中矩阵的第i行、第j列。输入三个整数n, i, j。输出分为三个部分同一行的所有格子坐标。同一列的所有格子坐标。同一主对角线从左上到右下方向的所有格子坐标。同一副对角线从右上到左下方向的所有格子坐标。输出顺序也有要求对于每一类坐标都需要按照数字大小顺序依次输出所有合法坐标。这暗示我们需要对生成的坐标进行排序或者更巧妙的通过控制遍历的顺序来直接满足输出要求。2.2 思路拆解四种情况的遍历策略基于以上需求我们可以将问题分解为四个独立的子任务每个子任务对应一种方向的遍历同行遍历行号固定为i列号col从1遍历到n。输出(i, col)。这里需要注意题目给出的位置(i, j)本身也在这一行中是否需要跳过根据题意“所有与(i, j)同行……的格子”它自身也应包含在内。所以直接遍历输出即可。同列遍历列号固定为j行号row从1遍历到n。输出(row, j)。主对角线遍历这条线上的点其行号与列号的差值是一个常数。对于给定点(i, j)恒有row - col i - j。我们需要找出所有满足此等式且row和col都在[1, n]范围内的点。一个高效的遍历方法是找到一个起始点然后同时向两个方向延伸。主对角线可以向左上方向和右下方向延伸。起始点可以通过计算得到左上方向的起始行start_row i - min(i-1, j-1)起始列start_col j - min(i-1, j-1)。然后从这个起始点开始行、列每次同时加1直到超出矩阵范围。副对角线遍历这条线上的点其行号与列号的和是一个常数。对于给定点(i, j)恒有row col i j。同样需要找到所有合法点。副对角线可以向左下方向和右上方向延伸。起始点计算左下方向的起始行start_row i min(n-i, j-1)起始列start_col j - min(n-i, j-1)不这样计算复杂且易错。更简单的方法是直接利用row col k(常数) 的关系让row从最大值向最小值遍历同时解出col k - row然后判断col是否在范围内。注意很多初学者在实现对角线遍历时会尝试写复杂的边界判断循环容易出错。我推荐使用“常数关系式”配合单变量遍历的方法逻辑更清晰也不易遗漏点。2.3 方案选型简洁性与效率的平衡对于本题n的范围在《一本通》中通常不会太大一般1000因此即使采用最朴素的“遍历所有点并判断是否满足条件”的O(n²)方法在时间上也是允许的。但这显然不是好方法。我们追求的应该是O(n)的解法即每种情况只遍历该行、列或对角线上的点其数量级最多是n。我推荐分别实现四个独立的循环模块。这样做的好处是逻辑隔离每个模块功能单一易于编写、调试和理解。输出顺序自然满足通过控制循环变量的增减顺序可以直接得到题目要求的“数字大小顺序”。代码可读性强比写一个复杂的多重判断结构要清晰得多。在输出格式上题目要求每个坐标用括号包裹且坐标之间用空格隔开。这意味着我们需要在循环中控制空格的输出通常是在非第一个输出的坐标前加一个空格。这是一个常见的输出格式控制技巧。3. 核心代码实现与逐行解析思路清晰后我们开始动手实现。我将使用C并给出两种风格的实现一种是直观的“分步计算法”另一种是更简洁的“向量延伸法”。我会对关键代码进行详细注释。3.1 基础实现分步计算法这种方法严格按照我们上面拆解的四种情况分别计算遍历的起点和终点。#include iostream using namespace std; int main() { int n, i, j; cin n i j; // 1. 输出同一行的格子 for (int col 1; col n; col) { // 控制空格不是第一个输出的元素就前面加空格 if (col ! 1) cout ; cout ( i , col ); } cout endl; // 每种情况输出完后换行 // 2. 输出同一列的格子 for (int row 1; row n; row) { if (row ! 1) cout ; cout ( row , j ); } cout endl; // 3. 输出同一主对角线左上-右下的格子 // 关键主对角线上 row - col 为常数 (i - j) // 我们先找到能使得row和col都在[1,n]范围内的最大row和col范围 // 一个技巧让row从大到小或从小到大遍历计算对应的col // 这里我们让row从1到n遍历计算col row - (i - j)然后判断col是否合法 bool firstOutput true; // 使用一个标志位来控制空格比用循环变量判断更通用 for (int row 1; row n; row) { int col row - (i - j); // 由 row - col i - j 推导得出 if (col 1 col n) { if (!firstOutput) cout ; cout ( row , col ); firstOutput false; } } cout endl; // 4. 输出同一副对角线右上-左下的格子 // 关键副对角线上 row col 为常数 (i j) firstOutput true; // 重置标志位 for (int row 1; row n; row) { int col (i j) - row; // 由 row col i j 推导得出 if (col 1 col n) { if (!firstOutput) cout ; cout ( row , col ); firstOutput false; } } cout endl; return 0; }代码解析与心得空格控制前两行同行、同列的遍历是完整的1到n我们可以用col ! 1或row ! 1来判断是否为第一个输出。但对于对角线我们遍历row从1到n但符合条件的点可能从中间开始用循环变量判断就不准了。因此我引入了firstOutput布尔标志位这是一个更健壮的做法。在输出第一个有效坐标后将其置为false此后输出前都加空格。对角线计算这是核心。主对角线利用row - col constant副对角线利用row col constant。通过遍历row直接解出col再判断其合法性。这种方法避免了去计算复杂的起点和步长思维负担小不易出错。边界判断if (col 1 col n)确保了坐标不会超出矩阵范围。这是必须的因为遍历所有row时计算出的col可能小于1或大于n。3.2 优化与通用实现方向向量法上面的方法已经很好但我们可以更进一步抽象出一个更通用的“沿方向遍历”的模式。这对于理解搜索算法如BFS、DFS中的方向数组很有帮助。#include iostream #include vector using namespace std; int main() { int n, i, j; cin n i j; // 定义四种方向右、下、右下主对角线、左下副对角线 // 每种方向用一个(dx, dy)表示表示行和列的变化量 int dx[] {0, 1, 1, 1}; // 行变化量 int dy[] {1, 0, 1, -1}; // 列变化量 // 注意左下方向是行1列-1所以dx1, dy-1 for (int dir 0; dir 4; dir) { bool firstOutput true; // 每个方向都需要从给定点(i, j)向两个相反方向走 // 我们用一个内层循环来处理一个方向上的两个朝向 for (int step -n; step n; step) { // step表示步数可正可负 if (step 0) continue; // step0就是原点我们在循环外单独处理或包含这里我们需要包含原点。 // 计算新坐标 int new_i i step * dx[dir]; int new_j j step * dy[dir]; // 检查是否在边界内 if (new_i 1 new_i n new_j 1 new_j n) { if (!firstOutput) cout ; cout ( new_i , new_j ); firstOutput false; } } // 注意上面的循环会漏掉原点(i,j)本身因为step从-n到n但跳过了0。 // 根据题目要求原点本身也是符合条件的点需要输出。 // 更优雅的方式是先输出原点再向两个方向延伸。 // 我们调整一下策略先输出原点然后step从1到n分别向正反两个方向探索。 // 为了清晰我们换一种写法 cout ( i , j ); // 先输出中心点 firstOutput false; // 现在已经有输出了 for (int step 1; step n; step) { // 正向 int new_i i step * dx[dir]; int new_j j step * dy[dir]; if (new_i 1 new_i n new_j 1 new_j n) { cout ( new_i , new_j ); } // 反向 (step取负) new_i i - step * dx[dir]; new_j j - step * dy[dir]; if (new_i 1 new_i n new_j 1 new_j n) { cout ( new_i , new_j ); } } cout endl; } // 注意这种方法输出顺序可能不是严格的行/列号递增需要额外排序不符合本题要求。 // 因此对于本题方向向量法在输出顺序处理上比较麻烦不如第一种方法直接。 // 这里展示主要是为了介绍方向向量的思想。 return 0; }实操心得方向向量法是算法竞赛中处理网格移动、相邻格遍历的利器。虽然在这道题里因为输出顺序要求显得有点“杀鸡用牛刀”但理解这种抽象思想对后续学习图论搜索、动态规划中的状态转移等至关重要。它把复杂的多方向判断统一成了一个循环结构。鉴于输出顺序的要求在本题中我们不推荐使用上面这种双向延伸的方向向量法因为它输出的坐标顺序是“中心点 - 正向一步 - 反向一步 - 正向两步 ...”不符合题目要求的“数字大小顺序”。但它作为一个教学示例展示了如何将问题抽象化。所以对于这道题最终采纳并推荐的是3.1中的“分步计算法”。它直观、高效且完全符合题意。4. 关键知识点深度剖析与扩展这道题虽然简单但背后涉及的知识点非常基础且重要。我们来深入剖析一下并看看这些知识能如何扩展到更复杂的问题中。4.1 二维数组的索引与数学坐标的映射这是初学者第一个容易混淆的点。在编程中我们常说a[row][col]row是行索引col是列索引。在数学或题目描述中点(i, j)也通常表示第i行第j列。两者本质是统一的。关键在于要明确索引的起始值。本题是从1开始而C数组默认从0开始。如果题目矩阵是从0开始编号我们的循环和条件判断就要相应调整。这种映射关系是处理所有网格类问题的基础。扩展思考如果题目问的是矩阵中某个“子方阵”的同行列对角线呢或者是一个非方阵m×n的矩阵呢我们的算法需要如何调整对于非方阵对角线的定义可能需要明确通常指所有满足row-col或rowcol为常数的点即使它们连成的线看起来不是45度。算法的核心——利用常数关系遍历并判断边界——依然不变。4.2 循环边界与条件判断的严谨性代码中的for (int row 1; row n; row)和if (col 1 col n)是保证程序正确的“守卫”。在编写循环时必须时刻自问循环的起点和终点是否正确是否可能漏掉端点条件判断是否覆盖了所有非法情况这道题提供了一个绝佳的练习场景。常见错误把col n写成col n导致漏掉最后一个点。在对角线遍历中没有进行边界判断直接输出计算出的(row, col)导致输出非法坐标。在控制空格时逻辑写反导致第一个点前多空格或最后一个点后多空格。4.3 利用不变量简化问题对角线上的常数关系这是本题最精华的部分。发现并利用“主对角线上行号减列号为常数”、“副对角线上行号加列为常数”这两个不变量是化繁为简的关键。这体现了数学思维在编程中的重要性。很多复杂的问题都是通过寻找不变量、规律、公式来简化的。扩展应用这种思想在“八皇后问题”中用于快速判断皇后是否在同一对角线在图像处理中可以用来处理像素的斜向扫描在动态规划中某些状态转移可能沿着对角线进行。4.4 输出格式控制的技巧信息学竞赛题目对输出格式要求往往非常严格。多一个空格、少一个换行都可能导致“格式错误”。本题就是一个典型练习。分隔符处理通用方法是使用一个bool isFirst标志。或者也可以将坐标存入一个vectorstring最后用join的方式输出但C标准库没有直接的join函数需要手动实现。换行符cout endl;会在输出换行符的同时刷新缓冲区。在大量输出时使用\n效率更高因为endl的强制刷新可能带来性能开销。但对于本题输出量小两者皆可。5. 调试技巧与常见问题实录即便思路正确实现时也难免遇到问题。下面分享我在教学和解题中学生们遇到的高频问题及解决方法。5.1 问题一对角线坐标计算错误导致漏点或包含非法点症状输出的对角线坐标数量不对或者出现了0或n1这样的非法坐标。诊断与解决推导公式务必从定义出发重新推导。设主对角线上任意点为(r, c)因为和(i, j)在同一条主对角线上所以r - c i - j。要遍历所有合法点最安全的方法是遍历所有可能的行号r(1到n)然后计算c r - (i - j)再判断c是否在1到n之间。不要试图去计算起点和步长那样更容易错。验证边界代入极端情况验证。例如n5, i1, j1左上角主对角线点应为(1,1),(2,2),(3,3),(4,4),(5,5)。你的公式能算出这些吗再试试i2, j3此时i-j -1。遍历r1时c1-(-1)2合法r5时c5-(-1)6非法。判断条件会将其过滤。正确。使用调试输出在计算每个点之前临时输出r和计算出的c观察中间结果。5.2 问题二输出格式错误多空格、少空格或换行不对症状提交后判题系统返回“Presentation Error”输出格式错误。诊断与解决肉眼检查首先将程序输出与题目样例对比。注意行末是否有多余空格这是最常见的错误。题目要求“坐标之间用一个空格隔开”这意味着最后一个坐标后面不能有空格。标准化控制逻辑强烈建议使用firstOutput标志位来控制空格。模板如下bool first true; for (遍历所有要输出的元素) { if (满足输出条件) { if (!first) cout ; cout 元素; first false; } } cout endl; // 这一行结束后换行检查换行确保每一部分行、列、主对角线、副对角线输出完后都换行。是四行输出不是一行。5.3 问题三程序逻辑正确但遇到大数据量如n1000时超时或输出混乱症状本地测试小数据正常提交后可能“Time Limit Exceeded”或输出异常。诊断与解决复杂度分析我们的算法是O(n)的四个循环每个最多n次迭代对于n1000完全在承受范围内。如果超时检查是否有死循环如循环变量写错或者在不该用endl的地方用了导致频繁刷新缓冲区。输入输出效率在C中对于超过10^5数量级的输入输出建议使用scanf/printf或关闭cin/cout的同步流来加速。ios::sync_with_stdio(false); cin.tie(nullptr);本题数据量不大一般不需要。但养成好习惯在竞赛程序开头加上这两句通常无害但之后就不能混用scanf/printf和cin/cout了。输出缓冲区如果输出量巨大使用\n代替endl可以避免不必要的缓冲区刷新提升效率。5.4 问题四理解偏差认为“对角线”只有一条线上的点症状只输出了左上-右下方向或右上-左下方向其中一个方向的点。诊断这是对“同一对角线”的理解问题。在矩阵中从一个点出发有两条对角线主对角线和副对角线。题目要求输出的是所有在同一对角线上的点即两个方向都要包含。解决回顾题目描述确认输出要求。我们的代码中必须有两个独立的模块来处理row-col常数和rowcol常数。为了更直观我将常见问题、原因和解决方法汇总成下表方便快速排查问题现象可能原因解决方案对角线坐标数量不对1. 计算公式推导错误。2. 边界判断条件有误如用了而不是。3. 遍历范围不对如只向一个方向延伸。1. 重新从数学定义推导row ± col constant。2. 使用1 n严格判断。3. 确保遍历所有可能行号(1~n)或使用双向延伸。输出格式错误 (PE)1. 行末有多余空格。2. 各部分输出之间没有换行。3. 括号或逗号格式不对。1. 使用firstOutput标志位控制空格。2. 每部分输出后使用cout endl;。3. 严格对照样例检查输出字符串。结果包含非法坐标(如0,0)边界判断缺失或逻辑错误。在所有生成坐标的地方输出前必须用if判断其是否在[1, n]范围内。程序运行超时 (TLE)1. 算法复杂度高如写了O(n²)的暴力循环。2. 有死循环。3. 输入输出效率低本题一般不会。1. 确保使用O(n)的算法。2. 检查循环变量是否在合理范围内变化。3. 对于大数据可使用scanf/printf或关闭cin/cout同步。只有一条对角线有输出只实现了一种对角线的计算主或副。检查代码确保分别实现了基于row-col和rowcol两种常数关系的遍历。6. 从本题延伸的算法思维与练习建议通过这道“同行列对角线的格”我们巩固了基础但学习不应止步于此。我们可以以此题为跳板探索更广阔的算法世界。6.1 方向数组通往图论搜索的钥匙我们在3.2节简要提到了方向向量(dx, dy)。这是解决网格类问题如迷宫、棋盘、矩阵遍历的超级工具。标准的四方向上、下、左、右和八方向包括对角线数组如下// 四方向上、下、左、右 int dx4[] {-1, 1, 0, 0}; int dy4[] {0, 0, -1, 1}; // 八方向包括对角线 int dx8[] {-1, -1, -1, 0, 0, 1, 1, 1}; int dy8[] {-1, 0, 1, -1, 1, -1, 0, 1};使用方式for (int d 0; d 4; d) { int nx current_x dx4[d]; int ny current_y dy4[d]; if (nx 1 nx n ny 1 ny n) { // (nx, ny) 是一个合法的相邻格 } }建议练习尝试用方向数组重写本题的对角线遍历部分虽然输出顺序是挑战。然后去找一些经典的“迷宫最短路径”、“岛屿数量”、“图像渲染”等问题你会发现方向数组是标配。6.2 预处理与存储当需要多次查询时本题是单次查询。如果题目变成有一个固定的n×n矩阵然后有Q次询问每次给一个(i, j)都要输出它的同行列对角线格子。如果Q很大比如10^5我们每次都用O(n)的方法计算就会超时O(Qn)。这时就需要预处理。我们可以提前计算出每个位置所在的行、列、主对角线、副对角线上所有点的集合。因为矩阵是固定的每个点属于哪一行、哪一列、哪两条对角线是确定的。我们可以用四个二维向量或者数组来存储这些信息。当查询时直接输出预存的结果时间复杂度是O(1) per query。思维扩展这种“空间换时间”的预处理思想在解决“多次查询”类问题时非常有效。例如计算二维前缀和以快速求子矩阵和。6.3 推荐练习题目为了彻底掌握这个知识点我建议按顺序完成以下练习《一本通》基础题完成本章节前后的相关题目如矩阵旋转、矩阵加法等巩固二维数组的基本操作。洛谷B2005字符三角形练习循环与字符输出。洛谷P2615神奇的幻方非常好的二维数组模拟题需要理解并实现规则。洛谷P1162填涂颜色经典的矩阵遍历问题可以用DFS/BFS配合方向数组解决。LeetCode 733图像渲染 (Flood Fill)练习方向数组和搜索的入门题。这道1120题就像一颗投入湖面的石子其涟漪可以波及到数组、循环、搜索、预处理等多个核心概念。编程学习就是这样把每一道简单的题做透、想深比盲目刷很多难题效果要好得多。在实际编码时耐心推导公式、严谨处理边界、细心控制格式这些习惯会让你在解决更复杂问题时更加从容。