看到“超级玛丽游戏”这个题名,老OI人应该都会心一笑——这哪是要你写一个能跳能踩蘑菇的马里奥,而是要求你在屏幕上原样输出一张固定的字符画。很多第一次接触信息学奥赛的同学,刷题列表里第一道题往往就是它。题目编号里的“1.1”说明它属于基础篇第一组,“10”则表明它是这一组里靠后的题号。但你别因为它是个“输出题”就轻视,恰恰是这种零逻辑、纯输出的题,最能暴露一个新手在格式、细节和工具链上的短板。这篇就把这道题从题目分析、代码实现到提交避坑,完完整整拆开讲一遍。
1. 题目拆解:这题到底在考什么
1.1 字面任务:输出一张固定字符画
先看任务本质:程序运行后,在终端或者评测系统的标准输出里,逐行打印一个由星号、井号、点号、空格组成的“超级玛丽”像素图形。图形本身是固定的,所有行、所有列的内容题目里都给得明明白白,不需要你算、不需要你判断、不需要你输入任何数据。你的程序只要做一件事——把这个图形一个字符不差地打出来。
这道题放在NOI基础篇里,目的非常纯粹:检验你有没有掌握程序最基本的输出语句,以及是否理解了“标准输出”这个概念。说得再直白点,就是看你知不知道怎么让程序往外“说话”。
很多没接触过算法竞赛的同学会问:这也太简单了吧?不就是复制粘贴一个字符串吗?如果只是复制粘贴,那确实不难。难的地方在于,你在自己电脑上跑出来的效果,和在评测系统里看到的判定结果,完全是两码事。本地终端可能容忍行尾多一个空格,评测系统则可能因为某个不可见字符直接给你判错。
1.2 真正要考的两项硬功夫
第一项硬功夫是“换行控制”。字符画是分行的,每一行结尾必须精确地输出换行符,不能多也不能少。用printf写\n,用cout写endl,都能达到目的。但如果你把某一行遗漏了换行,你会发现两行图形粘在一起,整个图案彻底变形。
第二项硬功夫是“复制粘贴的准确度”。说得难听一点,这道题就是一道“眼力题”。字符画中每一行的前面有多少个空格、中间是#还是*还是.,都必须和题目给定的完全一致。哪怕某一行开头少了一个空格,图案整体都会左移一位,提交结果就是 Wrong Answer。你如果用肉眼逐行比对,看半个小时未必能看出问题在哪;但评测机只需要一微妙就能检测出差错。
1.3 为什么这道题成了“新手劝退题”
有个特别有意思的现象:越是看起来没技术含量的题,AC率反而可能越低。原因很简单——新手的第一道题往往就卡在环境配置和输出细节上。有人IDE装好之后,写的第一个程序不是“Hello World”,而是一个复杂的字符画。他可能卡在很多莫名其妙的地方:编译报错、编码不对、本地运行正常但提交就编译失败、甚至有人把整个图形作为代码注释粘进去了。
这道题最能筛掉一种人:不读题、不检查、拿到样例就往代码里塞的人。等评测结果出来发现是Wrong Answer,你再回头仔细看,才意识到样例里的图形最后一行之后还有一个换行,或者某个长行的末尾藏着两个空格。所以我把这道题称作“新手劝退题”,不是因为它难,而是因为它用最简单的方式告诉所有刚入坑的人:在信息学竞赛里,程序的输出必须和标准答案“一个字节不差”。
2. 多种解法与选型思路
2.1 最无脑也最稳的方案:printf逐行输出
第一种方案,也是最直接的一种,就是用printf把图形的每一行单独打出来。你打开代码编辑器,从第一行开始,把字符画一行一行写进两个双引号之间。优点是完全贴合题目,哪一行有问题改哪一行,绝对不容易乱。
#include <cstdio> int main() { printf(" ********\n"); printf(" ************\n"); printf(" ####....#.\n"); printf(" #..###.....##....\n"); printf(" ###.......######\n"); printf(" ...........\n"); printf(" ###########\n"); printf(" ## ####\n"); printf(" ## ####\n"); printf(" ####\n"); printf(" ##\n"); return 0; }这里要注意,每个printf里的字符串末尾都带着\n,最后的return 0;别忘了写。有些评测系统不要求返回非零值,但养成写return 0;的习惯没有坏处。我在带新手的时候,从来都鼓励他们用方案一先跑通一次。原因很简单:第一版代码的容错率最重要,搞一堆花花肠子,一旦出错你根本不知道从哪查起。
2.2 用字符数组加循环,规范又省力
如果你已经学了数组和循环,完全可以换一种风格。把每一行看作一个字符串,存进一个字符串数组,然后用循环逐行输出。这种做法的优势在于,代码看起来更“程序化”,也更能体现你掌握了批量操作的思想。
#include <cstdio> int main() { char* lines[] = { " ********", " ************", " ####....#.", " #..###.....##....", " ###.......######", " ...........", " ###########", " ## ####", " ## ####", " ####", " ##" }; int n = sizeof(lines) / sizeof(lines[0]); for (int i = 0; i < n; i++) { printf("%s\n", lines[i]); } return 0; }本质上,两种写法输出的结果一模一样,区别只是代码的组织方式。在“超级玛丽”这道题上,二者没有性能差异,纯粹看个人习惯。我更推荐第二种,因为当你后面遇到“输出一张九九乘法表”或者“输出菱形图案”这类题时,循环加数组的思路能直接迁移过去。你可以把字符画理解成一张由字符串组成的表,每一行是表里的一行记录,循环就是遍历这张表。
2.3 另类思路:raw string一字不差输出
C++11以后提供了一种原始字符串字面量(raw string literal),用R"(...)"包裹内容,括号内的字符全部按原样输出,不需要转义任何东西。也就是说,你可以把整个字符画直接粘在代码里,连\n都不用写。
#include <cstdio> int main() { puts(R"( ******** ************ ####....#. #..###.....##.... ###.......###### ........... ########### ## #### ## #### #### ##)"); return 0; }这个方案极其适合“复制粘贴型”选手,因为图形是整块搬进来的,不容易因为某一行忘了写\n而出错。但有一个前提:你的编译器必须支持C++11标准。早年有些信息学竞赛环境还在用老旧的编译器,对R"(...)"支持不好。稳妥起见,如果你不确定评测环境的C++标准,还是用前两种方案吧。
3. 实操复现:从零到AC的完整过程
3.1 本地开发环境的准备
想把这题跑通,你至少需要一个C/C++编译环境。竞赛圈用得比较多的是在Windows下装一套集成开发环境,里面既带编译器又带编辑器。很多新手会卡在配置这一步,比如环境变量没配好,或者在IDE里点“运行”却提示找不到编译器。这里给一条最省事的路:如果你在Windows上,装一个Dev-CPP或者同为开源免费的工具,装完就能编译;如果你用Mac或Linux,系统自带的g++命令经常会帮上大忙。
我不建议新手上来的第一道题就折腾复杂的配置。先把环境弄到“能编译、能运行、能看到输出”的程度,就足够了。等以后遇到真正的工程型题目,再去研究调试器、断点、监视变量这些高级功能。这里有个小细节:在比赛评测系统里,很多都采用Linux环境,而你本地是Windows环境。Windows下写\n没问题,但有些Windows编辑器会在你敲回车时自动插入\r\n。如果你把代码文件保存后再提交,某些评测系统会自动处理换行符,但有些不会。为了避免踩坑,提交前一定要确认代码文件里没有混入莫名其妙的回车符。
3.2 从题目页面复制图形时的三个关键操作
第一步,把题目样例输出区域里的图形完整选中。这一步看起来毫无技术含量,实际上出错率极高。尤其当你用鼠标从第一行拉到最后一行的过程中,很容易把最后一行的换行漏掉,或者把某一行行首的空格选丢。我的建议是先把样例输出粘贴到记事本里,再随意选几行看看行首空格是否都在,然后再从记事本复制到代码编辑器。
第二步,注意行首空格的数量。有的图形第一行前面有16个空格,第二行前面有15个空格,每行都不一样。如果你在代码里手动敲这些空格,极容易数错。高手通常不会肉眼去数,而是把示例文本复制到代码里,让编辑器保留原始空格。这里分享一个实操小技巧:在大多数现代编辑器里,你可以选中一段含空格的文本,按Tab键会在行首统一加上一个制表符;按Shift+Tab则统一减少。你完全可以利用这个功能修正整体的缩进偏差。
第三步,确认图形里每个字符的类型。字符画里的.、#、*都不是乱码,也不是中文句号或中文井号。得检查你粘进去的是半角英文字符。很多输入法会在你输入中文时自动把标点替换成全角符号。全角字符和半角字符在视觉上区别很大,但在某些窄字体下看起来却很像。我就见过不少人,把代码里的#输成了中文“#”,编译都过不了,还一脸茫然。
3.3 提交前的三个自检步骤
本地运行通过不代表评测一定通过,提交前你要做三件事。
第一,打开一个干净的终端只运行你的程序,确认输出里没有多余的提示语。很多新手喜欢在代码里加一句“请输入数据”或者“这里是超级玛丽”,结果这些信息也被当成输出的一部分,直接导致判错。记住:评测系统只关心标准输出里的内容,其他一切多余字符串都是致命伤。
第二,检查文件保存格式。保存代码文件时,编码选择UTF-8通常最保险。有些老款IDE默认把文件存成GBK编码,一旦源码里有中文注释或中文符号,提交到Linux评测环境就可能编译报错。你拿不准的时候,干脆代码里不写任何中文,纯英文和数字最安全。
第三,手动数一下输出行数。字符画一共有多少行,你的程序就打印多少行,一个不多一个不少。本地跑完后,可以在命令行里执行./main | wc -l这样的命令统计行数,Windows下也可以在IDE里看输出框的行号。如果行数对不上,说明你的字符串数组里多了一行或漏了一行。
4. 常见问题与排查技巧实录
4.1 编译错误:到底哪里少了分号
新手遇到编译错误时,可以先看报错信息指向哪一行。printf这条语句,经常出现的问题是双引号没写全、末尾分号写成了中文分号、或者某个字母敲错。用自己敲代码的方式去逐字核对,效率很低。我建议你掌握一个技巧:把编译器报错信息里提示的那一行代码复制出来,和标准写法做逐字符对比。以printf为例,完整结构是printf("字符串");,先检查有没有(,再检查有没有"成对,最后看末尾有没有;。这就能解决一半的编译问题。
4.2 本地能跑,提交却是编译失败
这种情况十有八九是代码文件里混进了不可见字符,或者保存编码有问题。还有一种情况是,你把代码写在了某个私有目录下,提交时选了错误的文件。听起来很蠢,但我见过很多次。A同学做这道题的时候,本地创建了main.cpp,复制到评测系统的提交框里时,不小心把另一份写着“helloworld”的代码覆盖进去了。所以提交时一定要确认提交框里的代码和本地最终版本一致。
另一个比较隐蔽的原因,是你在代码里用了using namespace std;,但在评测系统里和某个内置名字冲突。正常情况下不会出问题,可一旦出了,你就要检查是不是把什么自定义函数名定义成了main以外的系统关键字。对这个题来说,最强的规避方式就是只用cstdio库,不引入多余头文件。
4.3 图形差一个字符,怎么找都发现不了
这是最让人抓狂的情况。图案看起来完全正确,但提交效果就是Wrong Answer。那时候你会觉得评测系统是不是坏了,甚至怀疑人生。实际上,问题多半出在“你看到的空格”和“实际输出的空格”不一致上。
第一,检查行尾空格。有些代码在复制图形时,每一行末尾都带了一个看不见的空格,而题目里该行末尾并没有空格。或者反过来:题目里该行末尾有空格,但你的代码复制时把末尾空格删了。第二,检查全角空格。某些输入法在中文状态下敲空格,会输出一个全角空格,它的ASCII码和普通空格完全不同,但肉眼几乎无法识别。第三,检查制表符。如果你复制的时候用了Tab缩进,代码里就可能在字符串中混入\t。它显示出来的宽度可能恰好让你觉得“对齐了”,实则字符完全不对。
破局的办法很简单:把题目给出的图形保存为文本文件,再把你程序输出的结果保存为另一个文本文件,用文本对比工具做逐行差异比对。没有工具的话,可以在命令行里用diff命令。它会非常明确地告诉你第几行第几个字符不一样。样例图形只有几行,完全手动比对也能搞定,但如果你养成了用diff的习惯,以后几百行的输出题再也不会慌。
4.4 换行符引发的悬案
Windows下写文件,每行结尾是回车加换行两个字符(\r\n),Linux下只有换行一个字符(\n)。如果你在Windows记事本里把图形复制到代码编辑器,可能每行末尾都带了一个不可见的\r,你打印时就会在每行多出一个回车符。这个回车符不会让图形“看上去”变化太多,但它会导致输出结果在二进制的层面和标准答案不一致。
规避方法也简单:第一,用专业的代码编辑器替代系统自带记事本,它们通常默认用LF换行;第二,如果你发现自己提交后提示Presentation Error或Wrong Answer,又确定字符都没错,优先排查换行符。很多评测系统会宽容地忽略行尾空格,但不会忽略行尾的回车符,所以这个问题必须重视。
4.5 常见错误速查表
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 编译报错 | 双引号、分号为全角字符 | 切换英文输入法重敲 |
| 编译报错 | 文件名或路径含中文 | 改名为纯英文保存 |
| 本地有输出,提交无输出 | 提交框选错代码文件 | 重新复制并对比 |
| Wrong Answer | 行首空格数量不对 | 用diff对比样例 |
| Wrong Answer | 行尾多余空格 | 检查每行末尾 |
| Wrong Answer | 输出多了一行提示语 | 删除所有多余printf |
| Presentation Error | 换行符不一致 | 检查文件换行格式 |
| 超时 | 不可能发生 | 检查是否写了死循环 |
5. 从“超级玛丽”延伸出来的刷题经验
5.1 做输出题也能积累算法思维
很多人觉得输出题对算法思维毫无帮助,我不同意。这道题让你理解了什么叫 “一次精确复现”。你写程序的过程,其实是把“题目里的一个静态数据”转换成“程序执行后的一个动态输出”。以后你做模拟题,本质上也是“复现”一个过程,只不过复现的对象从静态图形变成了动态变化。理解“程序的输出是确定的”这一点,对后续学习非常关键。
5.2 时间分配和调试策略
如果非要说这道题有什么难度,那唯一的分水岭就是细心。一个有效的时间分配策略是:把大部分时间放在“核对图形”上,而不是放在“敲代码”上。有人两分钟敲完代码,却在提交后反复错了十几次;我们反过来,花十分钟把图形核对清楚,一次提交通过,反而更省时间。
我自己当年还踩过一个坑:第一次提交的时候,因为图形里有一行的开头的空格比样例少了一个,整整排查了一个小时。后来我学会把所有有疑问的行单独打印出来,用肉眼盯着列号去数。现在我写这种带固定格式输出的题,早就不会傻乎乎地一行行人工比对了——直接上脚本或者利用编辑器的字符高亮,把空格、制表符、回车全部显示出来。很多编辑器都有“显示空格和制表符”的功能快捷键,先把这招学会,能让你在这类题上少走一大半弯路。
5.3 用这道题建立“一次通过”的信心
信息学竞赛中有一个说法叫“首杀”,也就是第一次提交就通过。能在简单题上达成一次通过,对新手建立信心特别重要。但这需要你在提交前就养成完整的自查流程:检查输出内容、检查是否有多余输出、检查字符类型、检查换行符。第四步,检查提交的文件是否正确。第五步,点击提交之后,再回想一遍代码逻辑。这套流程如果能在简单题上熟练,以后面对复杂题,你的出错率会大幅降低。
就我个人带人的经验而言,凡是能一次通过“超级玛丽”的同学,在后面写“字符串输出”类题目时,通常都会很稳。相反,那些在这道题上反复重交的同学,往往不是能力问题,而是性子太急,连样例都没复制对就去提交了。竞赛比的不只是你做不做得出,更比你在同等条件下谁更少失误。
最后再安利一个细节:如果你看到某个提交框旁边有“语言选择”的选项,一定要选和你本地编译器一致的语言标准。有很多评测系统默认用C++14或C++17编译,你就选和本地Dev-CPP对应的版本。没必要为了追求花哨而强行使用新的语法特性,稳定的写法才是竞赛场上的王道。这道超级玛丽虽然简单,但它是我见过最适合训练“输出洁癖”的题目——每一次通过,都是对你严谨程度的一次认可。