我接触洛谷P1765“手机”这道题,是在给新手班带练的时候。当时很多同学刷到这道题,第一反应都是“这不就是个模拟吗”,结果一提交就掉进各种坑里:有人读不进来带空格的字符串,有人把大写字母当成普通字符处理,还有人忘了空格本身也算一次按键。这道题表面上是入门难度,实际上把字符串处理、查表法、边界条件这些基本功全串起来了。对于刚接触算法竞赛的同学来说,这是一道性价比极高的练习题,既能巩固基础语法,又能提前感受“题目读不懂比代码写不出更致命”的竞赛常态。
只要你做过几道洛谷的入门题,就会发现P1765这类“规则模拟题”在竞赛里非常常见。它不会考你高深的算法,但会给你一套实际生活中的规则,让你用编程语言老老实实地复现一遍。这类题做得好不好,往往不取决于你会多少算法,而取决于你能不能把题目描述里的每一个细节都转化成代码逻辑。这也是我觉得值得专门写一篇博文来拆解这道题的原因——它看起来简单,却包含了很多“只可意会不可言传”的经验点。
1. 题目到底在问什么
1.1 题目背景:九宫格键盘的输入规则
洛谷P1765的题目背景设定在老式手机的多按键输入法上。用过功能机的朋友都知道,那时候的手机键盘不是现在触屏上的全键盘,而是一个9键或12键布局。每个数字键上对应若干个英文字母,比如2键上对应ABC,3键上对应DEF,以此类推。
要在这种键盘上输入一个字母,规则是这样的:你想输入“a”,按一下2键;想输入“b”,按两下2键;想输入“c”,按三下2键。也就是说,你的目标是字母在一个按键上的第几个位置,你就需要连续按几次这个键。而不同字母之间,比如“a”后面跟着“d”,由于“a”在2键上,“d”在3键上,直接切换按键就可以了。但如果连续的两个字母在同一个按键上,比如“a”后面跟着“b”,你按完“a”后不能直接按两下2键输出“b”,因为这样中间的停顿会被识别成输入了新的字符。所以遇到这种情况,你需要先按一下别的键来“分隔”,在经典的规则里就是用按一次空格键来分隔。
这就是P1765的核心模拟规则。题目会给你一句由小写字母、空格和句号组成的英文句子,让你计算按出这句话总共需要按键多少次。
1.2 核心考点:这道题到底考什么
很多第一次做这道题的同学,读完之后会觉得“这不就是查表吗,有什么好考的?”但仔细分析,这道题其实在考察三个层面的能力。
第一层是规则理解能力。题目描述里包含了“同键分隔”这个特殊规则。很多人在计算的时候只算了字母本身需要的按键次数,完全忘了同键切换时需要额外按一次空格键。这就是典型的“题目没读透”——竞赛里这类失误是致命的。
第二层是映射关系的组织能力。数字2到9一共8个键,对应26个字母,再加上空格和句号,一共有28个“输入目标”。你需要以某种方式把每个输入目标对应的按键次数和所属键位存下来。这考察的是你对表结构的理解,是开一个二维数组、用多个一维数组,还是直接在代码里写个查找函数,都能体现你的代码组织能力。
第三层是边界条件的处理能力。输入的句子既有大写字母又有小写字母,还有空格和句号。大写字母怎么处理?是不是要先算一次上档键?题目里到底有没有这个规则?这些细节决定了你的代码能不能过全部测试点。
1.3 数据范围与输入输出要求
P1765的输入格式和输出格式都很简短:输入只有一行字符,以回车结束,是包含字母、空格和句号的英文句子。输出只有一个数字,表示总共需要的按键次数。
这里有一个很多人容易忽视的点:题目说的是“输入只有一行”(题面一般写一行或多行但实际是一行),但这一行里是可能有空格的。这就意味着你不能用普通的cin >> s或者scanf("%s")来读入,因为这两种方式遇到空格就会停下来。你必须使用能够读取整行含空格字符串的方式。
另外还要注意句号。英文句号‘.’在九宫格键盘中出现在1键上,通常按一下就能输入句号。这个细节题目里会说明,但如果你之前做过别的手写输入法题,可能默认句号是按键次数不同的规则,那就容易踩坑。
2. 解题思路:从手工模拟到程序化处理
2.1 先手工模拟一遍,理解规则
在学习怎么写代码之前,我强烈建议你先拿纸笔手工模拟一遍。比如输入句子“i love you.”,我们手动算算需要按键多少次。
先把整句话拆成每个字符:i、空格、l、o、v、e、空格、y、o、u、.。
在九宫格键盘上,i在4键上,是第3个字母,所以输入i需要按3次。接下来是空格,空格不属于任何字母键,通常单独处理,按一次空格键,所以空格是1次。
然后是l,l在5键上,是第3个字母,需要按3次。l的前一个字符是空格,空格和l不在同一个键上,所以不需要额外的分隔键。接下来是o,o在6键上,是第3个字母,按3次。前一个字符l在5键上,键位不同,无需分隔。再往后v在8键上,是第3个字母,按3次;e在3键上,是第2个字母,按2次。
到这里,我们遇到了一个关键点:单词“you”内部,y在9键上,按3次(y是第2个字母,需要仔细数,wxyz分别在9键的1、2、3、4位,y是第3位),o在6键上,按3次,u在8键上,按2次(tuv,u在8键上第2位)。
最后是句号,句号在1键上按1次。
如果严格按照题意一个个数,这个例子的总按键数是:i(3)+空格(1)+l(3)+o(3)+v(3)+e(2)+空格(1)+y(3)+o(3)+u(2)+句号(1)=25次。
你自己动手算一遍,就能体会到这个过程里最麻烦的就是“数某个字母在按键上排第几位”,这恰好就是我们要传给程序去做的查表工作。手工模拟完,你再看题目里的规则,会清晰很多。
2.2 两种经典解法:映射表法和计算法
理解了规则之后,我们可以有两种思路来实现。
第一种是映射表法,也是最直观、最不容易出错的方法。把所有可能需要输入的目标(小写字母、空格、句号)和它们对应的“按键次数”建立一一对应关系。这个对应关系不需要额外计算,直接手动写好放在代码里供查询。
第二种是计算法,即不直接建立字母到按键次数的映射,而是先根据字母ASCII码推算出它在哪个键上、是第几位,然后实时计算。这种方法代码更“聪明”,但更容易出错,因为你必须把26个字母在8个键上的分布规律理清楚,还要处理大写字母和标点的特殊规则。
对于新手,我的建议很明确:选映射表法。这不是因为计算法不好,而是因为竞赛里最重要的永远是正确性优先。映射表法几乎不可能错,逻辑清晰容易查错,而计算法虽然看起来精简,但极容易因为边界情况而翻车。等你以后熟练了,可以再试试计算法,体会一下不同的编程思路。
2.3 为什么推荐映射表法而不是直接算
我见过太多新手在这个题上写出一堆复杂的ASCII运算代码,结果在字母分组的边界上疯狂报错。比如要判断某个字母在哪一组,就得手动划分a-c、d-f、g-i、j-l、m-o、p-s、t-v、w-z,这些分组不是均匀的,最后两组分别有4个和4个字符(t-v是3个,w-z是4个;严格说p-s是4个)。如果靠计算法,你得记清楚每个分组的起点和长度,维护成本相当高。
而映射表法呢?你只需要在一开始就把26个字母按顺序存好,算出每个字母的按键次数,然后查表就行。这个方法的好处在于,它把“规则计算”变成了“查字典”,计算核心只有一行代码,最终程序的正确性几乎完全取决于你建的表准不准。
打个比方:这就像你去一个陌生的城市,手里有一份详细的地图,你只需要照着地图走,而不需要临时判断东南西北。地图也许不够“高级”,但它永远不会带你走错路。
3. 核心细节解析与实操要点
3.1 字符到按键次数的映射表怎么建
我们来一步步建立这张表。经典的九宫格键位大概是这样分布的:
- 2键:abc,所以a需要按1次,b按2次,c按3次
- 3键:def,d按1次,e按2次,f按3次
- 4键:ghi,g按1次,h按2次,i按3次
- 5键:jkl,j按1次,k按2次,l按3次
- 6键:mno,m按1次,n按2次,o按3次
- 7键:pqrs,p按1次,q按2次,r按3次,s按4次
- 8键:tuv,t按1次,u按2次,v按3次
- 9键:wxyz,w按1次,x按2次,y按3次,z按4次
而空格和句号都是单独的键,按一次即可。
你要做的事情是,把这个映射关系用一个容器存起来。有两种常用办法:
一是直接建立一个长度为26的整数数组,下标从0到25分别代表a到z,数组里存储对应字母需要的按键次数。比如keyCount[0] = 1代表a按1次,keyCount[1] = 2代表b按2次。这种方式的查询逻辑是keyCount[ch - 'a'],非常简洁。
二是建立一个字符到次数的pair数组,逐个判断。这种方式代码稍长,但可读性更高,适合不熟悉数组下标的同学。
我推荐用第一种,因为它把查表逻辑变成了一个简单的下标映射。下面是这种建表方式的一个完整思路:
int keyTimes[26] = {1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,4,1,2,3,1,2,3,4};这个数组里的顺序其实就是26个字母a到z依次的按键次数。你对照上面的键位分布检查一下,p在第16个位置(下标15)是1,s在第18个位置(下标17)是4,z在第25个位置(下标24)是4,就说明表建对了。
3.2 大写字母的处理思路与常见误区
P1765的题面里关于大写字母的规则是需要仔细读的。题目通常会说,输入句子中可能出现大小写字母,而输入大写字母需要在对应的按键次数之上再加按一次“上档键”(通常用*键或#键),因此每个大写字母的按键次数=1次上档键+该字母对应的小写按键次数。
很多第一次做题的同学会在这里犯迷糊:要么完全忽略大写字母,直接对小写字母查表然后把结果加上去,结果少算了上档键;要么把大写字母当成单独的一个键位去查表,结果完全算错。
正确的处理方式是:先把大写字母转换成小写字母,查表得到对应小写字母的按键次数,然后再加上1次上档键。比如输入‘A’,先转成‘a’,查表得到a是1次按键,再加1次上档键,所以实际按了2次。如果你用C++,可以用tolower(ch);如果你用Java,可以用Character.toLowerCase(ch);如果用Python,直接ch.lower()。
还要注意一个问题:题目输入里是否包含空格和大写的组合?比如“Hello World”。这种情况下,大写H的处理、空格的处理、大写W的处理要分别处理,不能混在一起。你需要在遍历字符串的循环里,对每个字符分三种情况:小写字母、大写字母、空格或句号。每种情况分别累加次数。
3.3 同键分隔的特殊规则如何落实
这道题最关键也最容易漏掉的规则,就是“同一按键上的连续字母需要额外按键分隔”。具体来说,如果当前字符和上一个字符在同一个数字键上,并且两个都是字母,那么需要在输入当前字符之前多按一次空格键(有些题面会写按任意键分隔,但按键次数上通常按1次空格键计数)。
这里有一个在实现上很容易出错的概念:分隔键只算一次,而且是算在“当前字符”头上的。也就是说,如果当前字母和上一个字母同键,那么当前字母的实际按键次数 = 1(分隔键)+ 该字母在键上的位置次数。
这个规则只针对字母,大部分题目里空格、句号不参与同键判断。又或者题面另有要求,比如空格和某些键同键之类的特殊情况,那就得严格按照题面来。P1765最常见的描述里,空格不在任何字母键上,所以你可以只在当前字符是字母时才检查与上一个字符的键位关系。
就这个规则,我见过有人把它实现成“全局加一次分隔键”,结果统计出来数字总是偏大,就是因为没理解“只在连续同键字符之间才算”。正确做法是维护一个变量记录“上一个字符属于哪个键”,每次处理一个新字符时,先判断是否和上一个字母同键,再决定要不要额外加1。
上面这些细节,你在写代码前就要想清楚。不然等代码写完了再改,往往要绕不少弯路。
4. 实操过程与代码实现
4.1 C++ 完整解法:数组建表 + 逐字符扫描
我把C++的完整实现写在这里,每一处关键逻辑都有注释。这是我认为最简单、最适合新手理解的写法,也是我实际带学生时最推荐的版本。
#include <iostream> #include <string> #include <cctype> using namespace std; int main() { // 按键次数表,下标0-25对应a-z // 依次为 2键abc,3键def,4键ghi,5键jkl, // 6键mno,7键pqrs,8键tuv,9键wxyz int keyTimes[26] = {1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,4,1,2,3,1,2,3,4}; // 每个字符所属的键位,0表示不属于任何字母键(空格/句号) // 这里用2-9表示对应的数字键,方便判断"是否同键" int keyOfChar[26] = {2,2,2, 3,3,3, 4,4,4, 5,5,5, 6,6,6, 7,7,7,7, 8,8,8, 9,9,9,9}; string s; // 用getline读取整行,包含空格 getline(cin, s); int total = 0; int prevKey = -1; // 记录上一个字母所在的键,初始为-1表示无前驱 for (int i = 0; i < s.size(); i++) { char ch = s[i]; if (ch >= 'A' && ch <= 'Z') { // 大写转小写 ch = ch - 'A' + 'a'; // 大写字母需要多按一次上档键 total += 1; } if (ch >= 'a' && ch <= 'z') { int idx = ch - 'a'; int curKey = keyOfChar[idx]; // 如果上一个字符也是字母,且在同一键上,需要按分隔键 if (prevKey == curKey) { total += 1; // 分隔键 } total += keyTimes[idx]; prevKey = curKey; } else if (ch == ' ') { total += 1; // 空格按一次 prevKey = -1; // 空格不参与同键判断 } else if (ch == '.') { total += 1; // 句号按一次 prevKey = -1; } } cout << total << endl; return 0; }这里有几个值得强调的设计决策。
我专门增加了一个keyOfChar数组,用来记录每个字母属于哪个数字键。可能有同学会说:“我已经有keyTimes了,判断同键的时候直接把相邻字母比较一下不就好了?”不行,因为比较同键不等于比较按键次数。比如a是1次、d也是1次,但它们不在同一个键上,这就是为什么必须要有一个独立的键位表,而不是复用按键次数表。如果你想省内存,也可以直接把同一个键的字母用区间判断,比如判断两个字母是否都在a-c或者都在d-f之间,但那样代码分支会多不少。用一张键位表,是一个很清晰的工程化选择。
另一个值得注意的点是prevKey的维护。当遇到空格或句号时,我会把prevKey重置为-1,这是因为空格和句号本身不参与同键判断。如果不重置,就可能出现“o空格p”这种场景——o和p其实不同键,不加分隔没问题;但如果“a空格b”,a和b都在2键上,实际输入时因为中间隔着空格,它们不需要分隔键。如果不重置prevKey,程序就会误判a和b同键而多加一次按键。
4.2 Java 解法:代码对比与要点说明
在Java里,读整行用BufferedReader或者Scanner.nextLine()都很方便。我给出一版Java实现,逻辑与C++版本完全一致,但写法上更贴近Java习惯。
import java.io.BufferedReader; import java.io.InputStreamReader; public class Main { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String s = br.readLine(); int[] keyTimes = {1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,4,1,2,3,1,2,3,4}; int[] keyOfChar = {2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,7,7,7,7,8,8,8,9,9,9,9}; int total = 0; int prevKey = -1; for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (ch >= 'A' && ch <= 'Z') { ch = Character.toLowerCase(ch); total += 1; } if (ch >= 'a' && ch <= 'z') { int idx = ch - 'a'; int curKey = keyOfChar[idx]; if (prevKey == curKey) { total += 1; } total += keyTimes[idx]; prevKey = curKey; } else if (ch == ' ') { total += 1; prevKey = -1; } else if (ch == '.') { total += 1; prevKey = -1; } } System.out.println(total); } }用BufferedReader而不是Scanner,是因为在竞赛环境中BufferedReader的速度更快,处理一行包含空格的输入也更干净利落。这里要小心的是,br.readLine()如果输入为空会读到null,但洛谷的题目保证输入至少有一行,所以这里不用做额外判空。
Java版本里将大写转小写用的是Character.toLowerCase(ch),其实直接ch += 32也可以,但可读性差,而且容易在字符编码上产生误解。我建议还是用标准库函数。
4.3 Python 解法:简洁但不失严谨
Python处理这种字符串题目非常舒服,代码量最少。但要注意的是,Python对字符和数字的转换跟C++不一样,需要用ord()函数。下面是完整的Python实现。
s = input() key_times = [1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,4,1,2,3,1,2,3,4] key_of_char = [2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,7,7,7,7,8,8,8,9,9,9,9] total = 0 prev_key = -1 for ch in s: if 'A' <= ch <= 'Z': ch = ch.lower() total += 1 if 'a' <= ch <= 'z': idx = ord(ch) - ord('a') cur_key = key_of_char[idx] if prev_key == cur_key: total += 1 total += key_times[idx] prev_key = cur_key elif ch == ' ': total += 1 prev_key = -1 elif ch == '.': total += 1 prev_key = -1 print(total)Python里要注意,input()读取一行时是不包含末尾换行符的,这样正好。还有一个容易踩的坑:如果你在本地测试时,输入里包含多个连续空格,input()会原样保留,这正好符合题目要求。千万不要用split()去预处理输入,那会把空格全部切割掉,导致结果完全不对。
4.4 三个版本放在一起对比
为了让你看得更清楚,我把三种语言实现的核心差异整理成一张表:
| 对比项 | C++ | Java | Python |
|---|---|---|---|
| 读入一行含空格 | getline(cin, s) | br.readLine() | input() |
| 大写转小写 | ch = ch - 'A' + 'a' 或 tolower | Character.toLowerCase(ch) | ch.lower() |
| 字符转下标 | ch - 'a' | ch - 'a' | ord(ch) - ord('a') |
| 建表方式 | 两个int数组 | 两个int数组 | 两个list |
| 运行速度 | 最快 | 较快 | 相对较慢 |
就这道题的数据量而言,三种语言的性能差异完全可以忽略,因为总共只需要遍历一遍字符串,时间复杂度是O(n),n是字符串长度。但如果你今后要打竞赛,建议C++或Java至少掌握一种,因为它们在处理更复杂的数据规模时更从容。
5. 常见问题与排查技巧实录
5.1 读入问题:getline和cin混用的经典大坑
我在答疑的时候,发现很多同学P1765提交不过,问题出在“读入字符串”上。最经典的错误写法是先用cin >> n之类的东西读了一个数字,然后再用getline(cin, s)读字符串。这会导致getline读到一个残留的换行符,从而什么都读不到或者读到一个空字符串。
在P1765里虽然没有前置数字输入,但不少同学在本地测试时喜欢先写一个“请输入测试次数”之类的操作,结果就把自己带沟里了。如果你确实需要先读数字再读含空格的字符串,必须在两者之间加一个cin.ignore()来吃掉换行符。例如:
int t; cin >> t; cin.ignore(); while (t--) { string s; getline(cin, s); // 处理s }这是一个重要的通用经验,我在很多入门题里都见过类似的坑,尤其是那些需要读取多行含空格字符串的题目。尽早养成“cin之后如果需要getline,就加ignore”的意识,能帮你免掉无数调试时间。
5.2 空格与句号的特殊处理:为什么我老是多算或少算
有同学遇到这种现象:自己的程序对于没有空格的字符串测试都对了,但只要输入里出现空格,答案就不对。排查下来,往往出在空格被忽略或空格被当成普通字符处理上。
产生这个问题的原因通常是用了cin >> s读入,导致字符串从空格处断开,后面的所有字符全都没被处理。比如输入“hello world”,cin >> s只会读进“hello”,后面“world”直接被丢弃了。这种错误在本地调试时特别有迷惑性,因为你可能只输入了没有空格的测试用例,自然发现不了问题。
正确做法就是前文用的getline(cin, s)或者input()。如果你已经用了正确的方式读入,但结果还是不对,那就检查一下循环里对空格是否分了一支出来单独累加1。要注意的是,别把空格也放进查表函数里去查,否则可能查出一个任意的数值。
另外还有一个关于句号的坑:有些题面的规则里句号和空格都在1键,按一次,但有些变体题里句号可能要按好几次。P1765按一次即可。如果你是根据记忆写的代码,最好回题面里确认一下句号的按键次数。
5.3 同键分隔:最容易漏掉的那1次按键
同键分隔这个规则,是P1765的区分度所在。我见过的错误五花八门,但可以归结为两类。
一类是完全不知道这个规则,所以代码里根本没有判断同键的逻辑。这种提交通常会在隐含有连续同键字母的测试点上挂掉,比如“good”中的“o”和“o”就是同键,需要分隔键,但如果你没实现,按出来的次数就会比标准答案少1。
另一类是过度实现了这个规则,比如对每一对相邻字母都判断了一次,甚至对空格和字母也判断了一次。这会导致按键次数偏多。比如“a b”这种情况,a和b虽然都在2键上,但中间隔了一个空格,输入时不需要分隔键。如果你不对“空格”做特殊处理,很容易把a和b判断为需要加一次分隔键,从而多算。
处理这个问题的统一思路就是我在正确代码里写的:维护一个prevKey变量,在遇到空格或句号时重置为-1。这样既不会漏掉真正的同键分隔,又不会对跨空格的两个字母误判。
5.4 边界情况:全空格、大写跳跃、空行等
如果你想让代码更稳健,可以额外测试这些边界情况:
- 输入全是空格,比如“ ”,此时每个空格按一次,程序应该能正确输出空格数量。
- 输入“A”,大写字母需要上档键加1加第一次按键,共2次。
- 输入“Z”,z的按键次数是4,再加上1次上档键,共5次。
- 输入“aA”,a在2键第1位,按1次;A按上档键1次加a的1次,再加2键上连续输入需要分隔1次,共1+1+1+1=4次。
- 输入只有一句号“.”,输出1。
这些边界情况在洛谷的测试点里不一定都出现,但自己做一遍可以大大增强对题意的理解。
5.5 本地调试小技巧:构造随机输入对比
如果你想更高效地自测,可以写一个“暴力校检验证器”:先用一个逻辑很简单但不够优雅的方法算出结果(比如直接手写分支判断每个字符的按键次数),再用你的程序运行同一个输入,两个结果对比,不一致就可以定位问题。这个方法适用于几乎所有入门题和部分中档题,是我个人非常推荐的自我检测手段。
手动构造测试用例时,也可以刻意设计一些包含连续同键字母的句子,比如“see you soon”,或者大小写混合的句子“I Am OK.”,这些句子最能暴露分隔规则处理是否到位。
6. 从P1765出发:同类型题的泛化能力
6.1 这类“模拟生活规则”题在竞赛中的定位
P1765属于典型的“模拟+字符串”题型。在洛谷的题目体系中,这类题通常被标记为普及-/入门难度,但它的思想会延伸到更高难度的问题中。比如P1843、P14258这类题目名里包含字符串处理的题,很多都是P1765的变体:给出一套复杂的规则,要求你严格模拟,最终计算某种结果。
在算法竞赛里,“模拟”类题目占据了不小的比例。它们看似简单,实则非常考验做题的细致程度。很多选手在平时训练时对模拟题不以为然,觉得“只要照着题意敲代码就行”,但一到正式比赛,往往是在模拟题上因为漏看条件而丢分。P1765作为一道经典的模拟题,最大的价值就是帮你训练“读题细、规则全、边界清”的做题习惯。
从技巧角度看,这道题体现的思想是“空间换时间”——与其在每次判断时都重新计算按键次数,不如提前把映射关系存好。这其实是竞赛中最常用的优化手段之一。很多看似复杂的题目,一张预处理的表就能把问题从“每次现算”变成“查表即可”,大幅降低出错率。
6.2 变体题:从九宫格到其他映射场景
如果你想把这道题吃透,可以做几个很有意思的变体练手。
第一个变体是“键盘改成双拼/全拼输入法”,规则会变成一个字母可能对应多个组合键,计算方式会从“按键次数”变成“组合键数”,但核心思想仍然是查表模拟。
第二个变体是“计算某段电话号码对应的按键次数”,题目会给你一个电话号码,包括数字、*、#等符号,规则会更复杂,但同样可以用一个映射表把所有符号的按键次数存起来。
第三个变体是“数字转字母的组合输出”,也就是给定一串数字,让你求所有可能的字母组合。这个题目就变成了回溯算法题,虽然和P1765的模拟目标不一致,但它让你意识到“按键与字母映射”这个模型可以产生出完全不同的算法问题。
我在带新人时,常让他们把P1765的代码拿去做这些变体,既能巩固基础知识,又能提前预览后续要学的高级算法。这种“一题多练”的方法,比单纯刷题效率高得多。
6.3 我个人的做题体会
这道题我做了不止一次,每次都会有不同的感触。第一次做的时候,我用的是最朴素的逐个字符if-else判断,代码写了一长串,好不容易过了。后来再用映射表法,才意识到“表驱动”这种思想的威力——很多代码里的复杂逻辑,本质上是把一张表硬生生写成了if-else。
如果你想让自己的代码风格更进一步,可以试试“表驱动+函数封装”的写法。把“判断一个字符要按几次键”单独封装成一个函数,返回按键次数,主循环只负责读字符和累加。这种拆分的代码更清晰,以后遇到类似逻辑可以直接复用函数。
说到底,P1765只是众多入门题中的一颗小珍珠,但它是很多同学第一次体会到“规则模拟”乐趣的地方。如果有同学能把这道题完全吃透,理解查表、边界、读入处理这几个要点,后面刷再难的模拟题都会从容很多。