叠筐问题是很多刷题人入门阶段都会碰到的一道经典图形输出题。题目本身不复杂:给你一个奇数尺寸的筐和两种花色字符,要求从内到外一圈一圈交替叠出一个正方形筐,而且筐的四个角还必须是空的。我在某个OJ上第一次见到它的时候,以为随便写几个循环就能拿满分,结果连续提交了好几次都卡在格式上——不是四角多出字符,就是多组数据之间的空行不对。这篇博客就把叠筐问题的完整思路、代码实现和调试中踩过的坑一次性讲清楚。刚接触算法的朋友可以把它当作图形类题目的入门范本,有经验的朋友也可以顺便复习一下“分层填充”这类经典模型。
1. 先读懂题目:叠筐问题到底在考什么
1.1 题目描述还原:尺寸、花色与那个隐蔽的“四角”
叠筐问题的标准描述大概是这样的:输入一个奇数n(范围通常是1<n<80),以及两个字符,要求输出一个边长为n的正方形图形。图形从中心开始,中心一格使用第一个字符,往外每增加一圈就换一个字符,两个字符交替出现;最特别的要求是,整个大正方形的四个角不填字符,输出为空格。
这里要先说明一个容易踩坑的细节:不同题库对两个字符的定义略有差异。有的题目说“第一个字符是中心花色,第二个字符是外筐花色”,有的又说“第一个字符是外筐花色,第二个字符是中心花色”。最稳妥的做法是不要死记文字定义,而是先看样例,用样例反推出当前平台的规则。我在下文统一采用一种自洽的模型:中心为第0层,使用第一个字符a;向外第1层用第二个字符b,第2层又用a……依次交替。用这个模型可以解释绝大多数版本,真遇到相反定义时,只需要把a和b的初始赋值对调一下即可。
把这个问题想象成超市里那种大小差一圈的塑料收纳筐,一个套一个叠起来,从正上方俯视,你看到的是一圈一圈交替出现的筐壁花色。四角留空也很好理解:真正的筐壁是四条边,拐角在俯视图中往往被上层筐壁遮住,或者本身就是圆弧过渡,没有明确的“角”。这就是题目设计成四角为空的直观原因。
1.2 关键判断:中心字符与外圈字符由什么决定
题目里有个许多新手想当然的误区:认为最外圈一定使用第二个字符。实际上没那么简单。设n=2k+1,那么从中心到最外圈一共有k+1层:中心是第0层,最外圈是第k层。第0层用a,第1层用b,第2层用a……所以最外圈用的字符取决于k的奇偶性。
- 当k是偶数,即n=5,9,13…时,第k层是偶数层,最外圈用a,和中心同色;
- 当k是奇数,即n=3,7,11…时,第k层是奇数层,最外圈用b,和中心异色。
我当年第一次做这道题时,直接写了“最外圈用第二个字符”,结果n=5的测试就把我打回了原形。后来我在草稿纸上画了n=5和n=7两个图,才彻底搞明白这个奇偶关系。建议你也动手画一张图:中心写a,外面一圈写b,再外面写a,画完之后自然就理解了。
1.3 把“叠筐”翻译成二维矩阵模型
所有图形输出题的本质,都是把几何图形映射到二维数组的坐标上。叠筐问题中,我们用一个n×n的字符矩阵来表示最终图形,坐标范围是(0,0)到(n-1,n-1)。
在这个矩阵里,如何判断某个坐标属于哪一圈?关键规律是:一个格子所在的圈数,等于它到四条边最近距离的最小值。比如坐标(i,j),它向上有i行,向下有n-1-i行,向左有j列,向右有n-1-j列,这四个距离中的最小值d,就是这个格子所在的“外层深度”。d=0表示格子在最外圈,d=k表示格子在最中心。这个“到边界距离”的观察方式,比硬记圈号要通用得多,后面做螺旋矩阵、迷宫寻路都会用到。
有了这个坐标模型,叠筐问题就从“画图题”变成了“填表题”:无非是给每个坐标算出一个深度d,再根据深度决定填哪个字符。
2. 两套解法对比:逐层填充与坐标判断
2.1 方案A:从外向内逐层画筐
方案A的思路最贴近题目描述:先画最外圈,再画第二圈,一直画到中心。每一圈都是正方形边框,由四条边组成:上边一行、下边一行、左边一列、右边一列。
假设当前要画第layer圈(从外向内数,layer=0是最外层),这一圈的左上角坐标是(layer,layer),右下角坐标是(n-1-layer,n-1-layer)。四条边的填充方式为:
- 上边:把行layer、列从layer到n-1-layer的所有字符设为当前花色;
- 下边:把行n-1-layer、列从layer到n-1-layer的所有字符设为当前花色;
- 左边:把列layer、行从layer到n-1-layer的所有字符设为当前花色;
- 右边:把列n-1-layer、行从layer到n-1-layer的所有字符设为当前花色。
需要注意,四条边的四个交点(也就是这一圈的四个角)会被重复赋值,但这没有关系,赋同样的字符不会产生错误。真正需要额外处理的,是最外圈那四个角在输出时要变成空格,这个操作放在整个填充结束之后统一做就行。
方案A的优点是直观,完全模拟了叠筐的过程,循环变量清晰;缺点是需要提前算准最外层字符,而且要在填充后单独清理四角。
2.2 方案B:用坐标到边界的距离一次性定字符
方案B是我个人更推荐的写法,代码更短,也不容易漏边界。思路就是1.3节提到的坐标模型:先算出某个坐标到最近边界的距离d,再算出它到底是第几层。
具体来说,设n=2k+1,坐标(i,j)到最近边界的距离为:
d = min(i, j, n-1-i, n-1-j)
当d=0时在最外圈,当d=k时在中心。我们约定中心是第0层,所以一个深度为d的格子,它的层号是:
layer = k - d
判断字符时,直接用layer的奇偶性:layer为偶数填a,奇数填b。这样双重循环遍历所有坐标,一次就能把整个矩阵填完,不需要分层画边。
方案B的亮点在于把“分层”这个几何概念彻底转化成了数学计算,代码非常紧凑。缺点是需要先理解“到边界距离”这个抽象概念,对刚开始接触算法的朋友可能不够直观。
2.3 新手该选哪套
我已经不只一次在讨论区看到有人在叠筐问题上争论“哪种写法更标准”,其实没必要。两套方案都正确,选哪套取决于你现在处于什么阶段。
我把两者的对比整理成一张表:
| 对比项 | 方案A:逐层填充 | 方案B:坐标判断 |
|---|---|---|
| 直观程度 | 高,符合画图直觉 | 中,需要理解距离概念 |
| 代码量 | 中等 | 短 |
| 四角空格处理 | 需要额外清理 | 循环结束后统一清理 |
| 容易出错的地方 | 边界坐标、花色奇偶 | min表达式、层号换算 |
| 适合人群 | 初学、正在练循环边界 | 有一定基础、追求简洁写法 |
我的建议是:先把方案A老老实实写一遍,因为逐层填充能帮你建立“圈”的几何直觉;写顺了之后,再用方案B优化一遍,体会一下“用数学代替循环”的乐趣。两种都会了,这类题目基本难不倒你。
3. C语言实现:从画图到AC的完整过程
3.1 主流程与变量设计
整个程序的主流程非常清晰:读入数据 -> 清空画布 -> 填充图形 -> 清理四角 -> 输出结果。由于题目可能有多个测试用例,所以要用while循环一直读到文件结尾。
变量设计上,我用一个全局二维数组pic[80][80]来存图形,两个字符变量a和b表示两种花色,n表示边长。用全局数组有个好处:静态存储区的数组会初始化为全零,不会因为函数调用产生随机值;不过我后面还会显式清空画布,双保险。
读取输入时要注意格式控制。最安全的写法是:
scanf("%d %c %c", &n, &a, &b);这里的%d能自动跳过空白字符,%c前面的空格也会让scanf自动跳过换行和空格。如果写成scanf("%d%c%c"),第二个%c就很可能会读到一个换行符,导致图案错乱。
3.2 填充循环怎么写才不容易乱
我推荐用“层差”的方式来写填充循环,避免事先计算最外层字符。设k=n/2,这是一个整数(因为n是奇数)。从中心向外看,中心的那一格是第0层,最外圈是第k层。
循环变量layer从k递减到0,每层对应的正方形,其左上角坐标是(k-layer, k-layer),右下角坐标是(k+layer, k+layer)。比如layer=k时,左上角是(0,0),右下角是(2k,2k),正好是最外圈;layer=0时,左上角和右下角都是(k,k),正好是中心那一点。当前层使用的字符,由layer的奇偶决定:偶数层用a,奇数层用b。
完整填充代码如下:
for (int layer = k; layer >= 0; layer--) { char ch = (layer % 2 == 0) ? a : b; int x0 = k - layer, y0 = k - layer; int x1 = k + layer, y1 = k + layer; for (int j = y0; j <= y1; j++) { pic[x0][j] = ch; pic[x1][j] = ch; } for (int i = x0; i <= x1; i++) { pic[i][y0] = ch; pic[i][y1] = ch; } }这段代码的精髓在于layer直接代表“距离中心的层数”,而不是“从外往里数的圈号”。这样写,中心的第0层天然使用a,完全不用费心去算最外层到底用什么字符,因为当layer从k递减到0的过程中,每个格子的字符赋值正好覆盖了从外到内的所有圈。
注意,这里每条边都会被赋值两次(四个角尤其如此),但不要试图优化掉这种重复赋值,因为一旦优化就容易漏边。保持代码的简单和确定,比微小的性能提升重要得多。
3.3 输出格式:空行、行尾空格与四角空格
填充完成后,第一件事是清理最外圈的四角。四角的坐标分别是(0,0)、(0,n-1)、(n-1,0)、(n-1,n-1),把它们置为空格即可。但这里有一个特例:当n=1时,整个图形只有一个格子,这个格子既是中心又是“四角”,不能把它也填成空格,所以要加一个n>1的判断。
if (n > 1) { pic[0][0] = pic[0][n-1] = pic[n-1][0] = pic[n-1][n-1] = ' '; }接下来输出。图形题的输出有一个容易忽略的点:行尾空格不要盲目删。叠筐问题的四角空格位于第一行和最后一行的行首与行尾,它们属于图形的一部分,如果为了“整洁”去掉尾部空格,反而会破坏题的输出要求。正确做法是整行原样输出。
多组数据之间需要空一行,但最后一个案例后面不能有多余空行。我用一个first变量来控制:
if (!first) printf("\n"); first = 0;每次输出图形前先判断,只要不是第一组数据,就先打印一个空行。这样既能保证组间有间隔,又不会在文件末尾多出空行。
完整的输出代码:
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { putchar(pic[i][j]); } putchar('\n'); }到这里,叠筐问题的主干代码就完成了。把清空画布、填充、清理四角、输出这几步组合起来,就是一个可以提交的完整程序。
4. 实战踩坑:常见错误与排查技巧
4.1 字符多出一堆“马赛克”:数组初始化问题
我第一次写这道题时,图形里莫名其妙出现一些奇怪的字符,整个图案看起来像信号不好的电视屏幕。问题出在数组初始化:局部数组如果没有初始化,里面的值是随机的,填充循环只能覆盖到正方形范围内的格子,而我没有先清空整个数组就急着填充,结果那些没被覆盖到的位置就露出了“底噪”。
解决办法有两种:一是把数组定义成全局数组,静态存储会自动清零;二是显式初始化,比如用两层循环把整个数组先填成空格:
for (int i = 0; i < 80; i++) for (int j = 0; j < 80; j++) pic[i][j] = ' ';我的经验是,不管是不是全局数组,做二维填充题先清空画布永远没错。这一个习惯能帮你避免一堆莫名其妙的输出问题。
4.2 花色整体反了:奇偶判断的一个小技巧
现象是图形形状完全正确,但中心和外圈的颜色和预期相反。比如期望中心是a、外圈是a,实际输出中心是b、外圈是b。这种问题通常出在层号和字符的对应关系上。
排查思路其实很简单:找一个具体的n,比如n=5,k=2,在草稿纸上画一个5×5的格子,从中心开始标出每个格子的期望字符。然后对照代码逐层检查,看看中心层layer=0到底用的是哪个字符。我习惯用一个小技巧:只检查对角线上前三个格子的字符,因为对角线从外到内必然依次跨越最外圈、次外圈、中心,可以快速验证花色顺序是否正确。
另外提醒一句:不同平台的字符定义可能不同。如果你的逻辑完全正确但提交后答案错误,先回去翻翻题目的样例输出,看看它到底是“中心用第一个字符”还是“外圈用第一个字符”。不要和自己的思路较劲,以样例为准。
4.3 四角空格到底怎么处理才不破坏对称性
有四角空格要求的版本,必须在所有填充结束之后统一把四角置为空格。如果你在每一圈填充时都把该圈的“角”清空,那就麻烦了——内圈的角也会被清空,最后图形中间出现一块空洞,完全不像叠筐。
只清最外圈四角之后,图形仍然保持上下、左右以及中心对称。你可以利用这个性质自查:如果输出的图形左右不对称,多半是行列坐标写反了,比如把pic[i][j]写成了pic[j][i];如果上下不对称,可能是上边和下边的循环边界写串了。
顺便说一句,有些题库还收录了“四角不输出空格”的变体版本,输出的是完整实心正方形。遇到这种情况不要慌,看它给的样例即可。本质上只是最后一步清空四角做不做的区别。
4.4 输入输出陷阱:多案例空行与字符读取
叠筐问题通常是多组测试数据,这是另一个重灾区。最典型的错误写法是:
while (scanf(...) != EOF) { handle(); printf("\n"); }这种写法会导致第一个输出前多一个空行,最后一个输出后也多一个空行。在线评测系统对于空行格式的检查通常很严格,多一个少一个都算格式错误。我用的first变量法能完美解决这个问题。
还有一个坑在字符读取。如果上一行输入结束有个换行,而你的scanf写得不严谨,比如scanf("%d%c%c"),那么第二个%c读到的是换行符而不是花色字符,整个输出就会乱掉。正确写法是确保每个%c前面有空格,让scanf自动跳过空白字符。这个细节在C语言题目里非常常见,值得养成习惯。
5. 叠筐问题的算法价值与扩展
5.1 剥洋葱:一类分层填充题的通法
叠筐问题表面上是一道print题,但它背后的“分层填充”思想,在算法题里随处可见。所谓的剥洋葱模型,就是先从最外层开始处理,一层一层往内推进,直到中心。处理每一层时,动作都是类似的:确定当前层的边界,按规则填充或访问。
剥洋葱通法可以总结为三步:第一,确定总层数,通常是一个边长除以2的整数;第二,确定每一层的四个边界,通常左上角是(layer,layer),右下角是(n-1-layer,n-1-layer);第三,确定每层的处理动作,可能是填字符、求和、翻转,或者螺旋遍历。
掌握了这个模型之后,你会发现很多题目只是在“每层的动作”上做了变化,骨架其实完全相同。叠筐问题的填充动作是“画四条边”,螺旋矩阵的填充动作是“沿四条边顺次走”,回形遍历的动作是“按顺序读取”。
5.2 从叠筐到螺旋矩阵和回形遍历
如果你能不看任何题解,把叠筐问题中每条边的循环下标写对,那么螺旋矩阵的核心代码对你来说就很轻松了。螺旋矩阵要求从外到内依次填入1到n²的数字,本质上和叠筐一样,都是“一圈一圈地处理”。
两者的核心对应关系是这样的:叠筐每一圈的四条边上是同一个字符,而螺旋矩阵每一条边上是一串连续的数字;叠筐的层间切换只是换一个字符,螺旋矩阵的层间切换则是继续递增数字。方向控制上,螺旋矩阵需要在一个圈内走“右、下、左、上”四段,而叠筐的每条边是独立的循环,反而更简单。
我建议你做完叠筐之后,立刻去做一道螺旋矩阵或者蛇形填数,你会发现自己的边界处理能力明显提升了,因为叠筐已经把“圈的边界”这个概念练熟了。
5.3 图形题的自查方法:对称性、小数据与对拍
图形输出题有一个共同特点:样例通常只有一两组,且n都比较小,测试覆盖不足。如果你只拿样例测试,很可能会漏掉n=1、n=3这类边界情况。我个人的自查套路是固定三组数据:n=1、n=3、n=5。
- n=1:验证极端情况,输出应该只有一个字符,且不能有四角空格。
- n=3:验证“外圈与中心异色”的情况,因为k=1是奇数。
- n=5:验证“外圈与中心同色”的情况,因为k=2是偶数。
这三组能把奇偶两种花色关系都覆盖到。检查方法也很简单:把输出结果复制到文本编辑器里,看它左右是否对称、上下是否对称。叠筐图形是高度对称的,任何一边不对称都说明坐标写错了。
如果是在本地练习,还可以用一个小脚本生成期望图形,再跟你的程序输出做比对,也就是对拍。但更多时候,一张草稿纸加一支笔就够了。做题不是比谁交得快,而是比谁一次AC的把握大,养成先画图再写代码的习惯,能省下大量反复提交的时间。
我个人在实际操作中的体会是:图形题考的不是复杂算法,而是“把几何描述翻译成坐标规则”的基本功。叠筐之所以能成为经典入门题,正是因为它在简单的外表下,集中考察了数组初始化、循环边界、奇偶判断、格式控制这些最基础却最容易出错的能力。我见过有人逻辑完全正确,却因为数组没初始化而卡了一晚上;也见过有人把花色顺序颠倒了,反复提交五六次才醒悟。如果你正在入门阶段,建议认真把这道题吃透:先在草稿纸上画出n=5和n=7的真实图形,再动手写代码;写完以后,再主动试试n=1、n=3这种边界数据。等你能把叠筐稳稳地一次通过,后续再遇到螺旋矩阵、回形遍历这些同类题目,心里会踏实很多。