1. 先搞清楚这两个题到底在考什么
刷蓝桥训练题时,把“Anagrams问题”和“输出正反三角形”放到一起,乍一看有点莫名其妙:一个是字符串判断题,一个是图案输出题,八竿子打不着。但真正动手做一遍就会发现,这两个题其实是同一个核心能力的两个侧面——把现实问题拆成可计算的规律,再用代码精确地表达出来。这种能力,说白了就是算法入门最重要的基本功。
先说Anagrams问题。Anagram这个词来自英文,意思是“由相同字母重新排列形成的词”,比如“listen”和“silent”,字母集合完全一样,只是顺序不同。给定两个字符串,让你判断它们是不是这种关系,这就是题目核心。蓝桥这道题还有一个更贴近实际的小设定:比较时忽略大小写,也忽略空格。也就是说“Hello World”和“world hello”在题目逻辑里会被当作一组Anagrams。这个细节很关键,因为它直接决定了你的解题方向——不是简单判断字符串是否相等,而是要比较“字母的多重集合”是否一致。
“多重集合”这个概念听起来有点学术,其实很好懂。你想象两副扑克牌,把牌全倒出来摊在桌上,不去管原来的排列顺序,只问一副牌里的每种花色点数是否和另一副完全一样。这就是Anagrams问题的本质:不看顺序,只看“每个字母出现了多少次”。如果两串字符里每个字母的出现次数都相同,那它们就是Anagrams。
输出正反三角形这道题就直白多了。输入一个正整数n,输出由星号组成的三角形图案,正着来一遍再倒着来一遍。看起来只是打印几个星号,但很多人第一次写的时候反而栽跟头——不是多了一个空格,就是少了一行,或者行首缩进对不上。原因很简单:这题本质上考验的是“把每个位置上的字符数与行号挂钩”的能力。你需要在动手写代码之前,先在草稿纸上把第1行有几个空格、几个星号,第2行又有几个空格、几个星号,全部总结成数学公式。公式对了,代码只是翻译问题。
两个题放到同一份训练计划里其实挺合理。Anagrams考的是“看不见的规律”,你要从题目描述里抽取出字母计数这个逻辑核心;三角形考的是“看得见的规律”,形状就摆在那里,但你要把形状数字化、公式化,还得注意输出格式这种竞赛中最容易扣分的细节。无论哪个,都是把模糊需求变成严格逻辑的完整练习。
我自己带过不少刚开始刷题的朋友,一个明显的共性是:很多人不是不会写代码,而是不会“读题”。看到Anagrams直接上来比较字符串是否相等,忽略大小写和空格的要求完全没进脑子;看到三角形不知道先列出每行的空格数和星号数公式,一上来就手写for循环,写着写着下标就乱了。这篇文章就把这两个题从读题到实现完整拆开,把我试过的方法、踩过的坑、蓝桥系统判分时容易忽略的细节全部梳理一遍。
2. Anagrams问题的完整拆解
2.1 题面细节与隐藏要求
蓝桥训练里的Anagrams问题,原题通常会这样描述:输入两个字符串,判断它们是否为Anagrams,如果是输出Y,否则输出N。重点在于,题目明确写了“只考虑字母,忽略大小写和空格”。
这句话至少包含三层意思。第一层,空格要被剔除。意味着你在读取字符串后,得先把所有空格符号剔除掉,剩下的非空格字符才参与比较。第二层,大小写不区分。A和a算同一个字母,这要求处理时对字母做统一转换,要么全部转大写,要么全部转小写。第三层,只考虑字母。这句话有两种理解,一种是空格被忽略,其余字符都参与比较;另一种是除了字母以外所有字符都不参与比较。蓝桥原题多数采用的是“空格忽略但不强制剔除其他符号”的口径,稳妥的做法是在预处理时直接只保留字母,其他字符一律跳过。这样无论如何解析都是安全的。
另一个容易忽略的点是输入形式。题面说的是“输入两个字符串”,但这两个字符串可能都包含空格,所以不能简单地用scanf("%s")去读。用C语言做这道题,得用gets或者fgets整行读取,用Java就用BufferedReader读整行。很多人在本地测试时没问题,是因为测试数据恰好没有空格,一提交到蓝桥评测系统就出现答案错误,十有八九是读取方式的问题。
题目还常常有多组输入的要求,不是读一组判断一组就结束,而是要循环处理直到文件结束。蓝桥的训练数据有时会刻意塞多组输入来测试代码的健壮性,只处理一组数据的代码提交上去只能拿到部分分数。我建议一开始就把代码写成循环读取,每组输入独立处理,输出独立换行,这样无论评测数据里有几组,代码都能正确应对。
2.2 三种解法的原理对比
处理Anagrams判断,主流方案有三种:排序后比较、哈希计数、三计数器差减法。三种思路各有特点,适用场景也不同,我分别说一下。
第一种,排序后比较。把两个字符串分别排序,如果排序后完全相同,说明它们字母构成一致。C语言里可以用qsort,C++可以直接sort,Java可以先把字符串转成字符数组再排序。这种做法代码量小,逻辑清晰,缺点是时间复杂度是O(n log n),字符串很长时性能不如计数法。更重要的是,对于本题有“忽略空格和大小写”的要求,你还得先做预处理:转大小写、去空格。如果忘了过滤空格,两个明明互为Anagrams的句子排序后不相等,直接判断错误。
第二种,计数数组法。开两个长度为26的数组,遍历两个字符串,遇到字母就统一转成小写或大写,然后在对应位置计数。遍历结束后,逐个比较两个数组的每一位,完全相同就是Anagrams。这种做法的时间复杂度是O(n),代码同样很简短,而且天然处理了字母大小写问题,只需要额外跳过空格。题目里明确说明忽略空格,所以遇到空格时continue掉就行。
第三种,三计数器差减法。这种做法的思路很巧妙:只开一个长度为26的计数数组,遍历第一个字符串时对应位置加1,遍历第二个字符串时对应位置减1,最后看数组里是不是每一位都归零。一次遍历串1加计数,再遍历串2减计数,省去了一个数组的开销,代码也简洁不少。但需要注意边界情况:如果两个字符串长度悬殊,理论上早就该提前退出以节省时间,不过蓝桥这道题的数据规模通常不大,这个优化提不提都行。
从蓝桥训练题的实际数据规模来看,n是百万级别的可能性不大,所以三种方案在时间上都不会有问题。真正选哪种,取决于你写代码时的舒适度。我个人的建议是:入门阶段优先掌握计数数组法,因为它的思路最直观,从“多重集合相等”这个本质出发,一个数组就能讲清楚原理。排序法是考场上快速写出可用代码的备选方案,适合时间紧、没太多时间思考的情况。三计数器差减法在初学阶段容易想不明白“为什么遍历第二个串时要减”,一旦理解后反而最省事。
2.3 为什么我最终选择了计数数组方案
Anagrams的判断核心是字母多集相等,计数数组法在这个语义下是最匹配的。你用两个数组分别记录两个字符串的字母构成,然后逐位比较,每一步的逻辑都和题目要表达的意思一一对应,出错了也容易排查。排序法虽然代码短,但它把问题从“字母集合相等”变成了“排序后字符串相等”,语义上隔了一层。一旦题目要求过滤空格或者只考虑字母,排序法的预处理反而更容易出错。
我举个例子你就明白了。假设输入是“listen”和“silent”,排序法能得到两个完全一致的字符串,没问题。但输入是“hello world”和“world hello”时,两个串都含空格,排序前排不排空格直接影响结果。如果你只排了字母序列,那么排序后仍然一致;如果连空格一起排进去,排序后不一致,题目本来该输出Y的,你的代码输出N,直接判错。计数数组法因为处理流程中“遇空格就跳过”是显式写在代码里的,天然避免了这种坑。
还有一点是输出格式的坑。蓝桥的Anagrams问题要求输出单个字符Y或N,后面跟换行。有些同学在循环处理多组数据时,会在每组输出后多打印一个空行,这在本地看不出问题,但评测系统是按字节比对输出结果的,多一个空格多一个空行都会导致Presentation Error。后面我会专门用一个章节讲这类格式问题,这里先提个醒:输出Y或N时,字符串里不要带任何多余字符,换行用printf("\n")就好。
3. 输出正反三角形的规律与实现
3.1 用行号把所有数量关系表达出来
图案输出的题,核心永远是找规律。所谓“规律”,就是每一行内容与行号之间的数学关系。输出正反三角形,先只看正三角形那部分。以n=5为例,图案长这样:
* *** ***** ******* *********第一行1个星号,第二行3个,第三行5个,第i行的星号数是2i-1。空格数是n-i,用来实现居中对齐。这个规律其实很好验证:第一行n-1等于4个空格,第二行n-2等于3个空格,依次递减。一旦把“第i行 = (n-i)个空格 + (2i-1)个星号”这个公式写出来,代码就是两分钟的事情。
反三角形就是正三角形的镜像。继续以n=5为例,图案长这样:
********* ******* ***** *** *第i行(这里从上往下数)的空格数是i-1,星号数是2(n-i)+1。也可以换一种写法:让行号从n递减到1,空格数用n-i计算,星号数还是2i-1。两种写法本质相同,只是循环变量的方向不同。
有人看到这里会问:这种题有什么好讲的,不就是套公式吗?但实际写的时候,很多人会遇到一个很奇怪的问题——明明公式是对的,输出却不对。我在旁边帮人调试时就见过好几次,原因是循环里多写了一个空格。比如有人写成“先打印一个空格,再打印n-i个空格”,导致第一行有两个前导空格,整个图案往右偏移了一格。这种错在本地屏幕上看起来不严重,但蓝桥的判分是按字节比对,多一个空格就是错。
我建议初学者先用一张表把规律固化下来。以n=5的正三角为例,可以列出下表:
| 行号i | 空格数(n-i) | 星号数(2i-1) |
|---|---|---|
| 1 | 4 | 1 |
| 2 | 3 | 3 |
| 3 | 2 | 5 |
| 4 | 1 | 7 |
| 5 | 0 | 9 |
对照这张表写循环,就是三个循环:外层循环管行号,内层第一个循环打印空格,第二个循环打印星号,最后打印换行。逻辑上完全不会有歧义。写完正三角,反三角的表格同理,然后把两个图案拼在一起,就可以输出一个完整沙漏。
3.2 沙漏、菱形与题目变种
前文说的正反三角形,有时题目会要求连续输出,正三角在上、反三角在下,中间不空行,组合起来像个沙漏。实现方式就是在同一个外层循环里按两段处理。更常见的变体是输出菱形,本质就是把正三角形和反三角形拼接,中间一行共用,不重复输出。理解了三角形公式后,菱形无非是把反三角的起始行号改一下,或者单独处理中间行。
还有一类变种是数字三角形。比如输出对称数字三角,行的内容从1递增到2i-1再递减回1。公式思路完全一样,区别只在于每行的内容从星号变成了数字。这种题在蓝桥里出现频率不低,核心就是“内容由行号决定”这个思想。
我在实际练习中总结了一条经验:图案题的变体再多,最终都可以归约到“每行内容 = f(行号)”。只要你能画出样例图案,在旁边标出第1行、第2行、第3行分别有几个空格几个字符,规律就自动浮现了。不要凭感觉写,不要边写边试,先把表格列出来。
3.3 输出格式的严格要求
图案题的得分除了规律找得对不对,很大程度上取决于格式是否精确。蓝桥评测系统把程序输出的全部字节与标准答案比对,多一个空格、多一个空行、行首少几个空格、末尾多一个换行,都可能被判为错误。这类错误通常以Presentation Error的形式报出来,意思是“输出结果不对,或者格式有问题”。
应对方式只有一种:严格按照样例推导输出格式。题目样例给出的图案是什么样的,你的程序就必须输出成什么样。一般来说有这么几条硬性规则:行首空格必须对齐;每行星号打印完后直接换行,行尾不要再补空格;两个三角形之间是否需要空行,以样例为准。很多人在本地运行时,终端会把行尾空格显示得很不明显,到了评测系统就出问题。稳妥的做法是写完代码后用一个小技巧自查:把输出重定向到文件里,用十六进制查看文件内容,多出来的空格立刻现形。这个技巧我在后面“本地自测”部分会细说。
4. 完整可运行的参考代码
4.1 Anagrams问题C语言实现
下面给出一份完整的C语言实现,它支持多组输入,读取带空格的整行字符串,只统计字母并忽略大小写,最后逐组输出Y或N。
#include <stdio.h> #include <string.h> #include <ctype.h> #define MAXLEN 1005 void countLetters(const char *s, int cnt[]) { for (int i = 0; s[i] != '\0'; i++) { if (isalpha(s[i])) { cnt[tolower(s[i]) - 'a']++; } } } int main() { char s1[MAXLEN], s2[MAXLEN]; while (fgets(s1, MAXLEN, stdin)) { if (!fgets(s2, MAXLEN, stdin)) { break; } // 去掉fgets可能读进来的换行符 s1[strcspn(s1, "\n")] = '\0'; s2[strcspn(s2, "\n")] = '\0'; int cnt1[26] = {0}; int cnt2[26] = {0}; countLetters(s1, cnt1); countLetters(s2, cnt2); int flag = 1; for (int i = 0; i < 26; i++) { if (cnt1[i] != cnt2[i]) { flag = 0; break; } } printf("%c\n", flag ? 'Y' : 'N'); } return 0; }代码思路很直白。fgets逐行读取,因为字符串里可能含空格,scanf的%s没法用。fgets会把换行符也读进来,所以用strcspn找到换行符位置并替换成字符串结束符。countLetters函数遍历字符串,遇到字母就统一转换成小写,在对应下标里计数。最后比一比两个计数数组,全等就是Y。
这里有一个细节值得提醒:countLetters用isalpha判断字符是否为字母,然后用tolower转成小写,这样A和a落在同一个计数位置。如果你不放心编译环境的字符集,这种写法是基于标准C库的,在蓝桥的GCC环境里没问题。还有一种省事的写法是直接把两个串里的字母统一转成大写再计数,效果一样。
4.2 正反三角形图案输出C实现
黄金搭档第二题,完整实现如下。假设输入n,输出由星号组成的正三角形在上、反三角形在下的组合。
#include <stdio.h> int main() { int n; while (scanf("%d", &n) != EOF) { // 正三角形:第i行有n-i个空格,2i-1个星号 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n - i; j++) { printf(" "); } for (int j = 1; j <= 2 * i - 1; j++) { printf("*"); } printf("\n"); } // 反三角形:第i行(从1到n)有i-1个空格,2(n-i)+1个星号 for (int i = 1; i <= n; i++) { for (int j = 1; j <= i - 1; j++) { printf(" "); } for (int j = 1; j <= 2 * (n - i) + 1; j++) { printf("*"); } printf("\n"); } } return 0; }注意外层循环的行号从1开始,这是我刻意保持的写法。很多教材喜欢从0开始,但0-based写法的公式容易带出n-1这种偏移量,初学者经常绕晕。1-based的写法虽然内层循环的终止条件会多出一些-1或+1,但行号和自然语言里的“第几行”完全对应,更好排查问题。
输出正反三角形之后,如果需要输出菱形,只需把反三角形的第一行去掉,因为正三角形的最后一行和反三角形的第一行中间行重合。我在实际刷题过程中发现,同一个公式稍微调整循环边界就能适配多个变体,所以掌握1-based写法后多练几个变体,收益很高。
4.3 Java环境下的快速实现对比
蓝桥的Java组同样会考这类题,思路和C一致,但代码风格上有几个不同点。Anagrams问题的Java实现可以这样写:
import java.io.*; import java.util.*; public class Main { public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String s1; while ((s1 = br.readLine()) != null) { String s2 = br.readLine(); if (s2 == null) { break; } int[] cnt = new int[26]; countLetters(s1, cnt, 1); countLetters(s2, cnt, -1); boolean ok = true; for (int c : cnt) { if (c != 0) { ok = false; break; } } System.out.println(ok ? "Y" : "N"); } } private static void countLetters(String s, int[] cnt, int delta) { for (char ch : s.toCharArray()) { if (Character.isLetter(ch)) { cnt[Character.toLowerCase(ch) - 'a'] += delta; } } } }这里直接用了三计数器差减法,一个cnt数组就完成了两个串的统计。读串用readLine,天然处理了带空格的问题。Character.isLetter和Character.toLowerCase对应C里的isalpha和tolower。
三角形的Java实现同样简单,用System.out.print逐字符输出即可。不过Java的输出效率比C的printf慢,如果n很大、图案行数很多,建议用StringBuilder把整个图案拼接完再一次性输出。蓝桥训练题的n通常不大,直接print问题也不大,但养成用StringBuilder的习惯对后续解更大规模的题有好处。
4.4 本地自测的小技巧
本地写代码时,肉眼观察输出是否正确往往是不可靠的。尤其是图案题,行尾的空格在多数编辑器和终端里看不出来,但评测系统能看出。我推荐一个自测方法:把程序输出重定向到文件,然后用带十六进制视图的编辑器查看,或者用命令行工具检查。
Linux和Mac下可以用cat -A查看文件的每一行,行尾的换行符显示为$,空格显示为普通空格,一眼就能看出有没有多余空格。Windows下可以用记事本打开文件,打开“显示所有字符”功能。如果你用的是VS Code,装一个“Hexdump”插件,直接看字节内容是终极方法。
对于Anagrams这种判断类题目,更实用的自测方式是构造边界用例。比如“A”和“a”应该输出Y;“hello world”和“world hello”应该输出Y;“abc”和“abd”应该输出N;两组数据连续输入时,输出也应该连续两行“Y/N”,中间不能有多余空行。把这些用例提前准备好,每改一次代码就全跑一遍,能省去大量反复提交评测的时间。
5. 实战中容易踩的坑
5.1 输入读取的坑
Anagrams问题第一个坑就是我们前面反复提到的“含空格字符串的读取”。有些同学习惯用scanf("%s"),本地测试时输入恰好没有空格,样例过了就提交,结果在评测系统上WA(Wrong Answer),怎么查都查不出问题。原因是评测数据里确实有带空格的字符串,scanf按空白符分割输入,读到的只是第一个单词,后面的内容全被当作下一组数据的开头,整个判断逻辑全乱。
C语言里读取带空格的单行字符串,传统做法是gets,但这个函数在新版C标准里已经标记为过时,存在缓冲区溢出风险,蓝桥官方环境虽然一般还是能编译通过,但不推荐。fgets是更规范的选择,注意它会保留换行符,需要手动清掉。还有一种做法是用scanf("%[^\n]")这样的格式说明符读取到换行前,加上换行匹配符处理行尾,但可读性不如fgets,而且容易留下尾换行符没消费的问题。我调试多组输入时折腾过很久,最后稳定下来的方案就是fgets加strcspn清理换行符。
Java里用BufferedReader.readLine()读取整行,天然安全。唯一要注意的是多组输入时第一行可能为空字符串,需要处理一下空串的情况。不过竞赛数据一般不会故意给空串,遇到这种情况直接当成普通字符串处理即可。
5.2 演示错误与格式化输出
蓝桥的旧版评测系统里有种特殊的返回结果叫Presentation Error,简称PE,含义是“输出格式有问题”。它通常出现在两种情况:输出内容本身正确但多加了空格或空行,或者行尾多打了空格。PE和WA在很多评测系统里会分开标识,蓝桥早期版本会区分。现代系统大多统一归为WA,但为了稳妥,还是一开始就别格式出错。
我见过最典型的PE案例是图案题:程序在每行星号结束后多打了一个空格。在本地终端看,行尾空格几乎无感知,提交后却不能得分。排查方法就是把输出重定向到文件,对照样例逐字节检查。
还有一类问题出在换行符上。Windows环境下的换行是\r\n,蓝桥评测环境是Linux,只认\n。如果在Windows下用记事本编辑代码,代码文件本身的换行符可能被保存成\r\n,提交到Linux上编译运行时,printf输出的换行通常是\n,问题不大。但fgets在Windows下读取文件时会把\r\n都读进来,这时strcspn只清掉\n,\r残留在字符串末尾,会被当成普通字符参与统计,导致判断错误。解决办法是清理换行符时同时清掉\r:先看字符串末尾是不是\r,是就置为\0,再看是不是\n,一并清掉。这个细节在Windows本地调试、提交到Linux评测时非常关键,我在实战中踩过一次,排查了很久。
5.3 边界与特殊输入
Anagrams问题的边界情况主要围绕空串和单字符。如果两个字符串都是空串或者只含空格,理论上它们是Anagrams吗?按题目“只考虑字母,忽略空格”的规则,两者都退化成空集合,应该输出Y。计数数组全为0,比较时全相等,自然输出Y,代码不用特判。如果只含字母但数量不同,例如“a”和“aa”,计数数组一位不相等,输出N。这些逻辑在计数数组法下都是自动正确的。
三角形题的边界就是n等于1和n等于2。n等于1时,正三角一行一个星号,反三角也是一行一个星号,组合起来两行。n等于2时图案很小,但如果公式写错很容易发现。还有n很大的情况,只要循环边界正确,输出长度线性增长,不会有溢出问题。唯一要注意的是外层循环和外层循环不要套错。
5.4 排查问题时的思路顺序
当代码提交后报错,我不建议盲目改代码再试。先按这个顺序排查:第一步检查输入输出方式是否符合题面要求,尤其是字符串含空格时读取方式对不对;第二步检查题目要求是否全部落实,比如是否忽略大小写、是否忽略空格、是否多组输入循环;第三步用边界用例本地验证,看看输出逻辑是否有系统性偏差;第四步检查格式细节,重点看行尾空格和空行。把每一步都排除后再去读代码逻辑,往往很快就能定位问题。很多时候错误出在读题不仔细,而代码本身逻辑没问题。
6. 从这两个题延伸出去的经验
Anagrams问题背后的“多重集合相等”思想,在竞赛中会反复出现。比如字符串重组匹配、判断句子是否由相同单词构成、哈希统计后比较等。计数数组这个工具也不只用来处理字母,处理数字范围固定的频次统计问题时完全通用。你一旦掌握“用一个数组记录频次差异”的思路,后面遇到很多统计类问题都会豁然开朗。
输出正反三角形这类图案题,看起来偏门,但它是二维数组、模拟、规律题的入门标尺。蓝桥后续的“回形取数”“蛇形填数”等二维数组模拟题,思路本质都是“用坐标变化描述图案规律”,和三角形题一脉相承。把三角形输出练到闭着眼睛能写的熟练度,处理更复杂的坐标变换时心里就有底。
给刚起步的同学一个刷题路径建议:拿到一道题,先在纸上手动模拟样例,把每一步的变化写下来;然后尝试用数学公式描述这种变化;最后才动手写代码。这样的流程刚开始会偏慢,做十道题之后就明显感觉到读题和建模的速度上来了。我在教朋友刷题时发现,凡是坚持这个流程的,一两个星期后独立解题能力都会有明显提升。
最后说一个我个人的体会:刷题时不要怕用最笨的方法解出题目,重要的是把解题过程完整走一遍,再回头看能不能优化。Anagrams问题我第一次用的是排序法,单组数据能过,多组输入时才发现循环结构没处理好,后来改成计数数组法才顺滑。三角形的题我第一次写直接把循环边界搞混了,最后画了个表格才理清思路。这些初次的“笨”,反而是理解题目本质最好的老师。