
记录一下自己做 OpenJudge NOI 1.8 第 11 题“图像旋转”的过程。说实话这道题在信息学奥赛一本通里算是数组章节的常客难度不大但每次比赛或者练习里遇到“矩阵变换”类题目总有人在这里翻车——要么行列搞反要么下标换错要么输出格式出了毛病。尤其是第一次接触二维数组和坐标映射的同学非常值得停下来好好吃透它。这道题的核心任务很直接给定一个 n 行 m 列的矩阵把它顺时针旋转 90 度后输出。听起来像是“把图转个方向”这么简单可一旦落到代码里涉及的就是二维数组的下标映射、行列互换、循环边界控制这些基本功。我今天把从读题到推导、从 C 实现到 Python 实现、从常见错误到逆时针/180度变式完整梳理一遍给你一份能直接“抄作业”的方案。1. 题目拆解URL、题面和它背后的考点1.1 题面原文与输入输出格式题目本身非常简洁题面大意是输入第一行是两个正整数 n 和 m表示矩阵有 n 行 m 列。接下来 n 行每行 m 个整数组成一个矩阵。要求将这个矩阵顺时针旋转 90 度后输出。输出是一个 m 行 n 列的矩阵每个数字之间用一个空格隔开。举个例子输入3 3 1 2 3 4 5 6 7 8 9期望输出7 4 1 8 5 2 9 6 3这里 n3m3旋转前后都是 3 行 3 列很多人就会忽略“行和列会互换”这个关键点。如果换成 n2、m3 的不规则矩阵输出就会变成 3 行 2 列很多人第一次写就会在这里被卡住。1.2 为什么一道“旋转题”值得单独拎出来写光看题目你可能觉得简单但它在竞赛里的地位不低。一方面它是信息学奥赛一本通里二维数组这一章的经典过渡题前面学完一维数组、二维数组的基本读写突然来一个“矩阵变换”考察的恰恰是你有没有真正理解二维数组的存储逻辑而不只是会照着格式写循环。另一方面“矩阵旋转”是很多算法题的雏形。图像处理里面做图片旋转、游戏开发里做地图旋转、机器学习里做数据增强底层都会用到这种坐标映射的思想。你把这道题的推导过程弄明白了后面遇到“旋转图像”“翻转矩阵”“转置矩阵”这些题目基本就是套模板。所以我建议初学者不要满足于 AC而是要把公式推导和循环边界彻底搞懂。2. 从“看图旋转”到“下标换位”公式是怎么来的2.1 先拿 3x3 矩阵找感觉拿到这类题第一步不是写代码而是在草稿纸上画图。我习惯先在纸上写一个 3 行 3 列的矩阵再在旁边画一个旋转后的结果然后用箭头标出每个元素从旧位置到新位置的路径。以输入矩阵 A 为例A[0][0]1 A[0][1]2 A[0][2]3 A[1][0]4 A[1][1]5 A[1][2]6 A[2][0]7 A[2][1]8 A[2][2]9顺时针旋转 90 度后得到B[0][0]7 B[0][1]4 B[0][2]1 B[1][0]8 B[1][1]5 B[1][2]2 B[2][0]9 B[2][1]6 B[2][2]3仔细观察原矩阵的第一行1 2 3旋转后变成了 B 的最后一列而且顺序是从上到下1, 2, 3变成 B[0][2]、B[1][2]、B[2][2]。原矩阵的最后一列3 6 9旋转后变成了 B 的第一行但顺序变了7 4 1。你会发现旋转前后每个元素的移动其实很有规律只是“行变成列、列反向变成行”。这个规律不要用眼睛硬记要用坐标算。2.2 坐标映射公式推导我们要做的是建立 A[i][j] 和 B[x][y] 之间的对应关系其中 A 是 n 行 m 列B 是 m 行 n 列。思路是这样把矩阵旋转想象成把一张纸顺时针转动。原来在第 i 行、第 j 列的元素旋转之后到了新矩阵的哪一行哪一列从上面的例子能看出两个规律第一原矩阵的第 j 列经过顺时针旋转 90 度后变成了新矩阵的第 i 行吗不对我这里有更好的归纳方式。让我们逐个观察坐标变化还是用刚才的 3x3 例子A[0][0]1原来在左上角旋转后到右上角即 B[0][2]A[0][1]2原来在第一行中间旋转后到 B[1][2]A[0][2]3原来在右上角旋转后到右下角即 B[2][2]A[1][0]4原来在中间行左边旋转后到 B[0][1]A[2][0]7原来在左下角旋转后到左上角即 B[0][0]把这几组对应关系列出来原位置 A[i][j]新位置 B[x][y]A[0][0]B[0][2]A[0][1]B[1][2]A[0][2]B[2][2]A[1][0]B[0][1]A[2][0]B[0][0]先看行坐标A[0][0] 到了 B 的第 0 行A[1][0] 到了 B 的第 0 行A[2][0] 到了 B 的第 0 行。原矩阵列下标 j0 的元素都到了 B 的第 0 行再看 j1 的 A[0][1]到了 B 的第 1 行j2 的 A[0][2]到了 B 的第 2 行。所以新矩阵的行坐标就是原矩阵的列坐标x j。再看列坐标原矩阵行下标 i0 的元素A[0][0] 到 B 的第 2 列A[0][1] 到 B 的第 2 列A[0][2] 到 B 的第 2 列。也就是说i0 的时候y n-1-i 3-1-0 2。i1 的时候A[1][0] 到 B 的第 1 列y 3-1-1 1。i2 的时候A[2][0] 到 B 的第 0 列y 3-1-2 0。于是公式就出来了A[i][j] 旋转后应该放到 B[j][n-1-i]也就是B[j][n-1-i] A[i][j]这个公式对任意 n 行 m 列的矩阵都成立。旋转后的 B 有 m 行 n 列因为 j 的取值范围是 0 到 m-1n-1-i 的取值范围是 0 到 n-1。我一开始学的时候死活记不住这个式子后来发现一个更直观的记忆方式顺时针旋转 90 度其实就是原矩阵的“列”变成新矩阵的“行”原矩阵的“行”倒过来变成新矩阵的“列”。原数组的第 i 行第 j 列元素会跑到新数组的第 j 行第 n-1-i 列。你看行坐标直接交换成列坐标列坐标变成倒过来的行坐标。2.3 从公式到循环两种遍历方向公式推导出来之后写循环就有两种思路。思路一遍历原矩阵把每个元素放到新位置外层循环遍历 i原矩阵的行内层循环遍历 j原矩阵的列每次把 A[i][j] 赋给 B[j][n-1-i]。这样写的好处是不用关心目标矩阵的遍历顺序只要保证每个元素都被“搬”到正确的位置即可。思路二遍历新矩阵反推原坐标外层循环遍历 i新矩阵的行0 到 m-1内层循环遍历 j新矩阵的列0 到 n-1通过逆公式 B[i][j] A[n-1-j][i] 来取原矩阵中的值。这种写法不需要额外的矩阵来暂存直接按新矩阵的行列顺序去原矩阵里取对应值输出就行。两种思路都能 AC但我个人更推荐第一种因为它更贴近“模拟旋转过程”逻辑上不容易出错调试的时候也方便。第二种适合在空间受限或者你想省一个二维数组的时候用。3. 代码实现C 和 Python 各来一份3.1 C 标准写法先上我最常用的 C 写法。定义两个二维数组一个存原矩阵一个存旋转后的矩阵。需要注意数组大小要开够免得下标越界。#include iostream using namespace std; const int MAXN 105; int a[MAXN][MAXN], b[MAXN][MAXN]; int main() { int n, m; cin n m; for (int i 0; i n; i) { for (int j 0; j m; j) { cin a[i][j]; } } // 顺时针旋转90度 for (int i 0; i n; i) { for (int j 0; j m; j) { b[j][n - 1 - i] a[i][j]; } } // 输出旋转后的矩阵注意行列数是 m 行 n 列 for (int i 0; i m; i) { for (int j 0; j n; j) { if (j 0) cout ; cout b[i][j]; } cout endl; } return 0; }这里有几个细节想特别提醒第一MAXN最好根据题目数据范围来定。如果题目没说范围通常开 105 或 1005 足够。有些题 n、m 可以到 1000这时你开 105 就会越界程序可能运行出错或者输出乱码。建议看题面实在不行就开vectorvectorint动态分配最稳妥。第二输出的时候要控制空格。我习惯的判断方式是if (j 0) cout ;这样每行末尾不会多一个空格。OpenJudge 的判题系统通常不 Care 行末空格但有些 OJ 会严格要求所以最好还是养成严谨的格式习惯。第三旋转后矩阵是 m 行 n 列而不是 n 行 m 列。这个太容易写错了尤其是当 n 不等于 m 的时候一旦搞反输出矩阵的维数就不对看起来就像是“整个程序只输出了前几行”很多人会误以为旋转公式错了其实只是输出循环边界写错。3.2 Python 实现与注意点如果你用 Python 刷题那么可以用列表推导式来简化代码。但要注意Python 里创建二维列表有一个经典陷阱不要用[[0] * n] * m这种方式因为这样创建出来的每一行是同一个对象的引用改一个其他行也会跟着变。正确做法是用列表推导式[[0] * n for _ in range(m)]。n, m map(int, input().split()) a [list(map(int, input().split())) for _ in range(n)] # 初始化旋转后的矩阵m 行 n 列 b [[0] * n for _ in range(m)] for i in range(n): for j in range(m): b[j][n - 1 - i] a[i][j] for row in b: print( .join(map(str, row)))这段代码和 C 版本逻辑完全一致。Python 写起来更简洁但有一个地方要特别留意input().split()默认按照空白字符分割能处理多个连续空格这点不用担心。但如果输入数据特别大比如 1000x1000 的矩阵用 Python 的for _ in range(n)读入可能会有点慢这时候可以考虑用sys.stdin.read()一次性读入再切分。另外b [[0] * n for _ in range(m)]这里的行列不要写反。很多人习惯写[[0] * m for _ in range(n)]结果后面赋值和输出时矩阵形状不对程序会直接 IndexError。3.3 一维数组的另类思路有些题目限制了数组维数或者你想省点空间也可以用一维数组模拟二维矩阵。原理很简单假设原矩阵有 n 行 m 列那么元素 A[i][j] 在一维数组中的位置是i * m j。旋转后矩阵有 m 行 n 列元素 B[x][y] 在一维数组中的位置是x * n y。核心公式变为b[j * n (n - 1 - i)] a[i * m j];如果理解了这个映射你会发现“一维数组还是二维数组”只是存储方式不同坐标转换的思维完全一样。这在某些内存极小的竞赛环境里很有用日常刷题用二维数组就足够了。4. 那些年踩过的坑常见错误与调试技巧4.1 最容易翻车的三个点我在带学弟学妹刷题的时候发现这道题的错误主要集中在三个地方。第一个是行列搞反。原矩阵 n 行 m 列旋转后是 m 行 n 列。很多人在输出的时候惯性写成 n 行 m 列结果题目用例是 3x3 时没问题一换成 2x3 或者 3x2 就立刻出错。我的建议是拿到题目先圈出“输出的是 m 行 n 列”这个关键信息写循环之前先在注释里标明变量含义。第二个是下标公式用错。最常见的是把B[j][n-1-i]写成B[n-1-i][j]或者写成B[j][i]。前者是把“行坐标变成列坐标”这个步骤漏掉了后者是完全没处理反向。每次写完拿一个 3 行 4 列的矩阵在草稿纸上手动走一遍基本能当场发现。第三个是二维数组开小了。有些同学看到题目样例只有 3 行 3 列就开a[5][5]结果数据范围其实是 1 到 100。运行时下标越界在 C 里不一定报错可能只是悄悄覆盖了内存里的其他数据最后输出一串神秘数字。建议养成习惯数组至少比最大值大 5或者干脆开到 505、1005。4.2 用“手动走查”代替盲目调试如果你提交后答案是错的先别急着改代码。我的调试方法很简单把题目的样例输入复制出来在草稿纸上画一个 2 行 3 列的小矩阵然后用手算一遍旋转结果再拿你的程序输出对比。比如输入2 3 1 2 3 4 5 6人工旋转一下顺时针 90 度之后变成4 1 5 2 6 3如果你程序的输出是4 1 5 2 6 3那就说明主体逻辑没问题可能问题出在别的测试数据上。如果输出完全对不上就看第一个错位的元素是从哪个坐标来的倒推它被赋值到了哪里。这个方法虽然原始但往往比盯着代码发呆高效得多。还有一个很实用的小技巧在关键循环里临时加一行cout i i j j - x j y n-1-i endl;把每个元素的去向打印出来。这样你能直观看到坐标映射是否符合预期。调试完记得删掉这些输出。4.3 关于输入格式的补充说明OpenJudge 的题目有时候会在数据中夹杂空行尤其是从文件复制过来的样例数据。用cin n m这种流式读取会自动跳过空白字符所以一般不用担心空行问题。但如果你用scanf(%d, n)配合判定 EOF也要注意处理可能多出来的换行符。Python 这边如果用input().split()遇到空行会报错可以用sys.stdin.read().split()把所有数字一次性读出来再切片分配对刁钻数据会更稳。5. 举一反三逆时针、180度与转置5.1 逆时针旋转 90 度既然会了顺时针逆时针其实是一个道理。原矩阵 A[n][m]逆时针旋转 90 度后结果仍然是 m 行 n 列但坐标映射变成了C[m-1-j][i] A[i][j]拿 3x3 验证一下A[0][2]3 应该到结果矩阵的第 0 行第 0 列。m3j2所以 m-1-j0i0C[0][0]3和手动逆时针旋转的结果一致。如果你不想记两个公式也可以投机取巧逆时针旋转 90 度等价于顺时针旋转 270 度也就是先顺时针转 90 度再顺时针转 180 度。但实际写代码时直接用逆时针公式最省事。5.2 旋转 180 度旋转 180 度更简单行列都不互换只是行和列都反向D[n-1-i][m-1-j] A[i][j]结果矩阵仍然是 n 行 m 列。这个变换在很多题目里也会出现比如判断矩阵是否中心对称或者做图像翻转。它的规律很好记上下颠倒之后再左右颠倒。5.3 从“旋转”到“转置”的思维升级接下来我想多说一个进阶点转置。转置不是旋转它只是把矩阵的行和列互换但不做反向。公式是E[j][i] A[i][j]顺时针旋转 90 度和“先转置再水平翻转”是等价的。换句话说转置A[i][j] - A[j][i]水平翻转A[i][j] - A[i][m-1-j]先转置再水平翻转得到顺时针旋转 90 度的结果为什么说这个思维升级很重要因为真实的图像旋转算法里往往不是暴力地把每个像素搬运一遍而是先做转置再做镜像翻转这样可以借助缓存局部性提高效率。竞赛虽然不考这个但你要是能理解这层关系以后看更复杂的矩阵题会轻松很多。我把这三种常用变换整理成了一张表方便你复习变换类型坐标映射公式结果矩阵尺寸典型应用顺时针 90 度B[j][n-1-i] A[i][j]m 行 n 列图像旋转、地图旋转逆时针 90 度C[m-1-j][i] A[i][j]m 行 n 列图像镜像处理旋转 180 度D[n-1-i][m-1-j] A[i][j]n 行 m 列中心对称判断转置E[j][i] A[i][j]m 行 n 列矩阵运算基础5.4 更深一步原地旋转的思路如果你遇到的是“要求原地旋转”的题目也就是不允许使用额外数组那就复杂一些了。以 n x n 的方阵为例顺时针旋转 90 度可以通过四元素轮转完成。思路是把矩阵分成若干层从外到内每层再分成若干组每组四个元素循环互换位置。这一层一层的逻辑很有意思但超出了这道题的范畴我建议把基础的搬运式写法吃透之后再挑战原地旋转。不过有一点可以提醒方阵的原地旋转和长方阵的旋转处理方式不太一样。方阵转完还是 n 行 n 列可以通过多次交换实现长方阵原地旋转通常需要借助转置加翻转的组合操作或者干脆申请新数组竞赛里一般不刁难你去原地旋转不等长矩阵所以考试时优先考虑新开一个数组的写法。6. 刷题路上的心得体会最后以一个过来人的身份说几句。像“图像旋转”这种题在信息学奥赛的题单里属于那种“会者不难、难者不会”的过渡题。它没有高深的算法也不涉及复杂的数据结构但它考察的是你能不能把一个直观的几何操作转化成精确的数学表达。很多同学学到后面卡壳不是栽在动态规划和图论上而是栽在这种最基础的坐标变换和边界条件上。我自己的习惯是每刷一道矩阵题就在笔记里画一个 2 行 3 列的小例子然后手动把旋转、翻转、转置的结果都写一遍再把坐标变化规律记在公式旁边。这样积累几道题之后你会发现所有矩阵变换题都是同一套思维模型找到原坐标和新坐标之间的映射关系剩下的就是两个循环的事。这道题本身 AC 只算第一步真正有价值的是把顺时针旋转公式、逆时针旋转公式、转置公式放在一起对比记忆。等你以后做到图像处理相关的题目再回头看今天这几十行代码会觉得这段基础打得特别值。