打印图形在蓝桥杯里属于那种“看起来简单、做起来翻车”的典型题目。尤其六芒星这种带斜线、带交叉、带对称的图形,很多同学第一反应是找规律,然后对着样例硬凑循环,最后输出错位、少星号、多空格,交上去一脸懵。这篇我把六芒星从建模到画线到最终输出完整拆一遍,思路是通用的,理解之后换个图形也能直接用。
1. 先从“打印图形题”在蓝桥杯里的地位说起
蓝桥杯的打印图形题,省赛、国赛都出过,难度跨度很大。简单版让你打印直角三角形、菱形,这种基本是送分题;进阶版就上难度了,比如打印字母沙漏、打印螺旋矩阵、打印带参数的三角形组合体。六芒星属于进阶偏中等的一档,它本质上考的不是语法,而是你能不能把一个数学图形转化成二维数组里的坐标点。
先看常规套路。大部分人的做题思路是这样的:输入一个n,然后判断每行输出几个空格几个星号。像打印菱形,公式是现成的,第i行空格数、星号数都能直接算出来。但六芒星不行,因为六芒星不是简单的“左右对称逐行增减”,它有斜边、有交叉、有三角形叠加,硬找规律很容易把自己绕进去。
我见过不少人拿到六芒星题第一反应是:“这不就是两个三角形嘛,正着打印一遍,反着打印一遍。”理论上没错,但实际写的时候会发现,两个三角形叠加的部分怎么处理?中间交叉区域的空格怎么算?上下三角形的边长比例怎么定?这些问题靠肉眼找规律,效率极低,还容易出错。
所以这篇我换一个思路:把六芒星看作几何图形,在二维网格里画出来。这个思路对任何打印图形题都成立,而且代码写起来非常稳定,不容易翻车。
题目的输入输出我们做一个约定:输入一个正整数n,输出一个由星号组成的空心六芒星。六芒星由两个等边三角形叠加而成,正三角形尖朝上,倒三角形尖朝下,两者完全对称。n决定图形的大小,具体几何参数下面会讲。
2. 六芒星建模:一张纸一支笔解决一半问题
拿到这种题,第一步永远不是打开编译器,而是找张草稿纸把图形画出来。画完之后你会发现,所谓六芒星,本质上就是两个三角形的六条边。
2.1 顶点坐标怎么定
我采用的建模方式是这样的:
- 设总行数 H = 4n - 1,总列数 W = 4n - 1。为什么用 4n - 1?因为要同时容纳正三角形和倒三角形,并且让它们有足够的交叉区域,视觉上才像六芒星。如果直接用 2n,两个三角形只能勉强碰到,出来的图形更像沙漏,不像六芒星。
- 中心列 center = 2n - 1,这样左右两侧各留 2n - 1 列,图像居中。
- 正三角形(尖朝上):顶点 A = (0, center),底边左端点 B = (3n - 3, 0),底边右端点 C = (3n - 3, W - 1)。
- 倒三角形(尖朝下):顶点 F = (H - 1, center),底边左端点 D = (n, 0),底边右端点 E = (n, W - 1)。
以 n = 3 为例,H = 11,W = 11,center = 5。那么正三角形的顶点是 (0,5),底边在行 6,从列 0 到列 10;倒三角形的顶点是 (10,5),底边在行 3,从列 0 到列 10。两个三角形在行 3 到行 6 之间交叉,这个交叉区就是六芒星中间六边形部分的来源。
2.2 六条边的清单
确定了顶点,六芒星其实就是以下六条线段的集合:
| 线段 | 起点 | 终点 | 说明 |
|---|---|---|---|
| AB | (0, center) | (3n-3, 0) | 正三角形左腰 |
| AC | (0, center) | (3n-3, W-1) | 正三角形右腰 |
| BC | (3n-3, 0) | (3n-3, W-1) | 正三角形底边 |
| DE | (n, 0) | (n, W-1) | 倒三角形底边 |
| DF | (n, 0) | (H-1, center) | 倒三角形左腰 |
| EF | (n, W-1) | (H-1, center) | 倒三角形右腰 |
把这张表列出来之后,问题就从“怎么找规律”变成了“怎么在二维数组里画线段”。这是质的转变,因为画线段是一个已经被计算机图形学解决得很彻底的问题。
2.3 从图形到二维数组的映射
二维数组的每个格子 g[r][c] 对应画纸上的一个点,初始全部填空格,把六条边经过的格子填成星号,最后逐行输出数组,图形就出来了。
这里有个关键认知:字符画的坐标系和数学坐标系不一样。数学里 y 轴向上,字符画里行号向下;数学里点可以是浮点数,字符画里格子是整数坐标。所以画线的时候,必须处理“一条斜线经过哪些整数格子”这个问题。这就是下一节要讲的 Bresenham 算法。
3. 核心难点:怎么把直线画进字符网格
Bresenham 算法是计算机图形学里画直线的经典算法,用在打印图形题里可以说是降维打击。它的核心思想非常直观:在网格上从起点走向终点,每一步要么沿着主轴方向走一格,要么斜着走一格,具体怎么走,取决于当前偏离理想直线的误差。
3.1 为什么不能简单取整
最简单粗暴的画线方式是:遍历每一行,算出理想列号,四舍五入填星号。比如从 (0,5) 到 (6,0),斜率为 -5/6,第 1 行理想列号是 5 - 5/6 ≈ 4.17,四舍五入得 4;第 2 行是 5 - 10/6 ≈ 3.33,四舍五入得 3。
这个办法在小规模图形里完全够用,代码也简单。但有个问题:如果斜率很陡,比如从 (0,5) 到 (10,6),每行列号变化不到 0.1,取整之后会连续好几行都落在同一列,线条在某一行会出现断裂感。Bresenham 算法通过误差累积,能保证线条连续且尽可能贴近理想直线。
3.2 误差变量的直觉理解
想象你从操场这头走向对角线那头,理想路线是一条直线。但你的每一步只能向东、向南、或向东南斜跨一格。怎么走才能离直线最近?
Bresenham 的回答是:每走一步,维护一个“当前点偏离直线的误差值”。这个误差是从起点开始累积的。每一步检查:如果误差小于某个阈值,就沿主轴方向走;一旦误差超过阈值,就拐一下,斜着走,然后修正误差。
用代码说话,画线的核心逻辑是这样的:
static void drawLine(int r1, int c1, int r2, int c2) { int dr = Math.abs(r2 - r1); int dc = Math.abs(c2 - c1); int sr = (r1 < r2) ? 1 : -1; int sc = (c1 < c2) ? 1 : -1; int err = dr - dc; while (true) { g[r1][c1] = '*'; if (r1 == r2 && c1 == c2) break; int e2 = 2 * err; if (e2 > -dc) { err -= dc; r1 += sr; } if (e2 < dr) { err += dr; c1 += sc; } } }这段代码有三个关键点:
err = dr - dc是初始误差,它表示“行方向差距和列方向差距的差值”。- 每次循环用
e2 = 2 * err来判断,乘以 2 是为了避免和 0 比较时出现精度问题,纯整数运算。 - 两个 if 可能同时成立,也就是说可以斜着走一步——这是 Bresenham 比简单取整更优雅的地方,它在同一行内最多只填一个点,保证了线的单点连续性。
很多初学者第一次看这段代码会觉得绕,其实不需要背,只需要理解:它每一步都在回答“我应该继续直走,还是拐一下?”,判断的依据就是误差有没有超过半格。
4. Java完整实现与运行效果
完整代码不长,结构也很清晰:初始化二维数组、画六条边、输出结果。
import java.util.Arrays; import java.util.Scanner; public class Hexagram { static char[][] g; static int H, W; public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); sc.close(); H = 4 * n - 1; W = 4 * n - 1; g = new char[H][W]; for (int i = 0; i < H; i++) { Arrays.fill(g[i], ' '); } int center = 2 * n - 1; // 正三角形顶点和底边两端点 int[] A = {0, center}; int[] B = {3 * n - 3, 0}; int[] C = {3 * n - 3, W - 1}; // 倒三角形底边两端点和顶点 int[] D = {n, 0}; int[] E = {n, W - 1}; int[] F = {H - 1, center}; // 画六条边 drawLine(A[0], A[1], B[0], B[1]); // 正三角左腰 drawLine(A[0], A[1], C[0], C[1]); // 正三角右腰 drawLine(B[0], B[1], C[0], C[1]); // 正三角底边 drawLine(D[0], D[1], E[0], E[1]); // 倒三角底边 drawLine(D[0], D[1], F[0], F[1]); // 倒三角左腰 drawLine(E[0], E[1], F[0], F[1]); // 倒三角右腰 printGraph(); } static void drawLine(int r1, int c1, int r2, int c2) { int dr = Math.abs(r2 - r1); int dc = Math.abs(c2 - c1); int sr = (r1 < r2) ? 1 : -1; int sc = (c1 < c2) ? 1 : -1; int err = dr - dc; while (true) { g[r1][c1] = '*'; if (r1 == r2 && c1 == c2) break; int e2 = 2 * err; if (e2 > -dc) { err -= dc; r1 += sr; } if (e2 < dr) { err += dr; c1 += sc; } } } static void printGraph() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < H; i++) { for (int j = 0; j < W; j++) { sb.append(g[i][j]); } sb.append('\n'); } System.out.print(sb.toString()); } }运行 n = 3,输出效果大致如下(控制台字体不同可能有细微视觉差异,但坐标是准确的):
* * * * * * * * * * * * * * * * * * * * * * * * * * *注意第 10 行只有一个星号,这是倒三角形的顶点,和第 0 行的正三角形顶点上下呼应。中间几行出现四个星号,是两条腰和底边交叉造成的,这正是两个三角形叠加的正常效果。
4.1 这段代码有哪些易错点
第一个易错点:行列坐标搞反。数组索引g[r][c],r 是行、c 是列,画线的时候传参顺序一定不能乱。建议统一用(行, 列)的顺序写所有方法,别一会儿(x, y)一会儿(r, c)。
第二个易错点:底边行号推导。正三角形底边在3n - 3,倒三角形底边在n,这两个值是建模时确定的,改 n 的时候一定要代入检查一遍。n = 3 时代进去分别是 6 和 3,交叉区是行 3、4、5、6,共 4 行;n = 4 时分别是 9 和 4,交叉区更大,图形舒展。
第三个易错点:输出时不要自动加空格。很多同学打印字符画的时候,喜欢用System.out.print(g[i][j] + " ")来对齐,这在打印较小图形时看着还行,一旦图形变大,整个图会横向拉宽,星号之间的相对位置全变了。正确做法是直接输出数组内容,利用控制台的等宽字体自然对齐。
5. C++和Python也能用同一套思路秒掉
蓝桥杯允许的语言很多,Java、C++、Python都有。思路完全一样,区别只在语法细节。
5.1 C++版本
#include <bits/stdc++.h> using namespace std; char g[100][100]; int H, W; void drawLine(int r1, int c1, int r2, int c2) { int dr = abs(r2 - r1); int dc = abs(c2 - c1); int sr = (r1 < r2) ? 1 : -1; int sc = (c1 < c2) ? 1 : -1; int err = dr - dc; while (true) { g[r1][c1] = '*'; if (r1 == r2 && c1 == c2) break; int e2 = 2 * err; if (e2 > -dc) { err -= dc; r1 += sr; } if (e2 < dr) { err += dr; c1 += sc; } } } int main() { int n; cin >> n; H = 4 * n - 1; W = 4 * n - 1; memset(g, ' ', sizeof(g)); int center = 2 * n - 1; drawLine(0, center, 3 * n - 3, 0); drawLine(0, center, 3 * n - 3, W - 1); drawLine(3 * n - 3, 0, 3 * n - 3, W - 1); drawLine(n, 0, n, W - 1); drawLine(n, 0, H - 1, center); drawLine(n, W - 1, H - 1, center); for (int i = 0; i < H; i++) { for (int j = 0; j < W; j++) { cout << g[i][j]; } cout << '\n'; } return 0; }C++ 的memset按字节填充,对 char 数组填空格没问题。sizeof(g)用在全局数组上能正确得到总字节数,但如果数组变成局部变量,就得手动传大小,这是个容易踩的坑。
5.2 Python版本
def draw_line(grid, r1, c1, r2, c2): dr = abs(r2 - r1) dc = abs(c2 - c1) sr = 1 if r1 < r2 else -1 sc = 1 if c1 < c2 else -1 err = dr - dc while True: grid[r1][c1] = '*' if r1 == r2 and c1 == c2: break e2 = 2 * err if e2 > -dc: err -= dc r1 += sr if e2 < dr: err += dr c1 += sc def main(): n = int(input()) H = 4 * n - 1 W = 4 * n - 1 grid = [[' ' for _ in range(W)] for __ in range(H)] center = 2 * n - 1 draw_line(grid, 0, center, 3 * n - 3, 0) draw_line(grid, 0, center, 3 * n - 3, W - 1) draw_line(grid, 3 * n - 3, 0, 3 * n - 3, W - 1) draw_line(grid, n, 0, n, W - 1) draw_line(grid, n, 0, H - 1, center) draw_line(grid, n, W - 1, H - 1, center) for row in grid: print(''.join(row)) if __name__ == '__main__': main()Python 版本有个新手容易犯的错:初始化二维数组时用[[' '] * W] * H,这样每一行都是同一个列表的引用,改一行会带动所有行变。一定要用列表推导式[[' ' for _ in range(W)] for __ in range(H)]。
5.3 语言差异小结
| 语言 | 初始化方式 | 常见坑 |
|---|---|---|
| Java | Arrays.fill(g[i], ' ') | 记得先 new 每一行 |
| C++ | memset(g, ' ', sizeof(g)) | 数组别开小了,n 最大时 4n-1 可能超界 |
| Python | 列表推导式逐行创建 | 不要用乘法复制行引用 |
这些坑我在比赛训练时都踩过,尤其是 Python 那个引用问题,排查了半天才发现改一行全变了。
6. 进阶玩法:实心六芒星与外轮廓变体
掌握了画线法,六芒星的变体题也能轻松应对。蓝桥杯很喜欢在一个图形上做文章,可能同一道题换个问法,从打印空心变成打印实心,或者只打印外轮廓。
6.1 实心六芒星怎么实现
实心六芒星就是两个三角形的内部并集。判断一个点是否在三角形内部,可以用“左右边界夹逼”的办法。
正三角形的内部条件:
- 行 r 在 0 到 3n-3 之间
- 列 c 在左腰和右腰之间
左腰的边界函数:left = round(center - center * r / (3n - 3)),右腰的边界函数:right = round(center + center * r / (3n - 3))。
倒三角形的内部条件:
- 行 r 在 n 到 H-1 之间
- 列 c 在左腰和右腰之间
判断代码如下:
static boolean insideUp(int r, int c, int n, int center) { if (r < 0 || r > 3 * n - 3) return false; double ratio = (double) r / (3 * n - 3); int left = (int) Math.round(center - center * ratio); int right = (int) Math.round(center + center * ratio); return c >= left && c <= right; } static boolean insideDown(int r, int c, int n, int center) { int H = 4 * n - 1; if (r < n || r > H - 1) return false; double ratio = (double) (r - n) / (H - 1 - n); int left = (int) Math.round(ratio * center); int right = (int) Math.round((W - 1) - ratio * center); return c >= left && c <= right; }实心版的输出会非常饱满,整个六芒星被星号填满,视觉效果和空心版完全不同。这类题考察的是对“多边形内部”的理解,边界条件的处理比画线更考验细心。
6.2 只画外轮廓怎么改
有时候题目要求的是六芒星的外轮廓,也就是一个带六个尖角的六边形,中间没有内部线条。这种情况下,两条底边(BC 和 DE)不应该画出来,只画四条腰就够了。但是要注意,这四条腰需要截断到外轮廓的范围内,否则线条延伸到内部交叉区域,又会形成多余的线。
更稳妥的方案是:不用画线法,而是用“逐行扫描”法。对每一行,计算出外轮廓最左和最右的星号列号,只输出这两个。这样无论图形多复杂,输出一定是一个标准的空心外轮廓。
具体来说,对于六芒星的外轮廓,可以把它看作由六条线段围成的封闭六边形,每条线段用一个线性函数表示,然后逐行求左右边界。这个思路和画线的区别在于:画线法是把线画上去,扫描法是每行算边界。
6.3 同一套框架画其他图形
这个思路最大的价值在于通用性。打印三角形、菱形、六边形、五角星,本质上都是“在坐标网格上画线”或“判断点是否在多边形内”。
我之前拿这套框架打印过一个正五角星,把五个顶点坐标算出来,用 Bresenham 画五条边,十几行代码就搞定了。如果按照“找规律”的套路,五角星每行的空格和星号位置根本没有简单公式,硬算会非常痛苦。
| 图形 | 建模方式 | 核心操作 |
|---|---|---|
| 菱形 | 两个三角形上下拼接 | 画四条腰 |
| 六边形 | 六条边闭合 | 画六条线段 |
| 五角星 | 五个顶点按顺序连线 | 画五条线段 |
| 螺旋矩阵 | 一圈一圈向内缩 | 填数而非画线 |
所以与其针对每一种图形背一种规律,不如掌握“坐标建模 + 画线/判断”这套通法。
7. 打印图形题的通法:我踩过坑之后的做题流程
这部分分享的做题流程,是我刷了大量打印图形题之后总结出来的,不敢说最优,但一定比上来就写代码靠谱得多。
第一步,画图。把题目给的样例图形抄到草稿纸上,能画多大画多大。用尺子更好。画的过程中标出关键顶点,比如六芒星的六个顶点、三角形的底边位置。
第二步,定坐标。确定图形的总行数、总列数,给顶点坐标编号。这一步解决的是“图形长什么样”的问题,和具体编程语言无关,也是整道题的核心。
第三步,选方法。简单规则图形,比如菱形、三角形,可以用逐行公式法;复杂图形,比如六芒星、五角星,用坐标建模 + 画线法。判断标准是:图形每行是否有简单的递推关系,如果有就用公式,没有就老老实实画线。
第四步,编码测试。建议先写一个能输出最小 n 的版本,验证坐标计算正确,再测大 n 看图形是否变形。特别注意检查第一行、最后一行、中间交叉行的输出是否符合预期。
第五步,边界检查。n = 1 的时候图形变成什么样?有些题目 n 的下限不是 1,可能是 2 甚至更大,需要读题确认。n 很大时数组够不够大?Java 里char[][]开到几百乘几百完全没压力,但如果你用固定数组 C++ 就得算清楚上限。
7.1 实际做题时的心态调整
打印图形这类题有个特点:代码通常不超过几十行,但调试起来可能要花一两个小时。原因在于,错误往往不是语法错误,而是坐标计算错了某个偏移量,导致整个图形右边多一个空格、左边少一个星号。
我的建议是:如果在比赛或练习中遇到这种题,先把建模思路写在草稿纸上,不要急着敲代码。一个清晰的顶点坐标表,比十次盲目的试错都管用。
另一个建议:这种题适合用“辅助输出”来调试。在开发阶段,可以在 drawLine 函数里加一行调试代码,把每次画的起点和终点打印出来,对着一一核对,确认六条线都画对了再输出图形。比赛时记得删掉调试代码,这算是我踩过的一个小坑——有次省赛模拟题,我忘了删调试输出,直接影响了最终结果。
7.2 从六芒星题目里能带走什么
写完这道题,你真正收获的其实不只是“会打印六芒星”,而是理解了一种处理离散网格问题的通用方法:把几何问题坐标化,把坐标问题数组化。这个方法在以后做迷宫问题、棋盘覆盖、贪吃蛇、扫雷扩展格子等问题时都会反复用到。
六芒星只是一个载体,背后的思维模型才是关键。
最后说一点个人体会:打印图形题在蓝桥杯里属于性价比很高的题,它不像动态规划那样需要大量理论积累,也不像图论那样需要背模板,只要熟练掌握了坐标建模和画线算法,很多变体题都能在几分钟内搞定。建议大一大二准备蓝桥杯的同学,把这类型题当作优先掌握的对象,练上三五道同类题,基本上就能在赛场上拿到这部分分数。