第一次做这道题的时候,我对着样例一笔一笔画了半天,明明图形看着就是“回”字套“十”字,代码却怎么写怎么翻车。后来我换了个思路:先别急着找公式,直接把图形拆成“中心十字 + 外面的框 + 四角的连接段”,十几分钟就把代码写顺了。这篇文章就把这套思路完整展开,包括题目怎么拆、代码怎么组织、有哪些容易踩的坑,适合正准备刷蓝桥杯历届试题、或者被各种文字图形题卡住的朋友。
1. 这题到底在考什么:文字图形题不是死记硬背,是观察力加模拟能力
1.1 看到题目第一眼应该想什么
很多人拿到“打印十字图”这种题,第一反应是“这不就是个双层循环输出星号吗”,然后就开始猜坐标公式,猜了一上午还不对。实际上这类题考察的不是某个高深算法,而是两件事:你能不能从图形样例里看出重复结构,以及你能不能把这个重复结构转化成程序里的循环。
题目要求输出一个“十字图”,它本质上是一个由*和.组成的二维字符矩阵。输入是一个正整数n,输出的是边长随n变化的一张图。n 越大,图形越复杂。这种题的共同特点就是:数据规模很小,不需要考虑时间优化,真正难的是“别把图形画错”。
我建议拿到题目后的第一件事,不是写代码,而是拿纸笔把n=1, n=2, n=3的图形画出来,哪怕只是画出缩略结构。画的过程里你会看到很多重要信息:图形是否上下左右对称、边长的增长规律、中心十字是否始终保持不变、外层和中心之间靠什么连接。
1.2 为什么说这是个“小规模模拟题”
这类打印图形的题,时间复杂度和输入规模是线性平方关系。以n最大取 30 为例,边长也就是 125 左右,总共大约一万多个字符,哪怕用三重循环去暴力判点,现代电脑也就是一瞬间的事。
所以不要一上来就追求什么“最简代码”。先把正确性保证住,再考虑代码短不短。就我的经验来看,用二维字符数组先把图形填充完,最后统一输出,是这类题最稳的做法。因为你可以随时断点查看数组,看看哪一行哪个位置填错了,比在printf里逐字符判断要直观太多。
2. 先画图再写码:把十字图拆成中心十字、外框和四角连接段
2.1 中心和边长的关系
这道题里有个非常关键的规律:不管n多大,图形中心始终是那个 5×5 的小十字。你可以把整个图形想象成从这个小十字开始,一圈一圈向外扩展。
边长公式是:
边长 = 4 * n + 5也就是说,n每增加 1,边长就增加 4。为什么要加 5?因为最里面那个小十字本身就是 5×5。这个 5 就像一个“基底”,外面每多一层,上下左右各扩展 2 个字符,所以总边长加 4。
中心坐标也好算,就是边长的中点:
c = 边长 / 2 = 2 * n + 2只要抓住这一点,后面所有外框坐标都能以中心为基准计算,图形自然保持对称。
2.2 外框怎么定位
从中心往外看,每一层外框距离中心都是偶数距离。最靠近中心十字的那个框是第 1 层,它到中心的距离是 2;再往外一层,距离就是 4;以此类推。
如果用d表示某一层到中心的距离,那么这一层的外框由四个边组成:
- 上边:行号是
c - d,列号从c - d + 2到c + d - 2 - 下边:行号是
c + d,列号范围相同 - 左边:列号是
c - d,行号从c - d + 2到c + d - 2 - 右边:列号是
c + d,行号范围相同
这个“每边两端都缩进 2 格”的处理,保证外框四角空出来,不会被填成完整矩形。因为完整的矩形框会跟中心十字连成一片乱麻,就不像“十字图”了。
2.3 四角连接段是灵魂
如果你只画中心十字和外框,整个图看起来是散的:中心十字是中心十字,外框是外框,二者没有连起来。这时候就需要在四个角各补一小段“阶梯形”连接线。
以左上角为例,设这一层外框的左上角坐标为(r1, c1),其中r1 = c - d, c1 = c - d。需要额外填这几个点:
(r1 + 1, c1 + 2) (r1 + 2, c1) (r1 + 2, c1 + 1)这三个点加上外框本身的点,会形成一个类似“L”形的小转折,正好把中心十字延伸到外框。四角都做同样的处理,图形就是整体连通的。
我第一次做的时候就是漏了这一步,输出的图形中心是一个十字、外围是一个框,中间断成两截,怎么看怎么丑。后来补上四角连接段,才真正对味。
3. 分层填充实现:二维数组画十字图的全过程
3.1 为什么选二维数组而不是直接 printf
有些人喜欢边循环边printf,一行一行输出。但文字图形题有个麻烦:很多字符要跨行判断,如果你一边算一边输出,脑子得同时记住前面几行都填了什么,非常容易乱。
用二维数组g[][]做缓冲区就清晰很多:先把所有位置初始化为.,再根据规律把该填*的位置挨个填进去,最后统一输出。这个思路特别适合嵌套图形,因为你可以“先画中心,再画外圈”,顺序天然就是一层层向外。
3.2 完整代码参考
下面这段代码是我按“中心十字 + 逐层补框 + 四角连接”的思路实现的,注释写得比较细,方便对照上面的分析。
#include <bits/stdc++.h> using namespace std; const int MAXN = 200; char g[MAXN][MAXN]; int main() { int n; cin >> n; int m = 4 * n + 5; // 边长 int c = 2 * n + 2; // 中心坐标 // 1. 全部初始化为 '.' for (int i = 0; i < m; ++i) for (int j = 0; j < m; ++j) g[i][j] = '.'; // 2. 画中心十字,半径 2 for (int k = -2; k <= 2; ++k) { g[c][c + k] = '*'; // 横臂 g[c + k][c] = '*'; // 竖臂 } // 3. 从第 1 层外框开始,逐层向外画 for (int d = 4; d <= 2 * n + 2; d += 2) { int r1 = c - d, r2 = c + d; // 上边行、下边行 int col1 = c - d, col2 = c + d; // 左边列、右边列 // 3.1 画四条边 for (int j = col1 + 2; j <= col2 - 2; ++j) { g[r1][j] = '*'; // 上边 g[r2][j] = '*'; // 下边 } for (int i = r1 + 2; i <= r2 - 2; ++i) { g[i][col1] = '*'; // 左边 g[i][col2] = '*'; // 右边 } // 3.2 补四角连接段 // 左上角 g[r1 + 1][col1 + 2] = '*'; g[r1 + 2][col1 + 2] = '*'; g[r1 + 2][col1] = '*'; g[r1 + 2][col1 + 1] = '*'; // 右上角 g[r1 + 1][col2 - 2] = '*'; g[r1 + 2][col2 - 2] = '*'; g[r1 + 2][col2] = '*'; g[r1 + 2][col2 - 1] = '*'; // 左下角 g[r2 - 1][col1 + 2] = '*'; g[r2 - 2][col1 + 2] = '*'; g[r2 - 2][col1] = '*'; g[r2 - 2][col1 + 1] = '*'; // 右下角 g[r2 - 1][col2 - 2] = '*'; g[r2 - 2][col2 - 2] = '*'; g[r2 - 2][col2] = '*'; g[r2 - 2][col2 - 1] = '*'; } // 4. 输出整个矩阵 for (int i = 0; i < m; ++i) { for (int j = 0; j < m; ++j) cout << g[i][j]; cout << '\n'; } return 0; }3.3 这段代码的几个关键点
第一,中心十字的半径固定是 2,不管n怎么变都不改。你可以把这部分看成整张图的“种子”。
第二,最外层框的d最大值是2 * n + 2。因为中心坐标c = 2 * n + 2,当d = 2 * n + 2时,c - d = 0,刚好到达图形最上边;再配合对称性,四边都会落在边界上。
第三,四角连接段里有些点和中心十字或内层外框是重合的。比如左上角第一个点(r1 + 2, col1 + 2),在d = 4时,这个坐标就是中心十字左上臂的端点。重复填同一位置不会带来错误,g[][]本来就是覆盖赋值,反而省了判断是否越界的麻烦。
4. 进阶坐标判断法:一个循环直接判星号
4.1 坐标法到底在做什么
写完分层填充之后,你会发现这段代码虽然思路清晰,但还是有点长。有没有更短的做法?有,就是对每个坐标直接做一个布尔判断,判断条件写对之后,一个双层循环就能输出整张图。
坐标判断法的核心是:把(i, j)转换成相对于中心的偏移量。
x = abs(i - c) y = abs(j - c) t = max(x, y)这个t表示当前点离中心最远在哪一层。比如t <= 2时,点在中心十字所在的小方块里;t = 4时,点在第 1 层外框附近;t = 6时,点在更外面一层附近。
但要注意,不是所有t相同的位置都填*。外框边上要填,外框四角要空出来,中心十字的横竖臂要填,其他位置留点。所以坐标法的难点不在“算距离”,而在“综合各种条件”。
4.2 一种直观的判定骨架
我比较推荐的写法,是把“是否为星号”抽象成一个判断函数或一段逻辑,按优先级判断。
如果该点在中心十字的横臂或竖臂上,输出 * 否则如果该点在某层外框的四条边上,输出 * 否则如果该点在四角连接段上,输出 * 其他情况,输出 .这个骨架和前面二维数组填充是一一对应的。你完全可以把第 3 节代码里的填充逻辑改写成一组if条件,然后放到双层循环里逐点判断。这样代码会短一些,但可读性会下降。
我更建议做题时先用二维数组方法把题做出来,确保正确,然后再去琢磨坐标判断法。因为坐标判断法一旦写错一个边界,很难通过肉眼排查;而二维数组方法可以随时把中间结果打印出来看。
5. 实测中那些很容易翻车的细节
5.1 数组大小和初始化
这类题最冤枉的失误就是把数组开小了。边长最大是4 * n + 5,如果题目说n最大是 30,那么最大边长是 125,数组至少开到 150 以上才安全。很多题目会给n <= 30,有些人习惯性开100的数组,结果直接越界。
另外,二维数组初始化一定要做。如果不初始化,里面的内容是未定义的垃圾值,输出时会看到一堆乱码。用memset(g, '.', sizeof(g))或者双重循环赋初值都行,别偷懒省略。
5.2 输出格式:换行与空格
图形题对输出格式的要求一般很严格:每行结尾不要有多余空格,行与行之间用换行符分隔。最常见的错误是在printf("%c ", g[i][j])里多加了一个空格,整张图就错位了。
我一般用cout << g[i][j]连续输出,一行结束再cout << '\n'。这种写法没有行尾空格,也不容易手滑多打字符。注意题目要求用什么字符表示图形,就用什么字符,不要把*和$混用。
5.3 图形自查方法
写完程序别急着提交,先用小数据肉眼核一遍。你可以把n=1的输出结果跟题目样例逐字符比对。
更高效的办法是写一个简单的对称性检查:程序跑完后,把生成的矩阵存下来,检查它是否关于中心点、中心行、中心列对称。如果某个字符破坏了对称性,说明填充逻辑里有问题,而且问题点一定在对称轴的边界附近。这个方法我强烈推荐,很多隐蔽 bug 就是靠它查出来的。
6. 这类题还能怎么练:文字图形题的通用打怪路线
6.1 同类图形题的常见变体
“打印十字图”只是文字图形题里的一个经典代表。类似的还有打印菱形、打印沙漏、打印回形矩阵、打印螺旋矩阵等。它们的共同点是:图形由若干行字符组成,规律是重复的、可拆解的。
练这种题时,可以按这个顺序来:
- 先手动画出
n=1, n=2, n=3的图形,把每一层坐标写下来。 - 找出最小的重复单元。比如十字图的最小重复单元是中心十字和外框的连接方式。
- 用二维数组把重复单元填进循环,先保证正确,再考虑化简。
- 用对称性检查结果,别只看开头几行。
6.2 从图形题里带走什么
别小看这种“幼稚”的字符题,它对代码基本功的锻炼很实在:二维数组的使用、边界坐标的计算、循环嵌套的正确性、格式化输出的习惯,全都考到了。有些人刷题只爱做算法题,觉得输出图形没意思,结果真上了考场,遇到这类题反而手忙脚乱。
根据我的体会,做文字图形题最划算的投资是“画图”。在草稿纸上多画几张图,把规律看明白了,写代码就是顺水推舟的事。最忌讳的是连样例都没看懂就打开编辑器开始猜代码。先画两分钟图,能省后面二十分钟调试时间,这笔账怎么算都划算。