第 6~9 节只教了三件事:用值当下标的桶、用位置当下标的二维数组、把字符当数据的字符串。这一节不讲新算法,只做一件事——把这三件事放进 5 道黄题里,逼你在 30 秒内认出该用哪一件。
节奏建议:90 分钟 5 题。先花 3 分钟通读全部题面,把“一定会做”“有思路”“完全不会”分成三堆;再按从易到难的顺序单题限时 18 分钟。每写完一题立刻提交,不要攒到最后一起交——早一步看到 WA,就早一步知道自己错在哪个下标上。
| 题型 | 题目里的信号词 | 下标表示什么 | 固定写法 |
|---|---|---|---|
| 桶计数 | 出现次数 / 第几个 / 是否出现过 / 去重 / 数条数 | 值本身:字母、数字、和 | cnt[x]++ 累加,再从 0 扫到值域上界 |
| 二维数组 | n 行 m 列 / 上下左右 / 八个方向 / 网格 / 方阵 | 行号与列号 | 坐标从 1 开始,数组多开 5 格,方向用 (dx,dy) 常量数组 |
| 字符串 | 一行文字 / 含空格 / 不区分大小写 / 子串 / 每个字符 | 字符在串中的位置 i | 整行 getline,逐字符处理,时刻盯住左右边界 |
本节没有真正的新函数,但有 4 件工具会在 5 道题里反复出现。先花 10 分钟看完这 4 张卡,比写到一半再回来查要快得多。
| 组成 | 含义 / 格式要求 |
|---|---|
getline(cin, s) | 读一整行到 string s,读到 '\n' 为止,并把 '\n' 从流里取走。空格、Tab 原样保留在 s 里。 |
cin >> s | 只读一个“词”,遇到空白就停。含空格的句子用它只能拿到第一个词。 |
cin.ignore() | 丢掉缓冲区里的一个字符,通常用来吃掉上一次 cin >> 留下的 '\n'。 |
cin.get() | 读一个字符并返回它;不想保存返回值时和 ignore 等价。 |
使用顺序 | 先 cin >> 读入,再用 getline,中间必须插一句 cin.ignore();两次 getline 连用则不需要。 |
string s;
int n;
cin >> n; // 输入 "3\nhello world"
cin.ignore(); // 吃掉 3 后面那个 '\n'
getline(cin, s); // s = "hello world"
// 两行都是整行时,直接两次 getline,不需要 ignore
string w, t;
getline(cin, w); // 第一行
getline(cin, t); // 第二行漏了 ignore() 的症状很固定:getline 读到空串,后面所有数据整体错位一行,样例必然全错。
含空格的输入(P1308、P1765)必须整行读;用 cin 只会得到第一个词,答案稳定偏小。
| 组成 | 含义 / 格式要求 |
|---|---|
tolower(c) | 大写字母返回对应小写,其它字符原样返回。返回 int,但直接赋给 char 也能编译。 |
toupper(c) | 小写字母返回对应大写,其它字符原样返回。 |
参数 | 传的是“字符的整数值”,不是下标。写成 tolower(s[i] - 'a') 是错的。 |
等价写法 | 判断小写可以直接写 s[i] >= 'a' && s[i] <= 'z',不依赖 cctype,也不会踩 char 符号性的坑。 |
string s = "Hello World";
for (int i = 0; i < (int)s.size(); ++i)
s[i] = tolower(s[i]); // "hello world"
for (int i = 0; i < (int)s.size(); ++i)
if (s[i] >= 'a' && s[i] <= 'z') { /* 是小写字母 */ }tolower('A') 的结果就是整数 97,和 'a' 相等,类型不是 char 也没关系。
统一转小写之后,比较时就不用再管大小写——P1308 的标准做法就是先全转小写。
| 组成 | 含义 / 格式要求 |
|---|---|
s.size() | 字符个数。和 int 比较时先写成 (int)s.size(),否则 -1 会被当成巨大的正数。 |
s.find(t) | 从前往后找子串 t,返回第一次出现的下标;找不到返回 string::npos。 |
s.find(t, pos) | 从下标 pos 开始找。循环里要靠它不断往后推进,才能找全所有出现位置。 |
s.substr(pos, len) | 从 pos 开始取 len 个字符;len 超过末尾时自动截到末尾,不会越界报错。 |
s[i] | 下标访问单个字符,i 从 0 到 (int)s.size()-1。 |
string s = "abcabc";
int p = s.find("bc"); // p = 1
if (p != (int)string::npos) { /* 找到了 */ }
string t = s.substr(1, 2); // t = "bc"
int len = (int)s.size(); // 6find 返回的是下标,不是“第几个”。第一个字符的位置是 0——P1308 要的正是这个 0 起算的位置。
string::npos 被赋给 int 时会变成 -1,所以 (int)s.find(t) == -1 也是常见且正确的判空写法。
| 组成 | 含义 / 格式要求 |
|---|---|
memset(a, 0, sizeof(a)) | 把 a 的每个字节设成 0。只对 0 和 -1 安全,填 1 会得到 0x01010101 而不是 1。 |
sizeof(a) | a 必须是真数组。函数参数里的数组名是指针,sizeof 得到的是 8,不是长度。 |
fill(begin, end, val) | 按元素赋值,可以填任意值,区间左闭右开。 |
全局数组 | 开在 main 外面的数组默认全是 0,连 memset 都不用写,这是竞赛里最省事的做法。 |
int cnt[26] = {0}; // 局部数组:必须显式清零
int g[105][105]; // 全局数组:默认全 0
int b[10];
fill(b, b + 10, 7); // 10 个元素全变成 7多组数据忘了清空,第二组会带着第一组的残留,症状是“第一组对、后面全错”。
把大数组开在 main 里(栈上)会栈溢出 RE;元素数超过 10⁵ 就一律开全局。
给出 n 行 m 列的雷区,字符 * 表示地雷格,? 表示非地雷格。要求把每个非地雷格换成它周围八个方向上地雷格的个数,地雷格原样输出 *。“周围”指上、下、左、右、左上、右上、左下、右下八个直接相邻的格子。
输入格式:第一行两个整数 n、m(1 ≤ n,m ≤ 100)。接下来 n 行,每行 m 个字符。输出格式:n 行,每行 m 个字符,地雷格为 *,非地雷格为周围地雷个数。
题目只要求一件事:对每个非雷格,数它八个方向上有几个雷。所以数据结构必然是字符二维数组——输入里是 * 和 ?,不是数字。
动手前先把三个决定定下来,这三个决定就是这道题的全部难点:
剩下的是纯粹的循环:对每个格子,是雷就输出 *,不是雷就用 8 个方向数邻居、输出数字;每行结束补一个换行。
#include <bits/stdc++.h>
using namespace std;
char g[105][105]; // 全局数组:默认 '\0',越界位置不会误判成雷
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> g[i][j]; // cin 读 char 会自动跳过空格和换行,网格逐字符读不会错位
int dx[8] = {-1, -1, -1, 0, 0, 1, 1, 1}; // 八个方向成对定义
int dy[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (g[i][j] == '*') { cout << '*'; continue; } // 是雷,原样输出,不数邻居
int c = 0;
for (int k = 0; k < 8; ++k) {
int x = i + dx[k], y = j + dy[k];
if (g[x][y] == '*') ++c; // 只需判内容,不需判界
}
cout << c;
}
cout << '\n'; // 换行放在内层循环外,一行一个
}
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
char g[105][105]; | 字符网格必须用 char。用 int 存、再 cin >> 读 '*',cin 会立刻失败,后面全是 0。 |
cin >> g[i][j]; | 提取运算符读 char 时跳过所有空白,所以网格里没空格也能一行一行读下来。 |
int dx[8] = {-1,-1,-1,0,0,1,1,1}; | 八个方向的横坐标增量,和 dy 一一配对。顺序无所谓,成对就行。 |
if (g[i][j] == '*') { cout << '*'; continue; } | 雷格不输出数字,continue 直接跳到下一格,省掉一次 8 方向扫描。 |
for (int k = 0; k < 8; ++k) | 把八个方向写成循环,比 8 个 if 短,也不容易漏方向。 |
if (g[x][y] == '*') ++c; | 只判内容不判界:105 已覆盖 100 加 1 个偏移的所有下标,不会读到数组外面。 |
cout << '\n'; | 换行在列循环之外,保证每行恰好 m 个字符加一个换行。 |
字符网格写成 int g[105][105],再用 cin >> g[i][j]:读到 * 时 cin 失败,之后所有格子保持初值 0,答案全错(WA),而且看起来“代码没毛病”。
方向数组里混进了 (0,0):中心格被算成自己的邻居,每个数字都偏大 1。
数组只开 char g[101][101]:n = 100 时 i+1 = 101 越界,轻则 WA,重则 RE。多开 5 格几乎不花内存。
行列读反:cin >> n >> m 之后条件写成 j <= n,读入整体错位,样例第一行就挂。
字符串题里用 s.substr(i, m) == w 做比较:每次复制 m 个字符,长度 10⁶ 时慢一个量级,容易 TLE;逐字符比较才是 O(1) 额外空间。
关掉这一页,把上面的程序从头敲一遍,提交 P2670,先确认能看到 AC。
改一改:把八个方向换成四个方向(上下左右),样例输出会从 *10 / 221 / 1*1 变成 *10 / 110 / 1*1。先自己推一遍再跑,对不上就说明方向数组和你想的不一样。
再改一改:把雷的字符从 * 换成 #,只改输入读入和比较这两处——体会“字符只是数据”。
下面 5 道题就是本次训练。每题先只看题面和提示,自己写完再展开思路对照;拦不住的题先记下卡在哪一步,回头重看本节对应那一行。
| 题号 | 题目 | 考的是哪一节 | 要多想一步的地方 | 建议用时 |
|---|---|---|---|---|
| A | P1125 笨小猴 | 桶计数(第 6 节) | minn 只在出现过的字母里取 | 18 分钟 |
| B | P1789 插火把 | 二维数组(第 7 节) | 火把缺四个角,萤石是整块 5×5 | 20 分钟 |
| C | P1980 计数问题 | 数位拆解(第 9 节) | x = 0 时不能凭空补前导零 | 15 分钟 |
| D | P1308 统计单词数 | 字符串(第 8、9 节) | 匹配前要确认左右都不是字母 | 22 分钟 |
| E | P1765 手机 | 字符映射(第 8 节) | 空格算一次键,且必须整行读入 | 15 分钟 |
数组大小:最大下标再加 5,够不够?
初值:局部数组写 = {0} 了吗?全局数组默认 0。
输出:行末有多余空格吗?两个数之间是一个空格还是别的?不存在的情况输出的是 −1 还是 0?
读入:含空格的整行用 getline 了吗?getline 前需要 ignore 吗?
限时 100 分钟。每题先自己写 20~25 分钟;写不出来先看提示,再给自己 10 分钟;还不行才点开思路。每题都独立限时,不要一题耗死。
给出一个只含小写字母的单词(长度小于 100)。设 maxn 为出现次数最多的字母的出现次数,minn 为出现次数最少的字母的出现次数(只统计出现过的字母)。若 maxn − minn 是质数,就称这个单词是 Lucky Word。
输入格式:一行,一个只含小写字母的字符串。输出格式:两行。第一行是 Lucky Word 或 No Answer;第二行是一个整数,是 Lucky Word 时输出 maxn − minn,否则输出 0。
样例:输入 error → 输出 Lucky Word 和 2(r 出现 3 次,最少 1 次,3−1=2 是质数);输入 olympic → 输出 No Answer 和 0(每个字母都只出现 1 次,差为 0)。
先数每个字母出现了几次:26 个格子,下标就是字母本身。
minn 不是 0:没出现过的字母不参与比较,否则答案永远是 No Answer。
质数只用试除到 √d,但别忘了 0 和 1 都不是质数。
cnt[26],cnt[s[i] - 'a']++ 数出每个字母的出现次数。
扫一遍 26 个桶,跳过 cnt == 0 的,同时维护 mx 和 mn。
d = mx − mn。d < 2 直接判死;否则试除 2 到 √d,有因子就不是质数。
为什么这么写就对:桶把“谁出现最多”变成一次线性遍历;试除是判质数的标准 O(√d) 做法,d ≤ 100。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s; // 单词只含小写字母、无空格,cin 一次读完
int cnt[26] = {0}; // 局部数组必须清零,否则是随机值
for (int i = 0; i < (int)s.size(); ++i)
++cnt[s[i] - 'a']; // 字母 → 0~25 的下标,桶 +1
int mx = 0, mn = 1000; // mn 初值要大于任何可能的出现次数
for (int i = 0; i < 26; ++i) {
if (cnt[i] == 0) continue; // 没出现过的字母不参与 min
mx = max(mx, cnt[i]);
mn = min(mn, cnt[i]);
}
int d = mx - mn;
bool prime = (d >= 2); // 0 和 1 都不是质数,先一刀切死
for (int i = 2; i * i <= d; ++i) // 试除到 √d 就够
if (d % i == 0) { prime = false; break; }
if (prime) cout << "Lucky Word\n" << d << "\n";
else cout << "No Answer\n0\n"; // 不是 Lucky Word 时第二行必须是 0
return 0;
}| 片段 | 作用 |
|---|---|
cnt[s[i] - 'a']++ | 把字符映射成 0~25 的下标:'a' → 0,'z' → 25,桶长正好 26。 |
if (cnt[i] == 0) continue; | 漏了这句 mn 恒为 0,d 变成 mx,几乎全部输出 No Answer。 |
int d = mx - mn; | 题目要的就是这个差。 |
bool prime = (d >= 2); | 先把 d < 2 全部判死,后面循环才能从 2 开始、不用特判。 |
for (int i = 2; i * i <= d; ++i) | 只需试到 √d:若 d 有大于 √d 的因子,必有一个小于 √d 的搭档。 |
Lucky Word(首字母大写、中间一个空格),No Answer 同理。在 n×n 的方阵里放 m 个火把和 k 个萤石。火把照亮自己所在格,以及与自己横纵偏移的绝对值之和不超过 2 的格子(一共 13 格,是十字形:5×5 去掉 4 个角、再去掉四条边上与角相邻的 8 格);萤石照亮以自己为中心的完整 5×5 正方形(25 格)。没有光又没有东西的格子会生成怪物,求这样的格子数。
输入格式:第一行三个整数 n、m、k;接下来 m 行每行两个整数,是火把坐标;再接下来 k 行每行两个整数,是萤石坐标。坐标从 1 开始,保证 1 ≤ n ≤ 100,1 ≤ m ≤ 25,0 ≤ k ≤ 5。输出格式:一行一个整数,表示会生成怪物的格子数。
样例:输入 5 1 0 加一行 3 3(5×5 方阵,只有 1 个火把在正中)→ 输出 12(火把照亮 13 格,25 − 13 = 12)。
不要算面积、不要算重叠——直接开一个标记数组,照到就置 1,最后数还剩几个 0。
火光和萤石的区别只有一个:火把要跳过 |dx| + |dy| 超过 2 的那 12 个偏移。
输入是两段:m 个火把全部读完,才开始读萤石。
a[105][105] 全 0,a[i][j] == 1 表示格子 (i,j) 已经被照亮。
火把:dx、dy 各从 −2 枚举到 2(25 个偏移),|dx| + |dy| > 2 的跳过(正好 12 个),剩下的在界内就置 1。
萤石:同样 25 个偏移,一个都不跳,在界内就置 1。
最后数 a[i][j] == 0 的个数。复杂度 O((m+k)×25 + n²):最坏 25×25 + 100² ≈ 1.1×10⁴ 次操作,瞬间完成。
#include <bits/stdc++.h>
using namespace std;
int a[105][105]; // 全局数组:默认全 0,不用手动清零
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k, x, y;
cin >> n >> m >> k;
for (int t = 0; t < m; ++t) { // 先把 m 个火把全部读完
cin >> x >> y;
for (int dx = -2; dx <= 2; ++dx)
for (int dy = -2; dy <= 2; ++dy) {
if (abs(dx) + abs(dy) > 2) continue; // 十字形:这 12 个偏移照不到
int nx = x + dx, ny = y + dy;
if (nx < 1 || nx > n || ny < 1 || ny > n) continue; // 出界的偏移直接丢
a[nx][ny] = 1;
}
}
for (int t = 0; t < k; ++t) { // 再读 k 个萤石
cin >> x >> y;
for (int dx = -2; dx <= 2; ++dx)
for (int dy = -2; dy <= 2; ++dy) {
int nx = x + dx, ny = y + dy;
if (nx < 1 || nx > n || ny < 1 || ny > n) continue;
a[nx][ny] = 1; // 萤石是整块 5×5,不判距离
}
}
int ans = 0;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (a[i][j] == 0) ++ans; // 还是 0 的格子就是没被照亮
cout << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
int a[105][105]; | 开在全局:默认全 0,而且不受栈空间限制(8 MB 栈放不下大二维数组)。 |
abs(dx) + abs(dy) > 2 | 曼哈顿距离。等于 3 的有 8 个、等于 4 的有 4 个,共 12 个偏移被剔除,剩下的正好是 13 格十字。 |
nx < 1 || nx > n | 把越界统一处理成 continue,比给数组多开两圈更直观,坐标从 1 开始时尤其好写。 |
先读 m 个火把,再读 k 个萤石 | 输入是分两段给的。边读火把边读萤石会把坐标错位,答案全错。 |
if (a[i][j] == 0) ++ans; | 统计必须扫满 1..n 的整块方阵,只扫一部分就会漏。 |
试计算在区间 1 到 n 的所有整数中,数字 x(0 ≤ x ≤ 9)一共出现了多少次。例如在 1 到 11 中,数字 1 在 1、10、11 里共出现了 4 次。
输入格式:一行两个整数 n、x(1 ≤ n ≤ 10⁶,0 ≤ x ≤ 9)。输出格式:一行一个整数,表示出现次数。
样例:输入 11 1 → 输出 4(1、10、11 里的 1 各算一次,11 里有两个 1)。
不要找公式:n ≤ 10⁶ 时,把每个数拆成十进制位逐位比较就足够快。
拆一个数:反复取 % 10 拿到末位,再 / 10 丢掉末位,直到这个数变成 0。
x = 0 时要小心:数字 5 只有一位,不能因为“位数不够”就给它补前导零。
从 1 枚举到 n,每拿到一个数就把它复制到临时变量 t。
循环:if (t % 10 == x) ++ans; t /= 10; 直到 t == 0。
因为 t == 0 就停,数字 5 只会拆出 5 这一位,不会凭空多出前导零,x = 0 也自然正确。
总操作数约 n × 位数 ≈ 6×10⁶,一秒内轻松跑完。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, x;
cin >> n >> x;
long long ans = 0; // 答案类变量一律 long long,零成本
for (int i = 1; i <= n; ++i) {
int t = i; // 复制一份再拆,不要拆坏循环变量
while (t > 0) {
if (t % 10 == x) ++ans; // 末位是不是 x
t /= 10; // 丢掉末位
}
}
cout << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
int t = i; | 拆位会破坏数字,所以先复制。直接拆 i 会让 i 归零,for 里的 i++ 只把它加到 1,于是无限循环。 |
while (t > 0) | t 归零就停,这样数字 5 只贡献一位,x = 0 时不会多算前导零。 |
t % 10 | 取十进制末位;t % 10 == x 就说明这一位等于 x。 |
t /= 10 | 整数除法,末位被丢掉,下一位变成末位。 |
long long ans | n = 10⁶ 时答案不超过 6×10⁶,int 装得下;但用 long long 可以免掉一类低级错误。 |
给一个单词和一篇文章(一行),求这个单词在文章里出现了多少次,以及第一次出现时首字母在文章中的位置(从 0 开始计)。匹配不区分大小写,但必须是完整单词:to 不能匹配 today 里的 to。
输入格式:第一行一个单词(长度不超过 10);第二行一篇文章(长度不超过 10⁶,可能含空格,空格占一个字母位)。输出格式:若出现过,输出两个整数——出现次数、第一次出现时首字母的位置(从 0 开始计);若没出现过,输出 −1。
样例:输入单词 To、文章 to be or not to be is a question → 输出 2 0(位置从 0 起算,文章开头就是 to)。
第一行不会有空格,第二行会——所以第二行必须整行读。
先把单词和文章都转成小写,后面就只比较小写字母。
“完整单词”的判据是两条:左邻不是字母,右邻也不是字母,缺一不可。
两次 getline 读入单词 w 和文章 s,彻底避开 cin 与换行残留的坑。
逐字符 tolower,两边都变成小写。
枚举起点 i(0 ≤ i ≤ n−m):若 s[i−1] 是字母,说明 i 落在某个单词中间,跳过;若 s[i+m] 是字母,说明只是某个单词的前缀,也跳过。
通过了边界检查再逐字符比较,全等就 ans++,第一次成功时记下 i。
最后按 ans 是否为 0 分别输出。复杂度 O(n·m),m ≤ 10 时约 10⁷ 次比较,稳过。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string w, s;
getline(cin, w); // 第一行:单词
getline(cin, s); // 第二行:文章,可能含空格,必须整行读
for (int i = 0; i < (int)w.size(); ++i) w[i] = tolower(w[i]); // 统一小写
for (int i = 0; i < (int)s.size(); ++i) s[i] = tolower(s[i]);
int n = (int)s.size(), m = (int)w.size();
int ans = 0, first = -1; // first = -1 表示还没找到过
for (int i = 0; i + m <= n; ++i) {
if (i > 0 && s[i - 1] >= 'a' && s[i - 1] <= 'z') continue; // 左边还是字母 → 不在词首
if (i + m < n && s[i + m] >= 'a' && s[i + m] <= 'z') continue; // 右边还是字母 → 只是前缀
int k = 0;
while (k < m && s[i + k] == w[k]) ++k;
if (k == m) {
++ans;
if (first < 0) first = i; // 只记第一次,后面不再覆盖
}
}
if (ans == 0) cout << -1 << "\n";
else cout << ans << " " << first << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
getline(cin, w); getline(cin, s); | 两行都整行读。第二行含空格,用 cin >> 只会读到第一个词。 |
s[i] = tolower(s[i]); | 统一小写之后,比较时不必再反复转换,边界判断也只需要看 'a'~'z'。 |
s[i - 1] >= 'a' && s[i - 1] <= 'z' | 左边界检查:前面还有字母,说明 i 落在某个单词内部。 |
s[i + m] >= 'a' && s[i + m] <= 'z' | 右边界检查:后面还有字母,说明匹配到的只是某个单词的前缀。 |
if (first < 0) first = i; | 位置要求的是“第一次”,后续匹配成功不能再改。 |
if (ans == 0) cout << -1 | 题目要求不存在时输出 −1,不是 0 也不是 “0 0”。 |
to 会匹配到 today、together 里的 to,答案偏大。手机键盘上,数字键 2~9 依次对应 abc、def、ghi、jkl、mno、pqrs、tuv、wxyz。同一个键上的字母按顺序排列:按一次得第一个字母,按两次得第二个,依此类推(所以 s 在 7 键上要按 4 次)。空格按一次键。
输入格式:一行文字,只含小写字母和空格,长度不超过 200。输出格式:一行一个整数,表示打完这句话至少按了多少次键。
样例:输入 i have a dream → 输出 23(i 3 次 + 3 个空格 + h 2 + a 1 + v 3 + e 2 + a 1 + d 1 + r 3 + e 2 + a 1 + m 1 = 23)。
把“字母 → 按几下”直接做成一张 26 项的常量表,最快也最不容易错。
空格不在 26 个字母里,要单独判断,算 1 次。
这一行含空格,cin >> 只能读到第一个词。
key[26] = {1,2,3, 1,2,3, ...},下标 s[i] - 'a' 直接查表得到按键次数。
getline 读整行;遇到空格 ans += 1;遇到小写字母 ans += key[s[i] - 'a']。
为什么手写表比推公式好:pqrs 和 wxyz 是 4 个一组,前面都是 3 个一组,公式要分情况,表不用。
复杂度 O(n),n ≤ 200,怎么读都不会慢。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// key[i] = 打完第 i 个小写字母要按几下:a→1, b→2, ... z→4
int key[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};
string s;
getline(cin, s); // 含空格,必须整行读
int ans = 0;
for (int i = 0; i < (int)s.size(); ++i) {
if (s[i] == ' ') ++ans; // 空格在 0 键上,按一次
else if (s[i] >= 'a' && s[i] <= 'z') ans += key[s[i] - 'a'];
}
cout << ans << "\n";
return 0;
}| 片段 | 作用 |
|---|---|
int key[26] = {...} | 常量表:下标 0~25 对应 a~z,值就是“按几下”。查表 O(1),也不会算错分组。 |
getline(cin, s); | 整行读入,空格才会进到字符串里。 |
if (s[i] == ' ') ++ans; | 空格也要按一次键,这是最容易漏的一项。 |
ans += key[s[i] - 'a']; | 字母 → 下标 → 次数,一次查表搞定。 |
else if (s[i] >= 'a' && s[i] <= 'z') | 加 else:空格分支已经处理过,这里只需保证下标落在 0~25,不会越界。 |