洛谷显示屏题全攻略:字符画模拟题的按行输出与边界处理
洛谷题库里有一类题看着像是在考图形学实际上就是在考你能不能老老实实地按行输出——显示屏题就是其中之一。我第一次做这道题的时候第一反应是“这还不简单”结果连续两个版本栽在输入处理和行尾空格上最后虽然AC了但多花了大半个小时去查一个并不复杂的边界条件。后来回头看这道题恰恰是把“模拟题的基本素养”考得很透读懂输出格式、选对数据结构、看清输入终止条件三样缺一不可。这篇文章把这道显示屏题从题意、思路到完整代码再到我在提交时踩过的各种坑一次性讲清楚。无论你是刚接触这类字符图形题的入门选手还是想练模拟题手感的老选手都可以参考一下我的处理方式尤其是第四部分的WA复盘那几段代码我都是真正交上去吃过亏的。1. 这道显示屏题到底在考什么1.1 题面还原与样例推演尽管每个人的记忆版本不太一样但这类“显示屏”题目的内核高度统一给你一个不超过十位的非负整数要求把每一位数字放大成高5行、宽3列的块状图案然后按顺序横向拼接数字和数字之间空一列最终输出一个完整的字符画。举个例子如果输入是123那么按题目格式输出的第三行长这样X.X ..X XXX用点号代替空格再看会更清楚数字1写成三列“..X”数字2是“XXX”数字3是“XXX”中间用一列空格隔开整个宽度就是3 1 3 1 3 11列。总共输出5行形成一整块显示屏。我当时拿到题面后先做了一件事把0到9共十个数字的5行3列字形全部用字符串画出来画在草稿纸上。这一步看着蠢其实特别有用因为后面的代码能不能写得短、写得对完全取决于你对这十个模板的直觉是否清晰。比如数字4是左上、右上、中间三行有横线和竖线底行中间是点数字7只有三行横线在顶部底下全是右侧竖线。这些细节一旦记错输出就会露馅。1.2 三个最容易翻车的隐藏细节第一处隐藏细节是“数字与数字之间空一列”但行末不能有多余空格。这句话看起来是常识实际写代码时特别容易顺手在每一行末尾多打一个空格。大部分判题机对行尾空格并不宽容多出来的字符会被当成输出错误。第二处是输入不只有一个数字。这道题经常写成多组数据不断读入非负整数直到读入负数才停止。很多人写循环时忘了把负数排除在输出之外结果最后多打印了一个“-1”的字形WA得莫名其妙。第三处是数字0本身。题目说的是“非负整数”所以0是合法输入。有些同学看到“非负”就下意识地判断“如果数字大于0才输出”这样一来输入0时整个输出变成空白直接丢分。这三个细节单独拎出来都不难但它们会组合在一起变成一道专门消耗你耐心的模拟题。2. 我把思路从“拼接大图”改成“逐行打印”的全过程2.1 我拿到题的第一反应先把整个数字拼成五行我最初的想法非常直观既然每个数字占3列那我能不能先把每一位数字单独扩展成一个宽度为3的小片段然后把所有片段拼起来这个思路方向是对的但很多人会进一步想歪能不能先把每一位数字的5行字形“竖着拼”成一个完整的、只属于这个数字的5行块再把所有数字块“横着拼”如果真按这个思路实现你会发现自己陷入一堆字符串拼接的麻烦里每个数字要转成5行字符串每行还要记得补空格最后还得处理块与块之间的分隔列。代码越写越长bug越调越多。我当时写的第一版就是这么干的结果字符画内部疯狂错位尤其是数字2、5、6这种“左右不对称”的形状一旦拼接时少了一个空格整行图案就全歪了。2.2 写成循环后的关键外层按行内层按位后来我强迫自己重新观察输出结构才意识到一个更简单的事实不管有多少位数字每一行输出的内容永远是“第1个数字的第row行 分隔空格 第2个数字的第row行 分隔空格 ...”。所以循环顺序应该是外层循环是行号从第0行跑到第4行内层循环是数字串的每一位最里层再枚举该数字该行的3个字符。这和我之前设想的“先拼每个数字再组合”完全不同。写成伪代码就是for row in 0..4: for i in 0..len(s)-1: 输出 数字s[i]的第row行字形 如果 i 不是最后一位输出一个空格 输出换行这个结构才是整道题的核心。先想清楚“按行扫描”而不是“按数字拼接”代码量至少减少一半出错概率也大幅下降。这也是我想重点说的字符图形类模拟题分析的落脚点永远是输出结构的行列关系而不是某一个实体的完整形状。2.3 为什么预存字形表而不是现场用if生成有的同学可能会问十个数字的字形能不能不存表直接在循环里用 if 判断“这个数字第row行第col列是X还是空格”理论上可以但实际写起来非常痛苦。每个数字有3×515个格子十个数字就是150个格子你要用一堆 if-else 去描述“数字3的第三行第二列是X”这种信息不仅容易写错而且别人根本看不懂你的代码。更合理的做法是把字形直接预存成一个二维字符串数组比如mp[数字][行]表示某个数字的某一行字符串这样不仅直观而且后续调试时打印这个数组就能肉眼检查对不对。这里其实体现了模拟题的一个通用原则能用数据表表示的规则就不要用控制流去硬算。字形本身是静态数据静态数据就应该用静态表存储循环只负责“搬运”和“拼接”。3. 两份AC参考代码C和Python附逐段拆解3.1 C版二维字形数组 按行输出我习惯用C写竞赛代码下面这个版本是完全围绕上一节的“按行扫描”思路实现的也是我最终AC的版本#include iostream #include string using namespace std; // 数字0~9的字形模板高5行宽3列 // 用.表示空白输出时统一替换为空格 const string mp[10][5] { {XXX, X.X, X.X, X.X, XXX}, // 0 {..X, ..X, ..X, ..X, ..X}, // 1 {XXX, ..X, XXX, X.., XXX}, // 2 {XXX, ..X, XXX, ..X, XXX}, // 3 {X.X, X.X, XXX, ..X, ..X}, // 4 {XXX, X.., XXX, ..X, XXX}, // 5 {XXX, X.., XXX, X.X, XXX}, // 6 {XXX, ..X, ..X, ..X, ..X}, // 7 {XXX, X.X, XXX, X.X, XXX}, // 8 {XXX, X.X, XXX, ..X, XXX} // 9 }; int main() { string s; while (cin s) { if (s[0] -) break; // 负数是结束标记 for (int row 0; row 5; row) { for (int i 0; i (int)s.size(); i) { int d s[i] - 0; for (int col 0; col 3; col) { char ch mp[d][row][col]; cout (ch . ? : ch); } if (i 1 (int)s.size()) cout ; // 数字间空一格 } cout \n; } } return 0; }3.2 代码拆解每一行在干什么我先把几个关键点拎出来说一下。首先是const string mp[10][5]。这个数组的下标含义是mp[数字][行号]比如mp[3][2]就表示数字3的第2行字符串内容是XXX。存储的时候我特意用点号做空白因为纯空格字符串在源代码里非常容易数错位数而且显示也不清楚用点号占位后每一格都看得明明白白。其次是读取方式。我用string s直接读而不是int n这么做的好处有三个一是题目说数字不超过十位用字符串读天然不用担心溢出二是判断负数只需要看s[0] -三是后续要逐位输出字形字符串可以直接按下标访问非常省事。然后是输出循环。最外层for (int row 0; row 5; row)固定循环5行中间层遍历输入串的每一位数字最里层遍历该数字该行的3个字符。这里容易搞错的是三个循环的嵌套顺序我见过不少同学把row循环放到最里面导致输出变成“先把第一个数字的五行走完再走第二个数字”那是垂直拼接不是横向显示屏。最后是分隔列的处理。我采用的方式是每输出完一个数字块只要它后面还有数字就输出一个空格。这样最后一个数字后面不会产生多余空格行尾干净。3.3 Python版同一思路的另一种写法如果你比赛时习惯用Python代码可以写得更简短import sys # 数字0~9的字形5行3列.表示空白 digits [ [XXX, X.X, X.X, X.X, XXX], # 0 [..X, ..X, ..X, ..X, ..X], # 1 [XXX, ..X, XXX, X.., XXX], # 2 [XXX, ..X, XXX, ..X, XXX], # 3 [X.X, X.X, XXX, ..X, ..X], # 4 [XXX, X.., XXX, ..X, XXX], # 5 [XXX, X.., XXX, X.X, XXX], # 6 [XXX, ..X, ..X, ..X, ..X], # 7 [XXX, X.X, XXX, X.X, XXX], # 8 [XXX, X.X, XXX, ..X, XXX], # 9 ] for line in sys.stdin: s line.strip() if not s or s[0] -: break for row in range(5): parts [] for ch in s: d int(ch) parts.append(digits[d][row].replace(., )) print( .join(parts))Python版的核心逻辑和C一模一样区别只在输出拼接方式上。 .join(parts)这一句的作用是在所有数字块之间自动插入一个空格同时保证行尾没有多余空格可以说是这个写法里最优雅的一步。需要提醒的是Python的循环读入和C不太一样。for line in sys.stdin会把文件末尾当成自然结束但题目要求遇到负数结束所以循环里要判断s[0] -。另外如果某一行是空行要用if not s把它过滤掉否则下面s[0]会越界。4. WA复盘五个让我交过学费的输入输出边界4.1 负数结束符最容易把负号一起输出我最早犯的一个错误是读完字符串后没有判断负号直接把整个字符串送进输出循环。结果数据末尾的-1被当成两个字符“-”和“1”程序先试图打印负号的字形然后打印数字1的字形。为什么这会挂因为我的字形表里根本没有负号访问mp[- - 0]直接越界就算没越界输出也会多出一整块“-1”的形状。后来我学乖了任何“读到负数结束”的题第一件事就是检查终止标记而且要把终止标记排除在正常数据处理之外。4.2 数字0别让特判把输出清空第二个坑是数字0。我有个朋友第一次写这题时加了一个判断“如果数字是0就什么都不输出。”他的逻辑是0前面不应该有前导字符。问题是题目输入0意思就是要在屏幕上显示一个放大版的数字0而不是什么都不显示。我自己当时虽然没有犯这个错但在设计边界测试时专门用0去测了一遍输出确认前五行都是完整的“XXX / X.X / X.X / X.X / XXX”才放心。建议你们也这样凡是输入范围包含0的题测试用例里必须包含0。4.3 行尾空格的强迫症问题第三个坑回到行尾空格。有些判题系统对行尾空格是宽容的但正如我前面说的这道题如果严格比对行尾多一个空格就会出错。避免行尾空格有两种常见写法一种是像我的代码那样在输出数字块之前判断“是不是最后一个”不是才输出分隔空格另一种是先构造整行字符串最后去掉末尾空格。第二种写法在Python里很常见比如print( .join(parts))在C里则要谨慎使用cout ch 这种无脑追加的写法很容易在最后一个数字后面也跟一个空格。4.4 用string读还是用int读前导零之争还有一个容易被忽略的问题到底用string读还是int读如果你的输入是0123用int读进来会变成123自动去掉前导零用string读进来则保留0123输出时会打印4个数字的字形。常规判题数据一般不会给你带前导零的数字因为题目说的是“非负整数”不是“数字串”。但如果遇到练习数据恰好带前导零你就要想清楚自己期望输出哪种结果。我个人倾向于用string读因为题目限制数字位数不超过10位不存在溢出风险而且字符串访问每一位天然方便。如果你实在担心前导零问题可以用stoi转换后再转回字符串只是那样会多一步。4.5 多组数据之间到底要不要空行最后一个坑是关于输出格式的。有的题面会写“每组输出之间空一行”有的不会写。这道题里多组数据之间通常不需要空行直接连续打印即可。我遇到过一些同学在每组数据输出结束后习惯性地加一个cout endl理由是“多打一个空行也不影响吧”。如果题目没有要求这种多余空行真的会影响结果因为判题程序会把你输出的每一行和期望输出做逐行比较。所以遇到不确定的情况最稳妥的办法就是严格按照题面给的样例输出格式来一个多余的空行都不要加。5. 一套能平移到所有“字符画”题目的通用模板5.1 所有“画字符画”题目的共同骨架把这道题吃透之后你会发现很多“打印字符画”的题目不管图形是数字、字母、星星还是象棋棋盘底层结构都惊人地相似准备静态模板表图形每一行的内容 循环行号: 循环元素列表: 从模板表中取出当前元素当前行的内容并输出 如果不是最后一个元素输出分隔符 输出换行这个骨架的精髓在于“通过行号把多个元素横向对齐”。只要图形高度固定、每个元素宽度固定你都可以这么写。比如改成输出大写字母你只需要把mp[10][5]换成mp[26][5]再把模板字符串换成字母的形状主循环一行都不用改。如果再遇到要求“每个字符放大K倍”的题目思路也不变先按放大倍数扩展模板表比如原本一行的3个字符变成3*K个字符再套用同样的循环结构。所以这道题的价值并不只是让你AC一道水题而是帮你攒下一套可复用的模拟题套路。5.2 调试技巧打印表 对照样例我在调试这类题目时有一个很笨但很好用的技巧先写一个单独的打印函数把整个mp表输出到屏幕上肉眼核对每个数字的字形是否正确。因为字符图形题的报错很难直接从WA信息里看出来没准错在第3行第2列一个空格上你盯着样例根本盯不出问题。但如果你把自己的模板表打印出来和题面给出的数字示意对比一遍基本能第一时间发现是模板错了还是拼接逻辑错了。我在写这道题时就靠这个方法发现了数字4的第二行应该是X.X而不是XXX草稿上画得好好的写进代码里手一抖就写错了。另外提交前一定要在本地把输入0和输入负数这两条边界跑一遍。0保证输出不是空白负数保证程序能正常结束这两点几乎是所有模拟题通用自检项。5.3 扩展点阵屏、缩放显示与更大的模板如果你对这类题感兴趣还可以继续往几个方向扩展。比如把数字放大成“点阵屏”样式使用*和空格表示亮灭这就变成了一个位图图像处理的小项目再比如要求数字之间用两个空格分隔、或者要求输出多行文本组成的完整句子只要回到“按行扫描”的骨架都能轻松应对。我后来做字符画相关的小工具时还顺手把模板表换成了vectorstring把渲染逻辑独立成一个函数输入任意字符串就能输出对应的字符画。这种做法本质上还是从这道题里迁移过去的。所以别小看这种入门模拟题很多复杂问题的瓶颈恰恰出在你能不能把最基本的“按行输出”写得干净利落。最后讲一个个人心得这种题WA几次通常不是你不会而是你急着把代码写完提交。我建议拿到题目后先在草稿纸上把输出样例手推一遍再动键盘顺序对了一道模拟题能省下二十分钟的调试时间。这道显示屏题就是最好的练手对象。