打印沙漏题详解:从数学建模到代码实现的完整拆解
在 PTA、C 语言习题集和各类刷题平台上“打印沙漏”这道题几乎成了入门必刷的老熟人。题面特别简单给你 17 个“*”让你摆成一个沙漏形状。但就是这种看起来一动手就能画出来的题我在带新人学编程时反复看到他们卡住——卡住的原因不是不会用循环而是他们总想“用眼睛把沙漏画出来”却没想到程序画图的本质是“找规律 算公式”。这篇东西就把这道题从数学建模、代码实现、边界测试到同类图形题的迁移完整拆一遍。不管你是刚学循环的新手还是准备给学员讲题的老人都应该能从中拿走点能直接用的东西。1. 沙漏的数学结构先忘掉“画”把它当成数列看1.1 17 个“*”是怎么组成一个沙漏的先把题目给的 17 个星号在草稿纸上画出来就是下面这个样子***** *** * *** *****一共 5 行。上三角 3 行符号数分别是 5、3、1下三角 2 行符号数分别是 3、5。把它们加起来5 3 1 3 5 17。这个 17 不是随手给的它刚好对应一个“层数 n 3”的完整沙漏。这里要建立一个关键认知沙漏图形的每一行符号数量其实是一个奇数序列。从中间那行的 1 个符号开始往上每走一层符号数加 2往下每走一层符号数也加 2。于是从中心向外看符号数就是 1、3、5、7……这样一个等差数列。理解到这一步后面所有推导都会顺很多。很多新手看到图形题第一反应是“我一行一行写死”。比如 n 固定为 3 时第 1 行 5 个星号第 2 行 3 个第 3 行 1 个……直接三行 printf 解决。问题在于题目通常会给一个任意的总数 N比如 17、19、23甚至 100让你根据数量自动判断能摆几层。一旦要把“最大层数”算出来硬编码就彻底失效了。所以核心工作其实是两件一是算出最多能摆几层二是根据层数把图形循环出来。1.2 层数 n 与符号总数2n² - 1 是从哪来的假设沙漏最大一行的符号数是第 n 个奇数也就是 2n - 1。在 17 个星号的例子里最大行是 5对应 n 3。上三角从中间到最大行符号数分别是 1、3、5、…、2n-1这是一个标准等差数列。前 n 项和是1 3 5 … (2n - 1) n²下三角如果单独算是从 3 到 2n-1缺少一个“1”。所以下三角的符号总数是 n² - 1。于是整个沙漏total n² (n² - 1) 2n² - 1代入 n 3 验证2 × 3² - 1 17完全吻合。这就是“17 个星号正好组成 3 层沙漏”的数学解释。这个公式相当重要因为后面“给定任意 N求最大层数”完全依赖它。只要看得懂 2n² - 1 的推导你就不需要靠枚举硬试而是能把问题转化成一个“反解不等式”的过程。1.3 给定任意数量 N怎么求出最大层数 n题目如果只给 17 个星号那直接 n 3 就行。但实际练习里输入可能是 18、20、100 这种不整的数。比如 N 18n 3 时需要 17 个够n 4 时需要 31 个不够所以最大只能取 3剩下 1 个星号。题目有时候会要求把剩余符号数也输出这就是为什么很多版本里最后还有一行数字。求 n 最常见的做法是从 0 开始往上试探用 while 循环不断检查“下一层装不装得下”。写成代码是这样int n 0; while (2 * (n 1) * (n 1) - 1 N) { n; } int used 2 * n * n - 1; int remain N - used;循环里的 (n 1) 意思是“再多摆一层”如果多一层的总消耗仍然不超过 N就让 n 加 1。退出循环后n 就是能摆出的最大层数remain 就是剩余没用的符号。这段逻辑看着简单但边界容易踩坑特别是把公式里的 n 写成 n1 时运算顺序不对就会造成死循环或者少算一层。为什么不用开方直接求当然也可以写 n (int)sqrt((N 1) / 2)然后向下取整但浮点数在边界附近的精度问题偶尔会让结果差 1。这种题目 N 的范围通常不会特别大一个 while 循环撑死执行几百次性能完全不是问题所以老老实实用整数循环反而最稳。2. 从公式到循环让程序自动“画”出沙漏2.1 先把“行号”翻译成“空格数 符号数”算出 n 之后真正麻烦的是把图形一行行打印出来。别急着敲代码先在纸上列一张表。以 n 3 为例行号 i空格数符号数这一行长什么样105*****213***321*413***505*****看这张表能发现沙漏比普通菱形好处理的地方在于它的空格数从两侧向中间递增而符号数从两侧向中间递减。每行都要先打印空格再打印符号空格的数量决定了这一行往右偏移多少。如果用“上三角 下三角”的思路规律非常清晰。上三角第 i 行从 1 开始数的空格数是 i - 1符号数是 2 × (n - i) 1下三角从中心下面开始用一个新计数 k 从 1 到 n - 1空格数是 n - 1 - k符号数是 2 × k 1。这个思路比较符合直觉适合刚学循环的人。还有一种更简洁的“距离公式”写法。设中心行为第 n 行任意一行 i 到中心行的距离 m |i - n|那么这一行的空格数就是 n - 1 - m符号数是 2 × m 1。验证一下第 1 行 m 2空格是 2 - 2 0符号是 5第 4 行 m 1空格是 1符号是 3。完全正确。提示理解“距离”之后你会发现打印图形题大多都能用类似模型解决。不是背公式而是学会从“行号到图形参数”的映射关系。2.2 结构式写法 vs 距离式写法两种写法各有适用场景。结构式写法可读性强新人容易看懂每一层循环在干嘛距离式写法代码更短而且天然适合应对“中心对称图形”。下面这份 C 代码用的是上三角 下三角的结构式写法逻辑最直白#include stdio.h #include stdlib.h int main(void) { int N; char ch; scanf(%d %c, N, ch); int n 0; while (2 * (n 1) * (n 1) - 1 N) { n; } int used 2 * n * n - 1; int remain N - used; // 上三角从最大行到中心 1 个 for (int i 1; i n; i) { for (int j 1; j i; j) { printf( ); } for (int j 0; j 2 * (n - i) 1; j) { printf(%c, ch); } printf(\n); } // 下三角从中心下面一层回到最大行 for (int k 1; k n - 1; k) { for (int j 0; j n - 1 - k; j) { printf( ); } for (int j 0; j 2 * k 1; j) { printf(%c, ch); } printf(\n); } printf(%d\n, remain); return 0; }如果你更习惯用统一的距离公式可以把两个循环合并成一个。总行数是 2n - 1对每一行 i 求 m abs(i - n)然后打印相应空格和符号。这个写法在处理“空心沙漏”这类变体时会方便很多。2.3 用 Python 实现思路完全一样很多人用 Python 刷题逻辑和 C 完全一致只是语法更简洁N, ch input().split() N int(N) n 0 while 2 * (n 1) * (n 1) - 1 N: n 1 used 2 * n * n - 1 rows 2 * n - 1 for i in range(1, rows 1): m abs(i - n) print( * (n - 1 - m) ch * (2 * m 1)) print(N - used)这里用到了 Python 的字符串乘法本质上还是“先算空格数再算符号数”。所以这道题的核心思路和语言无关只要把数学关系和循环写法搞明白换任何语言都只是语法差异。3. 边界条件与常见坑样例过了为什么还是 WA3.1 scanf 读字符的那点破事C 语言版本里最容易翻车的其实是第一行输入。题目要求读入一个整数和一个字符通常写成scanf(%d %c, N, ch);如果粗心写成scanf(%d%c, N, ch);就会出大问题。因为用户输入17 *再回车后缓冲区里实际上是17 *\n%d读完 17 后紧接着的%c会读到一个空格或换行而不是星号。结果就是图形变成一行行空行或者符号全错。解决办法就是%d和%c之间加一个空格让 scanf 自动跳过空白字符。这算 C 语言入门非常经典的“读字符坑”打印沙漏这道题正好会踩到。3.2 剩余数量是 0 也要输出很多题目要求最后单独输出一行剩余符号数。比如给 17 个星号时剩下的是 0但输出格式仍然要求打一个0出来。新手经常觉得“都用完了还输出啥”然后漏写这一行直接 WA。还有种情况是 N 特别小。比如 N 1最多只能摆 1 个符号n 1used 1remain 0输出* 0此时下三角部分一个循环都不执行因为循环条件k n - 1n - 1 0。代码里下三角循环直接跳过不会多打一行这是正确的。如果循环条件没想清楚写成k n那 n 1 时 k 1 会执行一次输出一个多余的中心行图形就错了。3.3 行尾空格要不要输出沙漏图形要求左对齐每行符号输出完直接换行不要在行尾补空格。有些平台对行尾空格宽限但最好养成“不打印多余空格”的习惯因为换一个严格的裁判系统就会判错。核心原因是行首的空格决定缩进而行尾的空格对图形没有任何视觉贡献。所以循环里先打印空格再打印符号打完符号立即printf(\n)不要画蛇添足。3.4 常见的 WA 原因对照表我在实际带人调这道题时总结过几个出现频率极高的错误错误特征可能原因处理方式图形变成空行或乱码scanf 格式里 %c 读到了换行/空格改成%d %c中间加空格总符号数超了 Nwhile 条件边界算错检查公式是否写成 2×(n1)²-1N图形中间多了一个 1 符号行下三角循环条件多跑一次用 kn-1不要用 kn1最后一行的 0 没输出忘记打印 remain检查题目是否要求输出剩余数剩余数计算错误used 算成了 2×n×n1 之类统一用 2×n×n-1第一行前面多了空格空格循环范围写成 ji空格数应该是 i-1这些坑单独看都很小但考试或者刷题时任何一个都能让你反复提交吃 WA。我的建议是写完代码先手动测试 1、2、17、18 这几个值基本能把大部分边界问题都暴露出来。4. 从打印沙漏到一类图形题真正能带走的方法论4.1 图形输出题的通用拆题流程沙漏题只是图形输出大类里的一个代表。很多类似的题——打印菱形、打印三角形、打印九九乘法表、打印字母金字塔——底层思路完全一致。我总结的拆题流程是四步第一步把图形画在纸上不要直接在脑子里想。人脑对二维图形的缓存能力有限落笔才能看清规律。第二步给每一行编号然后写下这一行的空格数和符号数。这一步的本质是建立“行号到输出参数”的函数关系。以沙漏为例行号 i 和空格数、符号数之间是分段函数用距离公式后可以合并成带绝对值的表达式。第三步根据映射关系写循环。外层循环管行号内层循环先打空格再打符号。只要映射表列清楚了代码基本不会写错。第四步重点测试边界n 1、N 刚好等于某个完整沙漏符号数、N 比完整沙漏数大 1 且剩 1 个符号。这三个边界过掉题目基本稳了。4.2 学会“改一改”比“背代码”更有用沙漏题可以做很多变体。比如打印“空心沙漏”***** * * * * * *****改法很简单每一行先打印星号如果这一行既不是第一行也不是最后一行并且符号数大于 1那么中间的符号全部换成空格只在行首和行尾保留星号。用距离公式的写法就是先打印空格再判断当前行位置决定是打印 2×m1 个符号还是只打两个端点符号。再比如打印“菱形”它和沙漏刚好相反中间行符号数最多两端最少。只要把距离公式里的符号数从2×m1改成2×(n-m)-1空格数映射反过来就是一个标准菱形。如果你真正理解了行号映射表这个概念会发现这些题全是换汤不换药。4.3 我的实战心得不要急着让代码“跑起来”最后说点个人带新人的体会。我发现很多新手一拿到题目就开编辑器敲代码觉得敲得越快完成越快。但实际上图形题里最耗时间的部分永远是“思考规律”而不是“写循环”。一行行硬编码也能过样例但一旦输入变化代码就废了。我自己的习惯是拿到题先花五分钟在草稿纸上画图、列表、推导公式。把“17 个星号 5 3 1 3 5”这种关系写出来把“第 i 行空格多少、符号多少”这张表列出来代码只是把表翻译成循环。这个方法在打印沙漏、打印菱形、打印金字塔上都适用练熟之后你再看见任何“把符号打印成某种形状”的题第一反应都会变成“先找规律”而不是“先硬写”。这种先建模、再编码的习惯其实比通过这一道题本身更有价值。以后遇到复杂一点的算法题面对一个模糊的问题描述你能不能把它拆成清晰的数学结构和边界条件决定了你能走到哪一步。