“图形”这道题放在蓝桥杯C组程序题的C题位置看起来平平无奇好像只是让输出一张图。但真的上手做过一遍就会发现这道题考的东西非常集中观察规律、二维数组基本功、边界条件控制以及一个容易被忽略的复杂度意识。文章就从这几点展开把这道题的完整思路和多种写法都过一遍适合正在准备蓝桥杯、或者其他算法竞赛入门阶段的朋友参考。我在拿到题目之后先试了最直观的“圈地”写法写完又回头推了一个更快的公式解法。两种方案各有适用场景下面会一一拆开讲包括代码、踩坑点以及如果题目变形成“查询某一点的值”该怎么处理。1. 先搞清楚题目在考什么从“图形”两个字拆出核心考点1.1 这类题的典型长法常见版本是这样的输入一个正整数 n要求输出一个 n 行 n 列的矩阵数字从外圈向内圈依次递增同一个“圈”上的所有格子数字相同。以 n 5 为例输出长这样1 1 1 1 1 1 2 2 2 1 1 2 3 2 1 1 2 2 2 1 1 1 1 1 1而 n 1 的时候只有一行一个数什么圈子都不用画直接输出 1。这道题之所以叫“图形”是因为它把数学规律藏在了一张视觉上很规则的图里你需要做的就是把这张图的构成规则翻译成代码逻辑。1.2 题目背后真正想考察的能力很多同学看到“输出图形”四个字第一反应是找循环嵌套觉得只要想办法把星号或者数字打出来就行。这种理解其实不够。至少从这道题出发题目考察的不只是打印还有四层东西第一层是观察规律。你得先能在草稿纸上画出 n 1、n 2、n 3、n 4 的小图盯着它们看几秒钟意识到“数字是由外到内一层一层增加的”。这个观察动作本身在图形类题目里占了将近一半的分数。第二层是二维数组的熟练度。很多输出图形的题看起来可以“一边循环一边打印”不需要数组。但回形矩阵这类题不一样你很难通过一行一行的纯粹公式打印把图形画对因为中间位置的数字取决于它距离四条边的远近。所以一个相对高效的做法是先把结果写进二维数组再整体输出。能不能顺畅地操作二维数组决定了你写出来的代码是十分钟稳定通过还是半小时反复调试。第三层是边界控制。从外圈到内圈每一圈都有上、下、左、右四条边循环条件稍微写错一个符号就会出现数组越界、死循环、输出少一行多一行等问题。这一层在面试和比赛中都很容易成为拉开差距的地方。第四层是复杂度意识。如果 n 只有 100O(n²) 空间和 O(n²) 时间完全没问题。但如果题目要求回答的是“n 10⁹ 时某个坐标上的数字是多少”你就不能老老实实开一个二维数组了。这道 C 题按正常难度走n 不会大到离谱但你写答案的时候必须明白你用的算法在极限数据下是否还成立。2. 最稳的暴力解法四指针一圈一圈地填2.1 思路把图形拆成洋葱皮回形矩阵给人的第一感觉是“一层又一层”像洋葱。最外层是一圈 1往里一层是一圈 2再往里是自己的圈子。所以最直观的写法就是维护四个边界变量上边 top、下边 bottom、左边 left、右边 right然后每一轮循环填一整圈。每填完一圈四个边界就往中心收缩一格填充数字加 1。循环继续的条件是 top bottom 且 left right也就是说矩阵里还有位置没有填。还是拿 n 5 举例。第一轮填充时top 0、bottom 4、left 0、right 4数字 1。第二圈往里缩数字 2。第三圈剩正中间一个点数字 3。四轮循环结束整个矩阵填完。这个思路最大的优势是“好想”。它不需要任何公式推导只要懂二维数组和循环就能写出来。而且只要边界控制不出错结果一定正确。对于竞赛里数据范围比较温和的题这种暴力写法完全够用。2.2 完整代码和边界细节下面给出一个标准 C 实现。这里我用 vector 建二维数组避免手工 new 一堆数组时出现悬空指针或者内存泄漏问题在比赛中也更规范。#include iostream #include vector using namespace std; int main() { int n; cin n; vectorvectorint a(n, vectorint(n, 0)); int top 0, bottom n - 1; int left 0, right n - 1; int num 1; while (top bottom left right) { // 上边从左到右 for (int j left; j right; j) { a[top][j] num; } // 右边从上到下 for (int i top 1; i bottom; i) { a[i][right] num; } // 下边从右到左 if (top bottom) { for (int j right - 1; j left; --j) { a[bottom][j] num; } } // 左边从下到上 if (left right) { for (int i bottom - 1; i top; --i) { a[i][left] num; } } top; --bottom; left; --right; num; } for (int i 0; i n; i) { for (int j 0; j n; j) { if (j) cout ; cout a[i][j]; } cout \n; } return 0; }代码里有两个 if 判断特别容易漏掉漏掉之后后果还挺隐蔽。第一个是if (top bottom)它的作用是防止单行矩阵时“下边”这条循环把刚填过的格子重复覆盖。比如 n 1只有一行一列如果直接执行右下角向左的循环就会把数字填回已经填过的位置数据看着不错实际上整个矩阵就乱了。第二个是if (left right)同理防止单列时“左边”这条循环从下往上再次覆盖。为什么要加这两个判断因为我们的循环条件是四个边界只要还有空间就继续。当矩阵只剩一行时上下边界相等但它仍然满足 top bottom。此时“上边”已经把这一行填满了“下边”如果再执行一次就会把同一条边重新填一遍视觉上没有差别但如果你在后面做了累加或者额外判断就麻烦了。这个习惯值得养成圈形填充的题目四个方向里只有第一个方向是必然要执行的其他方向都要根据是否还有“内圈空间”来决定。2.3 这个方法适合在什么情况下用模拟法适合“要输出整个矩阵”的题目而且 n 不能太大。因为它的时间和空间复杂度都是 O(n²)。当 n 500 时完全没问题n 1000 时也能接受n 10000 时不可行——先不说内存要 100MB 级别光是输出那一堆数字时间就非常紧张了。所以我在做题时习惯把模拟法当作“第一版答案”先用它跑通了逻辑拿几个小样例验证然后再看题目到底需不需要优化。很多时候验证答案正确之后模拟法本身就已经足够提交了。但如果你想在同类题里走得更远那下一步的公式解法就很有必要。3. 一个公式直接算出任意位置的数值3.1 关键规律数字等于到最近边界的距离如果你把 n 5 的回形矩阵拿出来仔细看会发现一个很有意思的现象每一条外圈上的数字都是 1往里推进一格数字变 2再往里变 3。换句话说某个位置的数字取决于它离上边界、下边界、左边界、右边界这四个方向中最近的哪一个。离得越近数字越小。把这句话再翻译成坐标就得到一个非常简单的关系。假设矩阵的行列下标都从 0 开始位置为 (i, j)那么它到四条边的距离分别是到上边的距离i到下边的距离n - 1 - i到左边的距离j到右边的距离n - 1 - j取这四个距离的最小值再加 1就是这一层的数字。level min(i, j, n - 1 - i, n - 1 - j) 1为什么最后要加 1因为最外圈的格子到某条边的最小距离是 0但我们希望它显示 1而不是 0。这个 1 只是把距离的空间映射到数字空间。我自己当时发现这个规律的方式比较笨就是画了一张 n 6 的图把每个位置的数字写下来然后在旁边标上它距离上边、下边、左边、右边的距离看了几分钟才发现端倪。画图的过程看起来很浪费时间但对于培养“图形题的感觉”非常值得。3.2 不用数组直接打印的代码有了公式以后你甚至不需要二维数组。每一行循环过去每个位置用公式算一下直接输出就行。空间复杂度直接降到了 O(1)。#include iostream #include algorithm using namespace std; int main() { int n; cin n; for (int i 0; i n; i) { for (int j 0; j n; j) { int level min(min(i, j), min(n - 1 - i, n - 1 - j)) 1; if (j) cout ; cout level; } cout \n; } return 0; }这里我用了min函数嵌套一行代码把四个距离的最小值都算出来。代码看着短但这类写法你一定要在本地跑一下样例确认输出格式和题目完全一致。有些比赛题要求每行末尾不能有空格有些要求每两个数之间一个空格各不一样。我上面的代码统一用“第一个数前面不加空格后面的数前面加一个空格”的方式来处理这样行尾不会出现多余空格比较稳妥。3.3 任意坐标点查询这才是公式的隐藏价值如果题目再往前推一点不让你完整输出整个矩阵而是问你“第 x 行第 y 列的数字是多少”这时候模拟法就彻底没用了因为建完整矩阵的成本太高。而公式法只算那一个点复杂度是 O(1)。这里要特别注意一个坐标换算问题。题目里如果给出的行号和列号是从 1 开始数的你必须在代码里先减 1再带入公式。很多人栽在这一点上用了下标从 0 开始写的公式却直接往里面传从 1 开始的输入值结果全部错位。我通常会把查询逻辑封装成一个函数统一处理从 1 开始的输入int query(int n, int x, int y) { int i x - 1; int j y - 1; return min(min(i, j), min(n - 1 - i, n - 1 - j)) 1; }这样写最大的好处是只在一个位置做转换不容易漏。多组查询的时候调用这个函数一万次也只是常数时间性能极其富余。如果你将来遇到 n 特别大的情况比如 n 10¹⁸那么只要算单个点公式法依然是唯一可行的解法。4. 图形题的常见变体从回形矩阵到螺旋矩阵4.1 变化一螺旋矩阵和回形矩阵真不是一回事很多资料会把回形矩阵和螺旋矩阵混为一谈但它们的填充逻辑完全不同。回形矩阵里同一圈的数字是相等的一圈一圈从外到内每圈一个数字。螺旋矩阵则是从左上角开始按照顺时针方向蛇形一路填下去数字从 1 一直递增到 n²。螺旋矩阵也能用四个边界变量模拟只是每一圈的填充不是同一个数字而是一个连续递增的计数器。这里给出一个简单版本的核心框架int num 1; int top 0, bottom n - 1, left 0, right n - 1; while (top bottom left right) { for (int j left; j right; j) a[top][j] num; for (int i top 1; i bottom; i) a[i][right] num; if (top bottom) for (int j right - 1; j left; --j) a[bottom][j] num; if (left right) for (int i bottom - 1; i top; --i) a[i][left] num; top; --bottom; left; --right; }可以看到结构上和回形矩阵几乎一样区别只在于回形矩阵给同一个圈的所有格子填同一个数字而螺旋矩阵每填一个格子就把计数器加一。这个例子想说明的是四个边界指针的方法是一套通用模板你能记住它的边界控制规则就能同时应对多种“按圈处理矩阵”的题目。4.2 变化二字符图形和镂空图形图形题不一定都是数字。蓝桥杯和同类比赛里输出菱形、空心矩形、等腰三角形的题也经常出现。它们的共同点是要求你从行号出发推导出每一行的空格数量和字符数量。以空心矩形为例输入 n输出一个 n 行 n 列的方框边框用*中间为空。这题比回形矩阵简单很多核心逻辑是判断当前坐标是否落在边界上for (int i 0; i n; i) { for (int j 0; j n; j) { if (i 0 || i n - 1 || j 0 || j n - 1) cout *; else cout ; } cout \n; }关键是你要形成一种条件反射看到图形题先想“这个位置需不需要特殊处理”再想“普通位置是什么规则”。空心的本质是边界规则和内部规则不一致所以用 if 分开处理就是最自然的思路。4.3 变化三旋转和翻转一招通用坐标映射有些图形题不考生成考变换。比如给一个矩阵将它顺时针旋转 90 度输出。很多人会在新矩阵里一个格子一个格子地移动元素但如果写熟了可以直接记忆坐标映射关系顺时针旋转 90 度时原图的 (i, j) 会跑到新图的位置 (j, n - 1 - i)。你不需要在脑内旋转整个图像只要拿着这个公式遍历新图每个位置然后从旧图取出对应元素填进去。这个坐标映射的推导过程建议你自己拿一张方格纸走两遍一次顺时针、一次逆时针之后就能非常牢固地记住。这类变换题的核心思路和回形矩阵是一致的图形变化的本质是坐标变换。如果在考场上没记住公式你可以自己画一个 3×3 矩阵演算两分钟很快就能推出来。5. 高频报错与避坑清单5.1 数组越界和行列下标混乱这类题最容易栽的坑就是数组越界。尤其是模拟法里四个边界同时变化一个不留神就会出现a[i][right]里的i跑到bottom之外。我刚开始练这类题的时候至少有三次是因为循环里少写或者多写了一个等号导致死循环或者越界。排查建议先把 n 设成 3 或 4在代码里打上几个标记点看看每一轮之后 top、bottom、left、right 的变化是否符合预期。比如回形矩阵模拟法里每一轮结束top 应该 1bottom 应该 -1左右同理。如果发现某个边界没动多半是因为循环条件写得不对。另一个高频错误是下标从 1 开始和从 0 开始混用。如果你的矩阵是用 vector 开的下标务必都从 0 开始如果题目描述里说“左上角位置是 (1,1)”那你读取输入后要立刻减一。公式法尤其容易栽在这件事上建议所有涉及位置的变量只在一个统一入口处转换。5.2 输出格式问题真的是送命题图形题的另一个重灾区是输出格式。有些题目要求数字之间用空格隔开有些要求行尾不能有空格有些要求每隔几行空一行。你在本地测试的时候觉得没问题提交后一比对样例就错了这个时候多半是格式验证环境不匹配。我的处理习惯是把题面里的样例复制成一个文本自己程序输出的结果直接做逐行比对。比对时尤其注意空格数量以及最后一行的换行是否存在。现在的在线评测系统对多余空格和行尾空格的处理不完全一样但稳妥起见永远不要在行尾留空格。上面代码里我用if (j) cout ;的方式输出就是为了保证行尾干净。5.3 图形题通用三板斧做了不少图形类题目以后我觉得有一个固定的处理顺序特别适合新手当成套路来用。第一板斧画图。拿到题目先别敲键盘拿草稿纸把 n 等于 1、2、3、4、5 的情况全部画出来仔细观察数字之间的递增关系。这个过程能帮你快速判断题目到底是同心圈还是螺旋还是其他规律。第二板斧写暴力。如果规律不明确就先写最简单、最直接、最不容易出逻辑错误的版本比如回形矩阵的双层循环或者四个边界模拟法。暴力代码能跑出正确答案就能作为后续优化的对照标准。第三板斧找公式。暴力版本提交之后如果觉得题目可以优化或者数据范围不允许暴力再从画过的图里提取数学关系推导 O(1) 的公式。公式推导完成后用小几组 n 和暴力版本对拍一下确定无误后再提交。这套流程看起来费时间实际上在考场上能救你很多次。它保证你不至于一上来就锲而不舍地死磕某一个看似高明的优化思路结果连样例都没跑出来。图形题最怕的不是想不到妙招而是基础逻辑错了却还在拼命优化。把基础模拟法写稳把公式法做对再把这些输出格式、坐标转换的坑避开这类题目就基本拿下了。我个人后来做图形题的时间里反而是画图和检验格式花的时间最多真正写代码的时间往往很短。一道图形题写得快不快很大程度上不取决于手速而取决于你能不能在动笔之前把规律看清楚。