
1. 问题引入从棋盘到矩阵的思维转换最近在带学生刷《信息学奥赛一本通》的题目讲到1120这道“同行列对角线的格”时发现不少初学者会卡在一个看似简单实则关键的思维转换上。题目本身并不复杂给定一个n×n的棋盘或者说矩阵再给定一个棋子的位置(i, j)要求输出所有与这个棋子在同一行、同一列、以及两条对角线上的格子坐标。很多同学一看题目描述觉得这不就是几个循环的事儿吗上手一写却发现对角线上的格子找不全或者输出的顺序不符合题目要求又或者边界条件处理得一塌糊涂。这道题的价值远不止于写出正确的循环。它本质上是一道绝佳的“坐标思维”训练题强迫你从生活化的“棋盘”概念精准地切换到程序化的“二维数组索引”世界并熟练掌握基于坐标的数学规律推导。今天我们就来彻底拆解这道题不仅告诉你“怎么做”更要讲清楚“为什么这么做”以及在实际编码和竞赛中如何避免相关的坑。2. 题目核心需求与输入输出规范解读在动手写任何一行代码之前我们必须像解数学题一样把题目的每一个条件“翻译”成编程语言能理解的需求。这是避免后期反复修改和调试的关键。2.1 输入格式的确定性题目通常会明确输入格式三个整数依次为棋盘大小n以及棋子所在的行号i和列号j。这里有一个至关重要的细节也是竞赛题目常见的陷阱行列的编号是从1开始还是从0开始对于《信息学奥赛一本通》的这类题目除非特别说明否则通常约定棋盘的左上角为(1, 1)右下角为(n, n)。这意味着我们的二维数组索引需要与这个坐标系对齐。如果你习惯性地用C数组的0下标开始思考就会导致整个坐标计算系统偏移结果全错。所以我们第一步就要建立心理映射题目中的(i, j)直接对应我们程序中用于存储或遍历的坐标值不需要进行(i-1, j-1)的转换除非你用了0起始的数组但用题目坐标做计算那会更混乱。最清晰的做法是全程使用题目约定的1-based坐标系进行思考和计算输出时也直接输出这个坐标值。2.2 输出顺序的隐含规则输出要求分为三部分同一行的所有格子。同一列的所有格子。两条对角线上的所有格子。每一部分内部格子按什么顺序输出题目描述可能不会明说但根据在线评测系统OJ的惯例和常见要求通常默认按照坐标递增的顺序输出。对于行就是列号从小到大对于列就是行号从小到大。对于对角线情况稍微复杂一些但核心原则是确保你的输出顺序是确定且一致的这样才容易通过评测。一个安全的策略是在收集对角线上的坐标时就有意识地按照某种顺序例如从左上到右下或从矩阵一端到另一端进行遍历和存储然后统一输出。很多同学在这里失分不是因为找不到点而是因为输出顺序混乱与评测机的标准答案不匹配。2.3 边界条件与数据范围n的取值范围是多少题目没说但我们要有防御性编程的意识。在竞赛中n可能从1到100甚至到1000。这意味着我们的算法必须是通用的。特别要注意n1这种边界情况此时棋盘只有一个格子(1,1)。那么这个格子与自身在同一行、同一列、也在两条对角线上虽然对角线退化了。你的程序是否能正确处理输出会不会多出一些不存在的格子例如在寻找“左下到右上”的对角线时计算出的坐标可能超出[1, n]的范围必须过滤掉。任何涉及坐标加减运算的地方都必须立刻加上范围判断这是写出鲁棒性代码的基本习惯。3. 算法设计与核心逻辑拆解理解了需求我们就可以设计具体的解决路径了。这道题最好的解法就是模拟但模拟的思路有优劣之分。3.1 同行与同列单层循环的直白实现同行和同列的处理是最简单的它考察的是对二维坐标其中一个维度固定、另一个维度遍历的理解。同一行的所有格子行号固定为i列号col从1遍历到n。输出(i, col)。注意这里包含了棋子自身(i, j)。是否需要跳过自身题目要求输出“所有格子”通常包括自身。如果跳过反而可能出错。所以直接遍历输出即可。// 输出同一行 for (int col 1; col n; col) { cout ( i , col ) ; }同一列的所有格子同理列号固定为j行号row从1遍历到n。输出(row, j)。// 输出同一列 for (int row 1; row n; row) { cout ( row , j ) ; }这部分逻辑清晰几乎不会出错。关键在于输出格式要符合题目要求比如每个坐标用括号括起来或者用空格隔开需要仔细看题。3.2 两条对角线寻找坐标变化的数学规律这是本题的核心难点。棋盘上有两条对角线一条是“左上-右下”方向的主对角线另一条是“右上-左下”方向的副对角线或称反对角线。3.2.1 主对角线左上-右下方向这条线上的点有什么特征它们的行索引与列索引的差值是常数。更准确地说对于主对角线有行号 - 列号 常数。对于给定的点(i, j)这个常数就是i - j。 那么如何找到这条线上所有的点呢我们不能无限制地找下去因为棋盘是有限的。方法是从一个端点开始沿着对角线方向走到另一个端点。方向向量沿主对角线移动行号和列号每次同时加1向右下或同时减1向左上。寻找起点我们要找到这条对角线上行号最小或列号最小的那个点作为遍历的起点。这个点的坐标是(r_start, c_start)。如何计算从(i, j)出发不断同时减1直到行号或列号碰到边界1。所以起点行号r_start i - min(i-1, j-1)。因为i-1是向上走到边界1的步数j-1是向左走到边界1的步数我们只能取最小值否则另一个坐标就会小于1。简化一下r_start i - min(i-1, j-1) max(1, i - (j-1))其实更直观的方法是r_start i - min(i-1, j-1)。同理c_start j - min(i-1, j-1)。更简单且不易错的思路是直接计算起点。从(i,j)向左上走每次行、列减1直到其中一个等于1。所以start_step min(i-1, j-1)。那么起点就是(i - start_step, j - start_step)。遍历到终点从起点(r_start, c_start)开始每次行、列同时加1直到行号或列号超过n。循环条件while(r_start n c_start n)。用代码实现就是// 输出主对角线左上-右下 int r i, c j; // 先向左上走到起点 while (r 1 c 1) { r--; c--; } // 从起点向右下遍历输出 while (r n c n) { cout ( r , c ) ; r; c; }3.2.2 副对角线右上-左下方向这条线上的点特征不同它们的行索引与列索引的和是常数。对于点(i, j)常数和为i j。方向向量沿副对角线移动行号加1、列号减1向左下或者行号减1、列号加1向右上。寻找起点我们要找这条线上行号最小或列号最大的点作为起点。从(i, j)出发可以向右上走行减1列加1直到行碰到1或者列碰到n。向右上走的步数受限于最多走i-1步行到1或者n-j步列到n。所以start_step min(i-1, n-j)。起点坐标为(i - start_step, j start_step)。遍历到终点从起点开始向左下方向遍历行加1列减1直到行超过n或者列小于1。代码实现// 输出副对角线右上-左下 r i, c j; // 先向右上走到起点 while (r 1 c n) { r--; c; } // 从起点向左下遍历输出 while (r n c 1) { cout ( r , c ) ; r; c--; }注意这里演示的遍历方法是从端点开始找到所有点。也有另一种思路利用ij和i-j为常数的特性直接用一个循环遍历行号r然后根据关系式c i j - r对于副对角线或c r - (i - j)对于主对角线计算出列号c再判断c是否在[1, n]范围内。这种方法更简洁但需要对公式理解更深。上面介绍的“走到端点再遍历”的方法图形化思维更强更容易理解和调试。4. 代码实现、调试与常见“坑点”有了清晰的算法逻辑我们就可以着手编写代码了。但在将思路转化为C代码的过程中有几个细节必须高度重视否则极容易失分。4.1 完整代码框架与输入输出处理首先搭建一个稳健的程序框架。#include iostream using namespace std; int main() { int n, i, j; cin n i j; // 输入棋盘大小和棋子位置 // 第一部分输出同一行的格子 // 第二部分输出同一列的格子 // 第三部分输出主对角线上的格子 // 第四部分输出副对角线上的格子 return 0; }输入处理很简单。输出部分题目通常要求每行输出一组结果所有同行、同列、两条对角线的格子或者每组结果内部用空格隔开。务必严格按照题目要求的格式输出多一个少一个空格、换行符都可能导致“格式错误”。一个技巧是在循环内输出每个坐标时先输出坐标再判断如果不是最后一个坐标就输出一个分隔符空格。对于行、列这种规则遍历可以方便地判断col n但对于对角线由于我们是动态遍历判断“是否是最后一个点”稍微麻烦点。更通用的方法是将坐标先存入一个vector或字符串中最后统一输出这样可以轻松控制格式。4.2 易错点分析与排查清单即使逻辑正确下面这些坑点也足以让你在OJ上提交多次才能通过坐标范围校验缺失这是最大的坑。特别是在使用“公式计算法”求对角线坐标时计算出的列号c可能为0、负数或大于n。任何通过计算得到的坐标(r, c)在输出或使用前必须判断是否满足1 r n 1 c n。我们的“端点遍历法”在循环条件里已经做了判断相对安全。输出顺序不一致评测机是死板的它对比你的输出和标准答案是否完全一致包括顺序。如果你寻找对角线格子的顺序是“先左上后右下”但标准答案是“从左上角第一个点开始顺序输出”你的顺序可能就不对。确保你的遍历顺序是确定的、可描述的。通常按照“行号从小到大”的顺序输出对角线上的点是一个安全的选择。在我们的遍历法中起点就是该对角线上行号最小的点然后行号递增遍历自然符合这个顺序。重复输出自身点在行、列、两条对角线的输出中点(i, j)会被输出四次。这通常是题目允许甚至要求的输出所有格子。不要主动去重除非题目明确说明“不包含自身”。一切以题目描述为准。边界条件n1当n1时棋盘只有一个点(1,1)。你的程序应该输出第一行(1,1)第二行(1,1)第三行(1,1) // 主对角线第四行(1,1) // 副对角线 检查你的对角线查找逻辑在n1时是否会进入死循环或者计算出无效坐标。我们的while (r 1 c 1)在n1时条件为假不会进入循环直接从(1,1)开始输出是正确的。输入坐标非法题目一般保证输入的i, j在[1, n]范围内。但养成防御性编程的习惯没有坏处可以简单判断一下如果非法可以给出提示虽然OJ可能不检查这个。4.3 调试技巧与测试用例设计自己如何验证程序是否正确设计几个有代表性的测试用例常规情况n5, i3, j3。中心点两条对角线都很完整。角落情况n5, i1, j1。左上角点主对角线完整副对角线只有一部分。边缘情况n5, i2, j5。右边框的点。最小情况n1, i1, j1。较大nn10, i6, j4。测试程序在稍大数据下的正确性。对于每个测试用例最好先在纸上画出棋盘标出点(i, j)然后手动找出所有同行、同列、对角线的点并确定输出顺序。再运行你的程序对比输出是否一致。这是最有效的调试方法。5. 算法优化与思维拓展解决基础问题后我们可以思考一下这道题还有没有更深层次的价值能否引申出更通用的技巧5.1 从模拟到公式提升效率与理解我们之前用了“走到端点再遍历”的方法直观但代码稍长。我们可以直接利用对角线的数学特征来生成所有点无需寻找端点。对于主对角线左上-右下所有点满足r - c i - j。我们可以遍历所有行号r(1到n)然后计算c r - (i - j)。接着判断计算出的c是否在1到n之间。这样我们自然得到了按行号排序的点。// 输出主对角线公式法 int diff i - j; // 行号与列号的差是定值 for (int r 1; r n; r) { int c r - diff; // 根据关系式 c r - diff if (c 1 c n) { cout ( r , c ) ; } }这种方法代码更简洁并且天然按行号递增顺序输出符合常见的输出要求。对于副对角线右上-左下所有点满足r c i j。遍历行号r计算c i j - r再判断范围。// 输出副对角线公式法 int sum i j; // 行号与列号的和是定值 for (int r 1; r n; r) { int c sum - r; if (c 1 c n) { cout ( r , c ) ; } }公式法的优势思维更数学化代码极其简洁不易在寻找起点和方向的逻辑上出错。需要注意的坑必须对计算出的每一个c进行范围校验这是公式法的核心步骤绝对不能省略。5.2 举一反三此类问题的通用解决模式这道题代表了一类“基于二维坐标规则查找”的问题。其通用解决模式可以总结为问题转化将问题场景棋盘、矩阵、地图抽象为二维坐标系。规律抽象分析目标点集同行、同列、对角线、特定形状在坐标系中的数学规律。是某个坐标固定还是坐标之间存在线性关系如和固定、差固定或者是更复杂的函数关系遍历与筛选固定型行、列固定一个维度遍历另一个维度。关系型对角线遍历一个维度如行号利用关系式推导另一个维度然后进行范围筛选。复杂路径型可能需要使用方向数组如八方向int dx[] {-1, -1, -1, 0, 0, 1, 1, 1}; int dy[] {-1, 0, 1, -1, 1, -1, 0, 1};进行广度优先搜索(BFS)或深度优先搜索(DFS)。边界处理任何计算出的坐标在使用前必须检查是否在有效范围内。这是保证程序鲁棒性的铁律。输出控制严格按照题目要求的格式组织输出注意顺序和分隔符。掌握了这个模式你就能应对诸如“马踏棋盘”按日字形找点、“国王的视野”找八个方向的第一个棋子等更复杂的问题。5.3 在竞赛中的实战意义在信息学竞赛中这道题属于入门级的模拟题。它的主要考察点不在于算法的高深而在于选手的基本功是否扎实循环控制能否正确写出遍历的起止条件。边界判断是否有强烈的意识去检查数组越界。坐标计算能否在二维空间中熟练进行坐标运算。格式化输出能否耐心处理看似琐碎的输出格式要求。很多更复杂的算法如图论中的网格BFS、动态规划中的状态转移其基础都是对二维坐标的精确操作。这道题做不好后面遇到复杂问题很容易在细节上崩盘。因此务必通过这道题把“坐标处理”和“边界判断”这两个肌肉记忆练到形成条件反射的程度。