← 目录 第 10 / 60 节 全部 60 节

10 · 回顾二:数组与字符串综合

第 6~9 节三件事的合并验收:用桶统计次数、用二维数组处理网格、用字符串处理一行文字。5 道黄题限时训练,逐题给出完整思路与可提交代码。
⭐ 已学范围回顾训练入门+建议 90 分钟回顾5 题

第 6~9 节只教了三件事:用值当下标的桶、用位置当下标的二维数组、把字符当数据的字符串。这一节不讲新算法,只做一件事——把这三件事放进 5 道黄题里,逼你在 30 秒内认出该用哪一件。

节奏建议:90 分钟 5 题。先花 3 分钟通读全部题面,把“一定会做”“有思路”“完全不会”分成三堆;再按从易到难的顺序单题限时 18 分钟。每写完一题立刻提交,不要攒到最后一起交——早一步看到 WA,就早一步知道自己错在哪个下标上。

回顾课的目标不是“五题都 AC”,而是看到任何一道新题,都答得出两个问题:下标表示什么?要开多大?
题型题目里的信号词下标表示什么固定写法
桶计数出现次数 / 第几个 / 是否出现过 / 去重 / 数条数值本身:字母、数字、和cnt[x]++ 累加,再从 0 扫到值域上界
二维数组n 行 m 列 / 上下左右 / 八个方向 / 网格 / 方阵行号与列号坐标从 1 开始,数组多开 5 格,方向用 (dx,dy) 常量数组
字符串一行文字 / 含空格 / 不区分大小写 / 子串 / 每个字符字符在串中的位置 i整行 getline,逐字符处理,时刻盯住左右边界

本节没有真正的新函数,但有 4 件工具会在 5 道题里反复出现。先花 10 分钟看完这 4 张卡,比写到一半再回来查要快得多。

新函数拆解 · getline 与 cin.ignore(整行读入)
getline(std::cin, s); std::cin.ignore();
组成含义 / 格式要求
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 / toupper(大小写统一)
int tolower(int c); int toupper(int c);
组成含义 / 格式要求
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 的标准做法就是先全转小写。

新函数拆解 · string::find 与 substr(查找、截取)
s.find(t) s.substr(pos, len)
组成含义 / 格式要求
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();       // 6
注意

find 返回的是下标,不是“第几个”。第一个字符的位置是 0——P1308 要的正是这个 0 起算的位置。

string::npos 被赋给 int 时会变成 -1,所以 (int)s.find(t) == -1 也是常见且正确的判空写法。

新函数拆解 · memset 与 fill(批量赋值)
memset(a, 0, sizeof(a)); fill(a, a + n, 0);
组成含义 / 格式要求
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⁵ 就一律开全局。

例题 · 扫雷游戏洛谷 P2670普及-去洛谷做这题 ↗

给出 n 行 m 列的雷区,字符 * 表示地雷格,? 表示非地雷格。要求把每个非地雷格换成它周围八个方向上地雷格的个数,地雷格原样输出 *。“周围”指上、下、左、右、左上、右上、左下、右下八个直接相邻的格子。

输入格式:第一行两个整数 n、m(1 ≤ n,m ≤ 100)。接下来 n 行,每行 m 个字符。输出格式:n 行,每行 m 个字符,地雷格为 *,非地雷格为周围地雷个数。

输入: 3 3 *?? ??? ?*? 输出: *10 221 1*1
例题怎么想:一道题同时用上二维数组和字符

题目只要求一件事:对每个非雷格,数它八个方向上有几个雷。所以数据结构必然是字符二维数组——输入里是 * 和 ?,不是数字。

动手前先把三个决定定下来,这三个决定就是这道题的全部难点:

剩下的是纯粹的循环:对每个格子,是雷就输出 *,不是雷就用 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 个字符加一个换行。
这 5 个坑会直接 WA 或 RE,都是高频

字符网格写成 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 道题就是本次训练。每题先只看题面和提示,自己写完再展开思路对照;拦不住的题先记下卡在哪一步,回头重看本节对应那一行。

题号题目考的是哪一节要多想一步的地方建议用时
AP1125 笨小猴桶计数(第 6 节)minn 只在出现过的字母里取18 分钟
BP1789 插火把二维数组(第 7 节)火把缺四个角,萤石是整块 5×520 分钟
CP1980 计数问题数位拆解(第 9 节)x = 0 时不能凭空补前导零15 分钟
DP1308 统计单词数字符串(第 8、9 节)匹配前要确认左右都不是字母22 分钟
EP1765 手机字符映射(第 8 节)空格算一次键,且必须整行读入15 分钟
每题提交前的 30 秒检查

数组大小:最大下标再加 5,够不够?

初值:局部数组写 = {0} 了吗?全局数组默认 0。

输出:行末有多余空格吗?两个数之间是一个空格还是别的?不存在的情况输出的是 −1 还是 0?

读入:含空格的整行用 getline 了吗?getline 前需要 ignore 吗?

本次训练 · 共 5 题

做题方式

限时 100 分钟。每题先自己写 20~25 分钟;写不出来先看提示,再给自己 10 分钟;还不行才点开思路。每题都独立限时,不要一题耗死。

大题 挑战题 1 · 笨小猴
洛谷 P1125普及-去洛谷做这题 ↗

给出一个只含小写字母的单词(长度小于 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 的搭档。
这题最容易错的地方
  • 把没出现的字母算进 minn,导致 mn = 0。
  • 把 1 当成质数:单词里只有一种字母时 d = 0,只有两种且次数相差 1 时 d = 1,都会被误判成 Lucky。
  • 输出字符串写错:必须是 Lucky Word(首字母大写、中间一个空格),No Answer 同理。
  • 两个数字之间用了 cout << ' ',但这里根本不是一行两个数——是两行,别按套路写。
大题 挑战题 2 · 插火把
洛谷 P1789普及-去洛谷做这题 ↗

在 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 的整块方阵,只扫一部分就会漏。
这题最容易错的地方
  • 把火把也当成整块 5×5,照亮范围变大,答案偏小。
  • 两段输入顺序读反(先读萤石)→ 坐标全部错位,样例就挂。
  • 数组只开 100×100:坐标 100、偏移 +2 时 a[102][102] 越界,轻则 WA,重则 RE。
  • 忘记 n = 1 的边界:此时火把只照亮 1 格,答案应为 0;越界判断写错就会输出 1 或负数。
大题 挑战题 3 · 计数问题
洛谷 P1980普及-去洛谷做这题 ↗

试计算在区间 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 ansn = 10⁶ 时答案不超过 6×10⁶,int 装得下;但用 long long 可以免掉一类低级错误。
这题最容易错的地方
  • 把 while 写成 while (t >= 0) → 死循环,TLE。
  • 循环区间写成 0 到 n,多算了数字 0 本身(x = 0 时答案正好多 1)。
  • 用 to_string 再补零对齐位数 → x = 0 时把前导零也算进去,答案偏大。
  • 把 x 声明成 char:cin 读到的是字符 '1'(值 49),和 t % 10 的整数比较永远不相等,答案恒为 0。
大题 挑战题 4 · 统计单词数
洛谷 P1308普及-去洛谷做这题 ↗

给一个单词和一篇文章(一行),求这个单词在文章里出现了多少次,以及第一次出现时首字母在文章中的位置(从 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”。
这题最容易错的地方
  • 用 cin >> 读文章,只拿到第一个词,答案稳定偏小。
  • 只做左边界检查:to 会匹配到 today、together 里的 to,答案偏大。
  • 位置输出成 i + 1(从 1 开始计),全部偏大 1。
  • 忘了 ans == 0 时输出 −1,样例能过但测试点 1 就 WA。
大题 挑战题 5 · 手机
洛谷 P1765普及-去洛谷做这题 ↗

手机键盘上,数字键 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,不会越界。
这题最容易错的地方
  • 用 cin >> s,只统计到第一个词。
  • 忘了空格算 1 次:样例 "i have a dream" 有 3 个空格,答案会少 3。
  • 按键表写错(把 pqrs 当成 3 个一组),遇到 s 就少算 1。
  • 答案用 char 累加,超过 127 后变成奇怪的字符,输出乱码。
做完之后自问
延伸阅读