ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

蓝桥杯打印图形题的通法:用Bresenham算法绘制空心六芒星

蓝桥杯打印图形题的通法:用Bresenham算法绘制空心六芒星 打印图形在蓝桥杯里属于那种“看起来简单、做起来翻车”的典型题目。尤其六芒星这种带斜线、带交叉、带对称的图形很多同学第一反应是找规律然后对着样例硬凑循环最后输出错位、少星号、多空格交上去一脸懵。这篇我把六芒星从建模到画线到最终输出完整拆一遍思路是通用的理解之后换个图形也能直接用。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 11W 11center 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 语言差异小结语言初始化方式常见坑JavaArrays.fill(g[i], )记得先 new 每一行Cmemset(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 从六芒星题目里能带走什么写完这道题你真正收获的其实不只是“会打印六芒星”而是理解了一种处理离散网格问题的通用方法把几何问题坐标化把坐标问题数组化。这个方法在以后做迷宫问题、棋盘覆盖、贪吃蛇、扫雷扩展格子等问题时都会反复用到。六芒星只是一个载体背后的思维模型才是关键。最后说一点个人体会打印图形题在蓝桥杯里属于性价比很高的题它不像动态规划那样需要大量理论积累也不像图论那样需要背模板只要熟练掌握了坐标建模和画线算法很多变体题都能在几分钟内搞定。建议大一大二准备蓝桥杯的同学把这类型题当作优先掌握的对象练上三五道同类题基本上就能在赛场上拿到这部分分数。
返回列表