PAT乙级1002这道题,在OJ圈子里几乎算得上“梦开始的地方”。我记得自己刚接触PAT那会儿,第一次正经跑通的题就是它——题目短,需求直白,但就是这种看似简单的题,反而最能暴露基本功的窟窿。想写这篇东西其实有个很直接的起因:我前前后后帮不少刚入坑算法题的朋友看过代码,发现很多人在1002上不是不会做,而是栽在一些特别低级又特别隐蔽的细节上。所以这篇就围绕PAT乙级1002展开,既把标准解法讲透,也把容易丢分的地方掰开揉碎说清楚。不管你是刚开始刷PAT的新手,还是想帮别人讲题的老手,这内容应该都能让你有点收获。
1. 题目本身在说什么:一次拆完,别留疑问
1.1 原题要求只有三句话
PAT乙级1002,题目名是“写出这个数”。原题描述不复杂:读入一个正整数 n,计算它的各位数字之和,然后用汉语拼音写出这个和的每一位数字。
这里有几个关键信息要圈出来:
- 输入是一个正整数,不是负整数,也不是小数。
- 要求的不是输出这个数本身,而是先做一个“数位求和”,再对和做“拼音转换”。
- 输入上限是 n < 10^100,也就是最多100位十进制数。
- 输出的每个拼音之间要有一个空格,行末不能有多余空格。
举个例子:输入1234567890987654321123456789,先算数位和。这串数拆开加一遍,结果是135,那么输出就是yi san wu。注意是“三”的拼音san,不是“山”;是“五”的拼音wu,不是“无”。这种细节就是PAT判题系统最喜欢埋的点。
1.2 它真正在考察的四个基本功
别看题目简单,它其实同时考察了四个东西。
第一,输入输出格式的严谨性。PAT的评判对格式要求非常苛刻,最后多一个空格、少一个换行、拼音大小写写错,都可能直接判错。很多人算法思路没问题,却在格式上反复交学费。
第二,大数处理意识。看到“正整数 n”,第一反应是int,这确实是新手默认思路。但题目明确写了n < 10^100,这个量级根本不是C语言里任何整型能装下的,long long最大也就约9.2×10^18,差远了。所以必须换赛道,用字符串来接收输入。
第三,字符与数字的转换。用字符串读入之后,每一个字符都是'0'到'9'这样的ASCII字符,要参与加法运算就得先转换成对应的整数值。最常见也最标准的方式就是s[i] - '0'。
第四,数组映射和输出拼接。把0到9的拼音放进一个字符串数组,然后用和值的每一位数字去索引对应的拼音,这是一种非常基础的“查表”思路。后面做进制转换、做哈希映射的时候,这个思路会反复出现。
1.3 为什么说是“开胃菜”,却总有新手翻车
我在不同的学习群里看过不下二十个版本的1002代码。大部分人第一次都能写出来一个“看起来能跑”的版本,但真正提交上去就发现过不了。
翻车集中在几种情况:
- 用
int n; scanf("%d", &n);接收输入,一提交才发现运行错误或者答案错误。 - 把每一位数字直接相加时忘记减去
'0',得到的和莫名其妙地比预期大很多。 - 拼音表只写了9个元素,访问
pinyin[9]的时候越界或者输出为空。 - 最后多打了一个空格,被系统提示“格式错误”。
说实话,这些错误单独拎出来都不值一提,但它们恰恰说明一个问题:刷题的第一个门槛不是算法,是“准确理解题目到底要什么,并且严格按照要求输出”。搞定这一点,1002就是一道热身题;搞不定,后面每道题都会在同一个坑里摔一次。
2. 解题思路演进:从直觉方案到正确方案
2.1 第一反应为什么必然翻车
看到“算各位数字之和”,绝大多数人的第一反应是:
int n; scanf("%d", &n); int sum = 0; while (n > 0) { sum += n % 10; n /= 10; }这个思路本身没错,错在第一步。题目输入上限是10^100,也就是最多100位数字。C语言里int通常只有32位,最大才21亿多;long long虽然有64位,最大也就约922京(9.22×10^18)。100位十进制数远远超出这个范围。
可以直观感受一下:10^100这个数是1后面跟100个0,哪怕你用long long,也只能准确表示到10^18左右,高位数据全部丢失。一旦数据溢出,计算出来的数位和就错得离谱。
所以这道题的第一课就是:看到超大数,别往数值类型里装,老老实实用字符串。这不是什么高深的技巧,就是一个工程常识——数据的存储方式要跟数据本身的形态匹配。
2.2 字符串读入才是正解
正确的做法是把输入当成一串字符处理。C语言里用scanf("%s", str)读入,或者用C++的cin >> str,Python直接input(),Java用Scanner.next()。它们的共同点是:不关心输入数字有多大,只按字符逐个读入。
读进来之后,数位和的计算就变成了遍历字符串:
int sum = 0; int len = strlen(str); for (int i = 0; i < len; i++) { sum += str[i] - '0'; }这里有两个容易犯的毛病。第一个是把str[i] - '0'写成str[i]。如果输入的第一个字符是'1',它的ASCII码是49,你直接加进去,求和结果就会偏大48。第二个毛病是用strlen的时候忘了带头文件<string.h>,编译直接报隐式声明警告,虽然有些环境下还能跑,但属于定时炸弹。
至于数位和的存储类型,int完全够用。因为输入最多100位,每一位最大是9,和的最大值不会超过900,一个int装得绰绰有余。这也是题目设计里比较体贴的地方:输入很大,但中间结果很小。
2.3 和的每一位怎么拆,各有各的写法
数位和算出来之后,面临第二个问题:把和的每一位按从左到右的顺序输出拼音。
和的最大值是900,最多三位,所以方法很多。第一种是直接用sprintf把整数转成字符串:
char res[100]; sprintf(res, "%d", sum);然后遍历res,把每一位字符转换成数字,再去查拼音表。这个方案最直接,而且因为sprintf天然把整数转成高位在左的字符串,输出顺序不用额外处理。
第二种是“除10倒着存”。循环里用sum % 10依次取出个位、十位、百位,存进数组,最后逆序遍历数组输出。这个方案不依赖sprintf,但要多写几行,而且容易在逆序的时候把下标搞错。
第三种是递归输出,代码很优雅:
void print_sum(int sum) { if (sum / 10) { print_sum(sum / 10); } printf("%s", pinyin[sum % 10]); }递归进去再出来,天然保证了高位先输出。不过新手对递归还不熟的时候,不建议为了炫技用这种方式,等理解透彻了再用不迟。
我个人更推荐sprintf方案。理由很简单:可读性好,代码短,不容易在边界条件上犯错。刷题的首要目标是正确AC,不是写得最玄乎。
3. C语言标准实现,与那些必须知道的输出细节
3.1 一份可以直接AC的参考代码
下面这份是我很常用的C语言写法,干净、直白,适合作为模板:
#include <stdio.h> #include <string.h> int main() { char str[105]; scanf("%s", str); int sum = 0; int len = strlen(str); for (int i = 0; i < len; i++) { sum += str[i] - '0'; } char *pinyin[] = {"ling", "yi", "er", "san", "si", "wu", "liu", "qi", "ba", "jiu"}; char res[100]; sprintf(res, "%d", sum); int res_len = strlen(res); for (int i = 0; i < res_len; i++) { if (i != 0) { printf(" "); } printf("%s", pinyin[res[i] - '0']); } printf("\n"); return 0; }这里我开了char str[105]而不是char str[101],纯粹是习惯性多留一点余量。题目说输入不超过100位,加上字符串结尾的'\0',最低要求是101个字符,开105不会错。
char *pinyin[]这种写法表示一个字符串数组,每个元素指向一个字符串常量。索引从0到9,刚好对应数字0到9的拼音。注意不要写成char pinyin[],那是二维字符数组的另一种声明,容易跟一维数组混淆。
3.2 输出格式的魔鬼细节
PAT对输出格式的检查非常细,我见过太多人死在空格上。看输出部分:
for (int i = 0; i < res_len; i++) { if (i != 0) { printf(" "); } printf("%s", pinyin[res[i] - '0']); }这个写法的逻辑是:第一个拼音前面不输出空格,从第二个开始,每个拼音前面补一个空格。这样出来的结果就是yi san wu,而不是yi san wu(末尾多空格),也不是yisanwu。
反面写法是:
for (int i = 0; i < res_len; i++) { printf("%s ", pinyin[res[i] - '0']); }这个会在最后一个拼音后面也带一个空格。如果PAT判题系统对行末空格做精确比对,这就是一个“格式错误”或“答案错误”。
另一个细节是换行。大部分PAT题目要求输出最后带一个换行符,所以我在最后加了printf("\n")。有的同学觉得不带换行也能过,确实有些题目的判题逻辑不卡这个,但不是所有题都这样。与其赌运气,不如每条输出都规规矩矩带换行。
3.3 关于“和值为0”这种边界情况
PAT的输入明确是正整数,所以理论上数位和最小也是1,不会出现0这种情况。比如输入1000,数位和是1,输出yi。但如果题目改成了“自然数”,允许输入0,那数位和就是0,应该输出ling。
在我上面这份参考代码里,sprintf(res, "%d", 0)得到的是字符串"0",循环遍历时res[0] - '0'恰好是0,查表得ling。也就是说,这段代码天然兼容了sum == 0的情况,不用额外加if判断。这个设计纯属顺手,但确实省心。
当然,如果要追求绝对的严谨,可以在计算完后加一句:
if (sum == 0) { printf("ling\n"); return 0; }两种写法都行,看个人习惯。我平时倾向于让代码自然包含边界,避免为不存在的输入增加分支。
3.4 实测感受与编译注意事项
我用这份代码在PAT系统提交过,编译选项是标准自带,直接AC,运行时间几乎为0毫秒,内存占用几百KB。就这道题的规模来说,性能和内存根本不构成任何压力,别担心。
需要注意几个编译级别的问题:
- 一定要
#include <string.h>,否则strlen没有声明。 sprintf需要<stdio.h>,这个一般不会漏。- 如果用的是较新编译器,
scanf可能建议改成scanf_s,但PAT的OJ环境是Linux下的GCC,直接scanf没问题。
还有,不要在代码里用gets(str)来读输入。gets不检查缓冲区边界,早就被移出标准了。而且读入的内容可能带换行,处理起来反而麻烦。scanf("%s", str)会跳过空白字符,输入又是单个数字串,用它最干净。
4. 换个语言再看一遍,Python、Java和C++的写法差异
4.1 Python:三行代码,但有一个反直觉的坑
如果允许用Python刷PAT乙级,1002的解法可以写得非常短:
s = input() total = str(sum(int(c) for c in s)) pinyin = ["ling", "yi", "er", "san", "si", "wu", "liu", "qi", "ba", "jiu"] print(" ".join(pinyin[int(ch)] for ch in total))核心逻辑就三行。int(c) for c in s把输入的每个字符转成整数,sum()求和,再str()转回字符串,最后逐位查表并用空格拼接。
但这里面有个反直觉的地方:如果直接用input()读,假设输入是"0"(虽然PAT不给这种数据),sum(int(c) for c in s)是0,str(0)是"0",输出ling,没问题。真正要注意的是Python的int不限制大小,你其实可以不转字符串直接求,但那样做没有任何好处,反而让代码绕了一道。
另外,Python里input()一次读一行,如果OJ的测试数据文件末尾有额外换行,input()会自动去掉,不用额外strip()。但如果你用了sys.stdin.read()读整个文件,就得小心末尾换行被当成输入的一部分。
4.2 Java:用String接收,再用charArray遍历
我第一次用Java写这题时,第一反应是BigInteger,后来想想纯属多此一举。因为既然只是数位求和,根本不需要把输入变成数值,直接按字符串处理就行。
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); String n = sc.next(); int sum = 0; for (int i = 0; i < n.length(); i++) { sum += n.charAt(i) - '0'; } String[] pinyin = {"ling", "yi", "er", "san", "si", "wu", "liu", "qi", "ba", "jiu"}; String total = String.valueOf(sum); StringBuilder sb = new StringBuilder(); for (int i = 0; i < total.length(); i++) { if (i > 0) { sb.append(" "); } sb.append(pinyin[total.charAt(i) - '0']); } System.out.println(sb.toString()); } }这里的坑在于String.charAt(i)返回的是char,直接参与加法时要记得- '0'。另外用StringBuilder拼接字符串比+连接更高效,虽然在1002这种小规模问题上差距可以忽略,但养成好习惯后面处理长字符串时会受益。
Java类名必须是Main,不然OJ找不到入口。这个忘了我第一次也犯了,很冤枉。
4.3 C++:string和数组的结合最顺手
C++的写法其实和C很像,但std::string让长度获取和字符遍历更自然:
#include <iostream> #include <string> using namespace std; int main() { string s; cin >> s; int sum = 0; for (char c : s) { sum += c - '0'; } string pinyin[] = {"ling", "yi", "er", "san", "si", "wu", "liu", "qi", "ba", "jiu"}; string total = to_string(sum); for (int i = 0; i < total.size(); i++) { if (i != 0) { cout << " "; } cout << pinyin[total[i] - '0']; } cout << endl; return 0; }C++11之后的范围for让字符遍历变得很舒服。这里唯一要注意的是total[i]返回的是char,和Java一样要减去'0'再当下标用。
我自己的体会是:这道题用C语言写最能理解底层细节,用Python写最省事,用C++写最均衡。如果你还在学习阶段,建议至少把C语言的版本完整手敲一遍,别一上来就用Python带过,否则会错过字符串和字符转换这个关键基本功。
5. 从1002说开去:PAT乙级刷题路线与常见教训
5.1 通过这道题之后,我建议的刷题顺序
1002在PAT乙级里的位置相当于“体检项目”,它帮你在入门阶段确认自己有没有方向性的大问题。通过之后,可以按照下面这个顺序继续:
| 题号 | 题目要点 | 为什么推荐放在前面 |
|---|---|---|
| 1001 | (3n+1)猜想 | 循环和条件判断的入门组合 |
| 1008 | 数组元素循环右移 | 理解数组下标运算和取模 |
| 1011 | A+B 和 C | 基础输入输出,注意数据范围 |
| 1016 | 部分A+B | 字符处理和数位拼接 |
| 1037 | 在霍格沃茨找零钱 | 进制换算思想,趣味性强 |
不要一上来就挑战高分难题。乙级题目的特点是“考基本功不考偏题”,把上面这些题刷完,你基本上就把循环、数组、字符串、格式化输出这些核心能力盘活了。之后再往上走,去看甲级的时候才会轻松一些。
5.2 PAT乙级的“十年老坑”清单
刷题数量上来之后,很多错误其实是重复的。我总结了一张高频踩坑清单:
| 错误类型 | 典型表现 | 正确做法 |
|---|---|---|
| 数据范围判断错误 | 用int读大数,溢出后答案错 | 见“超大数”就考虑字符串 |
| 字符忘记转数字 | sum += s[i],结果偏大 | 用s[i] - '0' |
| 拼音表越界 | pinyin[sum],sum超过9 | 先转成字符串逐位处理 |
| 输出末尾带空格 | 每个拼音后都加空格 | 第一个前面不加,之后补空格 |
| 域名拼写错误 | “si”写成“shi”,“ba”写成“pa” | 多读题示例,提交前自查 |
| 类名/功能名问题 | Java类名不是Main | 按OJ要求命名 |
这些坑没有一个是“算法不会”,全是“审题不细”或“基础不稳”。但它们造成的扣分比想象中严重,因为PAT判题是零容忍的,一个字符不符就是整个测试点不得分。
5.3 比代码更重要的能力:读题和自测
我知道很多人刷题的习惯是看到题目就上手写代码,恨不得三分钟提交。但1002这种题最能教育我们:读题两分钟,胜过改码两小时。
拿到题之后,先做三件事:
- 圈出输入范围。看到10^100,第一时间想到字符串。
- 圈出输出格式。看到“拼音之间空格分隔”,第一时间想到循环拼接规则。
- 手算一遍示例。把输入的示例自己算一遍,确认和输出对得上,再开始写代码。
这三件事看着不起眼,却能避开至少80%的基础错误。我后来养成的习惯是:写完代码之后,先把题目给出的示例跑一遍,再自己构造两组边界数据——一组是最大长度输入,一组是可能导致和值最大的输入(比如100个9)。这两组数据如果都能正确输出,这道题基本就稳了。
6. 个人操作中的一点体会
最后聊点我自己的经验吧。实话实说,1002不是一道能让人产生“我很强”错觉的题,它更像是算法路上的一把尺子:量一量你对输入输出、数据类型、字符处理这些基本功掌握到什么程度。
我见过有的同学在这道题上反复提交七八次,每次都被不同的小细节挡住,然后很沮丧地来问我是不是不适合写代码。其实完全不是。PAT这种判题系统就是很“死板”,它不会因为你思路对就手下留情,任何格式问题都是错误。但这恰恰是好事——早早在入门题上被教做人,总比到了职场交付项目时被线上Bug折磨要强。
如果你现在正在1002上挣扎,我的建议是:不要急着看别人的满分代码,先把你自己写的版本跑一遍,一个测试点一个测试点地看错误输出,找到问题所在。改完再提交,再发现问题再改。这个过程本身,比最终那个AC的绿色结果值钱得多。