2.2 字符串 参考代码
P5733 [深基6.例1] 自动修正
0-based C 风格字符串
参考代码
#include <cstdio>
#include <cstring> // 引入字符串处理库,用于 strlen
const int N = 105; // 定义一个常量 N,表示数组的最大大小,防止溢出
char a[N]; // 定义一个字符数组 a,用于存储输入的C风格字符串
int main()
{
// C风格字符串基础
// scanf 在格式串后面的参数需要的是内存地址
// 对于 int 这样的基本类型变量,变量名前面需要加 & 来取地址
// 对于数组 a 而言,数组名 a 本身就代表数组的起始地址,相当于 &a[0]
scanf("%s", a); // 读取一个不含空格的字符串,并存入数组 a
// 假设输入的是 Luogu4!
// 存储情况会是 a[0]='L'; a[1]='u'; ... a[6]='!'; a[7]='\0';
// 字符数组里每一个字符都是一个char
// C风格字符串以空字符 '\0' 结尾
// 'c'->'C' ASCII码差32
int len = strlen(a); // 获取字符串的实际长度(不包括末尾的 '\0')
for (int i = 0; i < len; i++) { // 循环遍历字符串中的每一个字符
// a[i] 就是当前要处理的字符
if (a[i]>='a' && a[i]<='z') { // 判断 a[i] 是否为小写字母
// 如果是小写字母,将其转换为大写
// 在ASCII码中,'a' 和 'A' 相差 32,'b' 和 'B' 也相差 32,以此类推
a[i]-=32; // 另一种等效写法是:a[i]=a[i]-'a'+'A';
}
}
printf("%s\n", a); // 输出修改后的整个字符串
return 0;
}
1-based C 风格字符串
参考代码
#include <cstdio>
#include <cstring>
const int N = 105;
char a[N];
int main()
{
// 使用 1-based 索引的 C 风格字符串处理
// a + 1 表示从数组 a 的第二个位置(索引为1)的地址开始接收输入
// 这样做是为了让字符串的第一个字符存储在 a[1],第二个在 a[2],以此类推
scanf("%s", a + 1);
// 假设输入的是 Luogu4!
// 存储情况会是 a[1]='L'; a[2]='u'; ... a[7]='!'; a[8]='\0';
int len = strlen(a + 1); // 从 a[1] 开始计算字符串的长度(直到遇到'\0'为止)
for (int i = 1; i <= len; i++) { // 循环遍历字符串,索引从 1 到 len
// a[i]
if (a[i]>='a' && a[i]<='z') { // 判断 a[i] 是否为小写字母
a[i]-=32; // 如果是,则通过减去ASCII码差值32,将其转换为大写
}
}
printf("%s\n", a + 1); // 同样从 a[1] 的地址开始输出字符串
return 0;
}
C++ 风格字符串
参考代码
#include <iostream>
#include <string> // 引入字符串库,用于 std::string 类
using namespace std;
int main()
{
// C++ 风格字符串处理
string a; // 声明一个 string 类型的变量 a,它会自动管理内存
// 从标准输入读取一个字符串到变量 a
// cin 会自动按空格分割
cin >> a;
// 使用 .size() 成员函数来获取字符串的长度
// a.length() 也有同样的效果
int len = a.size();
for (int i = 0; i < len; i++) { // 循环遍历字符串中的每一个字符,从索引 0 开始
if (a[i]>='a' && a[i]<='z') {
a[i] -= 32;
}
}
cout << a << "\n"; // 使用 cout 输出整个修改后的字符串,并添加一个换行符
return 0;
}
P1914 小书童——凯撒密码
可以将 26 个小写字母 a 到 z 映射到数字 0 到 25,一个小写字母字符变量 ch 对应的数字是 ch - 'a',将这个数字后移 n 位,就变成了 ch - 'a' + n。由于是循环移动,需要对 26 取模,以确保结果仍在 0 到 25 的范围内。所以,移动后的数字是 (ch - 'a' + n) % 26。最后,需要将这个新的数字再转换回字母,即 ( ... ) + 'a'。综合起来,一个小写字母字符变量 ch 经过加密后,就变成了 (ch - 'a' + n) % 26 + 'a'。
参考代码
#include <cstdio>
char psw[55]; // 定义一个字符数组来存储密码,大小55足够(题目要求<=50)
int main()
{
int n;
scanf("%d%s", &n, psw); // 使用 scanf("%d%s", ...) 可以连续读取一个整数和一个字符串
// 遍历字符串中的每个字符
// pws[i] != '\0' 是遍历C风格字符串的经典条件,'\0'是字符串结束标志
for (int i = 0; psw[i] != '\0'; i++) {
// 对当前字符 psw[i] 进行凯撒密码加密
// 1. pws[i] - 'a' 将字符 'a'~'z' 映射到数字 0~25
// 2. + n 加上偏移量
// 3. % 26 取模,实现循环(如 'z' 之后是 'a')
// 4. + 'a' 将数字 0~25 重新映射回字符 'a'~'z'
psw[i] = (psw[i] - 'a' + n) % 26 + 'a';
}
printf("%s\n", psw); // 输出加密后的完整字符串
return 0;
}
P5734 [深基6.例6] 文字处理软件
参考代码
#include <cstdio>
#include <iostream>
#include <string>
using namespace std;
int main()
{
int q; // 存储操作次数
string s; // 存储文档内容
cin >> q >> s; // 读取操作次数 q 和初始字符串 s
for (int i = 0; i < q; i++) { // 循环执行 q 次操作
int op; cin >> op; // 读取操作码 (1, 2, 3 or 4)
if (op == 1) {
// 操作1:在末尾追加字符串
string t;
cin >> t; // 读取要追加的字符串 t
s += t; // 使用 += 运算符将 t 追加到 s 的末尾
cout << s << "\n"; // 输出修改后的字符串
} else if (op == 2) {
// 操作2:截取子串
int a, b; cin >> a >> b; // 读取起始位置 a 和长度 b
s = s.substr(a, b); // 提取从 a 开始,长度为 b 的子串,并用它覆盖原字符串 s
cout << s << "\n"; // 输出修改后的字符串
} else if (op == 3) {
// 操作3:在指定位置插入字符串
int a; string t;
cin >> a >> t; // 读取插入位置 a 和要插入的字符串 t
s.insert(a, t); // 在索引 a 处插入字符串 t
cout << s << "\n"; // 输出修改后的字符串
} else {
// 操作4:查找子串
string t;
cin >> t; // 读取要查找的子串 t
// s.find(t) 返回 t 在 s 中首次出现的索引
// 如果找不到,它返回一个特殊值 string::npos
// 将 string::npos 强制转换为 int 时,通常会得到 -1
int pos = s.find(t);
cout << pos << "\n";
}
}
return 0;
}
习题:P3741 小果的键盘
解题思路
最终的答案要么是字符串原来的 VK 数量,要么是原来的数量加一,因为改变一个字符最多只能创造一个新的 VK 对。因此,算法分为两步:
- 计算基准值:首先,计算并记录字符串在不做任何改变的情况下,已经包含了多少个 VK 子串。
- 寻找改进机会:其次,检查是否存在一种单字符的改动,能够使 VK 的数量增加 1。
- 模式一(KK...):如果字符串以 KK 开头,将第一个 K 改为 V 就会产生一个新的 VK,即 VK...。
- 模式二(...VV):如果字符串以 VV 结尾,将第二个 V 改为 K 就会产生一个新的 VK,即 ...VK。
- 模式三(...XXX...):如果字符串中存在连续三个相同的字符(即 VVV 或 KKK),改变中间的那个字符也能产生一个新的 VK。例如,VVV -> VKV,KKK -> KVK。
参考代码
#include <iostream>
#include <string>
using namespace std;
int main() {
int n;
string s;
cin >> n >> s; // 读取字符串长度 n 和字符串 s
// 1. 统计原字符串中 "VK" 的数量作为基准值
int vk = 0;
for (int i = 0; i <= n - 2; i++) {
if (s[i] == 'V' && s[i+1] == 'K') vk++;
}
// 2. 寻找是否存在通过一次改变就能增加一个 "VK" 的机会
// 找 VVV KKK 开头的KK 结尾的VV
// 0~n-1
// VVV KKK i:1~n-2 s[i-1]==s[i] && s[i]==s[i+1]
// KK i==0 s[i] s[i+1]
// VV i==n-1 s[i] s[i-1]
if (n > 1) {
for (int i = 0; i < n; i++) {
if (i==0 && s[i]=='K' && s[i+1]=='K') {
vk++; break;
}
if (i==n-1 && s[i]=='V' && s[i-1]=='V') {
vk++; break;
}
if (i>=1 && i<=n-2 && s[i-1]==s[i] && s[i]==s[i+1]) {
vk++; break;
}
}
}
cout << vk << "\n";
return 0;
}
P1597 语句解析
参考代码
#include <cstdio>
using namespace std;
char code[300]; // 定义一个足够大的字符数组来存储输入的代码字符串
int a[3]; // a[0] 存变量 a 的值,a[1] 存变量 b 的值,a[2] 存变量 c 的值
// 全局数组默认初始化为0,满足题目“未赋值的变量值为0”的要求
int main()
{
scanf("%s", code); // 读取整行代码字符串
int i = 0; // i 用来遍历代码字符串
while (code[i] != '\0') { // 当 code[i] 不是字符串结束符 '\0' 时,循环继续
if (code[i] >= 'a' && code[i] <= 'c') { // 判断当前字符是否是一个变量名 (a, b, c)
// 如果是,说明找到了一条赋值语句的开头
// 1. 确定被赋值的变量
// 'a'-'a'=0, 'b'-'a'=1, 'c'-'a'=2 映射到数组索引
int cur = code[i] - 'a';
// 2. 确定赋的值
// 赋值语句格式为 "v:=x;",所以值在索引 i+3 的位置
char r = code[i + 3];
// 3. 判断值得类型并执行赋值
if (r >= '0' && r <= '9') {
a[cur] = r - '0'; // 如果 r 是数字字符,将其转换为整数并赋值
} else {
// 如果 r 是字母,说明是变量赋值
// a[r - 'a'] 就是变量 r 当前的值
a[cur] = a[r - 'a'];
}
// 4. 跳过这条处理完的语句
// "v:=x;" 固定占5个字符
i += 5;
} else ++i; // 如果当前字符不是变量名,说明它在语句中间,直接跳过
}
printf("%d %d %d\n", a[0], a[1], a[2]); // 输出 a, b, c 的值
return 0;
}
习题:P5660 [CSP-J 2019] 数字游戏
解题思路
这是一个基础的字符串处理问题,题目的目标非常明确:统计一个固定长度的 01 字符串中 '1' 的个数。只需要遍历字符串中的每一个字符,判断它是否为 '1',并用一个计数器来记录 '1' 出现的次数即可。
参考代码
#include <cstdio>
// 定义一个字符数组,大小为10,足以存放8个字符和字符串结束符'\0'
char s[10];
int main()
{
// 从标准输入读取一个字符串到数组s中
scanf("%s", s);
// 初始化一个整型变量ans,用于累计字符'1'的数量
int ans = 0;
// 循环遍历字符串的前8个字符
for (int i = 0; i < 8; i++) {
// 利用ASCII码的特性进行计数。
// 字符'1'的ASCII码比字符'0'的ASCII码大1。
// 因此,s[i] - '0' 的结果:
// - 如果 s[i] 是 '0', 结果是 0。
// - 如果 s[i] 是 '1', 结果是 1。
// 将这个结果累加到ans中,即可实现对'1'的计数。
ans += (s[i] - '0');
}
// 输出最终的计数结果
printf("%d\n", ans);
return 0; // 程序正常结束
}
P8090 [USACO22JAN] Herdle B
参考代码
#include <cstdio>
#include <algorithm>
using std::min;
// 定义二维字符数组来存储答案和猜测的矩阵
// 使用 1-based 索引,所以大小设为 5×5
char ans[5][5], guess[5][5];
// cnt1:记录答案矩阵中,非绿色匹配的各字母数量
// cnt2:记录猜测矩阵中,非绿色匹配的各字母数量
int cnt1[26], cnt2[26];
int main()
{
// 读取输入
// ans[i] + 1 表示从该行的第二个位置(索引1)开始读取
for (int i = 1; i <= 3; i++) scanf("%s", ans[i] + 1);
for (int i = 1; i <= 3; i++) scanf("%s", guess[i] + 1);
int green = 0, yellow = 0; // 初始化绿色和黄色计数器
// 第一步:计算绿色匹配,并为黄色匹配做准备
// 遍历 3×3 矩阵的每一个位置
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (ans[i][j] == guess[i][j]) { // 如果答案和猜测在 (i,j) 位置的字母相同
green++; // 绿色匹配数加一
} else {
// 如果不相同,则这两个字母可能参与黄色匹配
// 将它们分别计入各自的频率数组
// ans[i][j] - 'A' 将字母 'A'~'Z' 映射到索引 0~25
cnt1[ans[i][j] - 'A']++; cnt2[guess[i][j] - 'A']++;
}
}
}
// 第二步:计算黄色匹配
// 遍历26个英文字母
for (int i = 0; i < 26; i++) {
// 对于每个字母,其黄色匹配的数量取决于它在两个非绿色集合中出现的最小次数
yellow += min(cnt1[i], cnt2[i]);
}
printf("%d\n%d\n", green, yellow); // 输出结果
return 0;
}
P1781 宇宙总统
题目明确指出,票数可能会非常大,达到 100 位数字。这意味着不能使用标准的整型(如 int 或 long long)来存储票数,因为它们会溢出。解决这个问题的最佳方法是使用字符串来存储每个候选人的票数。
当票数以字符串形式存储时,需要一种方法来比较哪个字符串代表的数字更大。先比较长度,如果两个字符串的长度不同,那么长度更长的那个字符串代表的数字更大。例如,"1022356"(7 位)肯定比 "985678"(6 位)大。再比较字典序,如果两个字符串的长度相同,那么可以直接按字典序比较。例如,"987" 的字典序大于 "879"。
参考代码
#include <iostream>
#include <string>
using namespace std;
int main()
{
int n;
cin >> n; // 读取候选人数量
string ans; // 用于存储当前找到的最高票数
int id = 0; // 用于存储最高票数对应的候选人编号
for (int i = 1; i <= n; i++) { // 循环 n 次,处理每个候选人的票数
string s; // 临时变量,用于存储当前读取的票数
cin >> s;
// 将比较逻辑直接内联在 if 语句中
// 条件1:s.size() > ans.size() -> 如果当前票数更长,则它更大
// 条件2:s.size() == ans.size() && s > ans -> 如果长度相等,则按字典序比较
// 使用 ||(或)连接,满足任意一个条件即为真
if (s.size() > ans.size() || (s.size() == ans.size() && s > ans)) {
// 如果当前票数 s 更大,则更新最高票数和对应的编号
ans = s; id = i;
}
}
cout << id << "\n" << ans << "\n"; // 输出最终结果
return 0;
}
P1055 [NOIP2008 普及组] ISBN 号码
参考代码
#include <cstdio>
// 定义一个字符数组 s,用于存储输入的ISBN号码字符串
// 大小为20,足够容纳 "x-xxx-xxxxx-x"(13个字符)加上末尾的空字符 '\0'
char s[20];
int main()
{
scanf("%s", s); // 从标准输入读取一个字符串到数组 s 中
int sum = 0; // 用于累加计算加权和
int num = 0; // 用作乘数,从1递增到9
// 计算校验和
for (int i=0; i<=10; i++) { // 循环遍历字符串的前11个字符(即 "x-xxx-xxxxx" 部分)
if (s[i]>='0' && s[i]<='9') { // 判断当前字符是否是数字 '0' 到 '9'
num++; // 如果是数字,乘数加1
// s[i] - '0' 将数字字符转换为整数值
// 然后乘以对应的乘数 num,并累加到 sum 中
sum += num * (s[i]-'0');
}
}
sum%=11; // 对加权和取模11,得到理论上的识别码(一个0-10的整数)
// s[12](char) sum(int)
// 'X' 10
// '0'~'9' 0~9
// 验证识别码并输出结果
// s[12] 是输入字符串中的识别码字符
// 比较计算得出的 sum 和 s[12] 是否匹配
if (s[12]=='X' && sum==10) printf("Right\n"); // 情况1:正确,输入的识别码是 'X' 且算出的结果是 10
else if (s[12]!='X' && s[12]-'0'==sum) printf("Right\n"); // 情况2:正确,输入的识别码是数字,且其整数值与算出的结果相等
else { // 情况3:错误
if (sum==10) s[12]='X'; // 如果正确的识别码是10,将其字符串的第13个字符修改为 'X'
else s[12]=sum+'0'; // 否则,将正确的识别码(0-9的整数)转换为字符,并修改字符串
printf("%s\n", s); // 输出修正后的整个ISBN字符串
}
return 0;
}
P1125 [NOIP2008 提高组] 笨小猴
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
char s[105]; // 字符数组 s,用于存储输入的单词,长度105足够
int cnt[30]; // 整型数组 cnt,用于统计26个小写字母的出现次数
int main()
{
scanf("%s", s); // 从标准输入读取一个字符串(单词)
// 1. 统计字母频率
for (int i = 0; s[i] != '\0'; i++) { // 遍历输入的字符串 s,直到遇到字符串结束符 '\0'
cnt[s[i] - 'a']++; // s[i] - 'a' 将字符 'a'~'z' 映射到数组索引 0~25
}
// 2. 找出最大和最小频率
int maxn = 0; // 初始化最大频率为0
int minn = 105; // 初始化最小频率为一个比最大可能频率大的数
for (int i = 0; i < 26; ++i) { // 遍历26个字母的频率统计
if (cnt[i] > 0) { // 只考虑在单词中实际出现过的字母(即频率大于0)
maxn = max(maxn, cnt[i]); // 更新最大频率
minn = min(minn, cnt[i]); // 更新最小频率
}
}
// 3. 计算差值并判断是否为质数
int num = maxn - minn; // 计算最大与最小频率之差
int flag = 1; // 质数判断标记,1表示是质数,0表示不是
// 使用试除法判断 num 是否为质数
// 循环从 2 开始,到 sqrt(num) 结束
for (int i = 2; i * i <= num; i++) {
if (num % i == 0) { // 如果 num 能被 i 整除,说明 num 不是质数
flag = 0; // 标记为非质数
break; // 已经确定不是质数,无需继续循环
}
}
if (num == 1 || num == 0) flag = 0; // 处理特殊情况:0 和 1 都不是质数
// 4. 输出结果
if (flag == 1) printf("Lucky Word\n%d\n", num); // 如果 flag 仍为1,说明 num 是质数
else printf("No Answer\n0\n"); // 否则,num 不是质数
return 0;
}
P1308 [NOIP2011 普及组] 统计单词数
这道题要求在一个文章字符串中,查找一个特定单词出现的次数和首次出现的位置。关键点有两个:
- 匹配时不区分大小写。
- 必须是“独立单词”匹配,而不是子串匹配。
为了方便比较,最简单的方法是将目标单词和文章都转换为全小写(或全大写)。
处理“独立单词”匹配是本题的核心难点,如果直接在文章中查找单词,可能会出现子串匹配的情况。例如,在文章 "go together" 中查找单词 "to",直接查找会找到一个位置,但这不是一个独立的单词。一个独立的单词,它的左右两边要么是空格,要么是文章的开头或结尾。可以采用一个非常巧妙的技巧来解决这个问题:在处理的目标单词的前后都加上空格,在处理后的文章的前后也都加上空格。通过这个预处理,任何在原始文章中的独立单词,现在在新的文章字符串中都会变成前后有空格的形式。这样,问题就从复杂的“独立单词匹配”转化为了简单的“子串查找”。
参考代码
#include <iostream>
#include <string>
using namespace std;
int main()
{
string word;
// 1. 读取目标单词和文章,因为可能包含空格,所以使用 getline
getline(cin, word);
// 2. 将目标单词全部转换为小写,以实现大小写不敏感匹配
for (int i = 0; i < word.length(); i++)
if (word[i] >= 'A' && word[i] <= 'Z') word[i] += 'a' - 'A'; // 'a'-'A' 是大小写字母间的ASCII差值
// 3. 在单词前后加上空格,这是处理“独立单词”匹配的关键技巧
word = " " + word + " ";
string s;
getline(cin, s); // 读取文章
// 4. 同样地,将文章全部转换为小写
for (int i = 0; i < s.length(); i++)
if (s[i] >= 'A' && s[i] <= 'Z') s[i] += 'a' - 'A';
// 5. 同样地,在文章前后加上空格
s = " " + s + " ";
// 6. 循环查找
int pos = s.find(word), cnt = 0, ans = 0;
while (pos != -1) {
if (cnt == 0) { // 如果是第一次找到(cnt为0)
ans = pos; // 记录下首次出现的位置
}
++cnt; // 出现次数加一
pos = s.find(word, pos + 1);
}
// 7. 根据查找结果输出
if (cnt > 0) cout << cnt << " " << ans << "\n"; // 如果找到了
else cout << "-1\n"; // 如果没找到
return 0;
}
P1553 数字反转(升级版)
本题输入的“数字”最多可以达到 20 位,解决本题应从字符串角度考虑。
无论是整数部分、小数部分、分子还是分母,反转时都有一个共同的规则:反转后,新的最高位不能是 0(除非这个数本身就是 0)。
参考代码
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
cin >> s;
if (int(s.find('.')) != -1) {
int idx = s.find('.');
int bg = idx - 1;
while (bg >= 0 && s[bg] == '0') --bg; // 找到小数点前最近的第一个非0位置
if (bg < 0) cout << "0";
for (int i = bg; i >= 0; --i) cout << s[i];
cout << ".";
bg = idx + 1;
while (bg < s.length() && s[bg] == '0') ++bg; // 找到小数点后最近的第一个非0位置
if (bg == s.length()) cout << "0";
for (int i = int(s.length()) - 1; i >= bg; --i) cout << s[i];
cout << endl;
} else if (int(s.find('/')) != -1) {
int idx = s.find('/');
int bg = idx - 1;
while (bg >= 0 && s[bg] == '0') --bg;
if (bg < 0) cout << "0";
for (int i = bg; i >= 0; --i) cout << s[i];
cout << "/";
bg = int(s.length()) - 1;
while (bg > idx && s[bg] == '0') --bg;
if (bg == idx) cout << "0";
for (int i = bg; i > idx; --i) cout << s[i];
cout << endl;
} else if (int(s.find('%')) != -1) {
int idx = s.find('%');
int bg = idx - 1;
while (bg >= 0 && s[bg] == '0') --bg;
if (bg < 0) cout << "0";
for (int i = bg; i >= 0; --i) cout << s[i];
cout << "%\n";
} else {
int bg = int(s.length()) - 1;
while (bg >= 0 && s[bg] == '0') --bg;
if (bg < 0) cout << "0";
for (int i = bg; i >= 0; --i) cout << s[i];
cout << endl;
}
return 0;
}
整行字符串读取
在 C++ 中,cin >> s 或 scanf("%s", ...) 默认以空白符(空格、制表符、换行符)作为分隔符,这意味着它们只能读取一个不含空格的“单词”,当需要读取包含空格的完整一行时,可以使用 getline 函数。
getline 是 C++ <string> 库中提供的函数,专门用于从输入流中读取一整行数据。
例题:P5015 [NOIP2018 普及组] 标题统计
题目的输入可能包含空格,标准的 cin >> s; 会在遇到第一个空格时就停止读取。为了读取包含空格的一整行,需要使用 getline(cin, s);。这个函数会从输入流 cin 中读取字符,直到遇到换行符为止,并将读取到的所有字符(包括空格)存入字符串 s 中。
参考代码
#include <iostream>
#include <string> // 引入字符串库,用于 std::string 和 getline
using namespace std;
string s;
int main()
{
// 从标准输入(cin)读取一整行,并将其存入字符串 s
// 这可以正确处理包含空格的输入
getline(cin, s);
int ans = 0; // 初始化计数器 ans 为 0,用于统计有效字符数
for (int i = 0; i < s.size(); i++)
if (s[i] != ' ') ans++;
cout << ans << "\n";
return 0;
}
习题:P1598 垂直柱状图
解题思路
首先,需要一个地方来存储 26 个大写字母('A' 到 'Z')的出现次数,一个大小为 26(或稍大)的整型数组是完美的选择。数组的索引 0 对应 'A',1 对应 'B',以此类推。程序需要读取四行输入,由于每行可能包含空格,所以应该用 getline() 来读取一整行。对读取到的每一行字符串,进行遍历,检查每个字符是否为大写字母。如果是大写字母,就将其对应的计数器加一。重复这个过程,直到四行全部读取和统计完毕。
打印柱状图是一个逆向思维的过程,不是从下往上“画”每个柱子,而是从上到下“扫描”整个画布。首先,需要直到整个图最高有多少行,这取决于出现次数最多的那个字母的频率。从最高行开始,一直打印到最底下一行。对于每一行(假设当前行高为 \(i\)),需要遍历 26 个字母(从 'A' 到 'Z')来决定在每个字母对应的列上打印什么。如果某个字母的频率大于或等于当前行高 \(i\),说明这个字母的柱子“够得着”这一行,就在这一列打印一个星号。否则,就打印一个空格。
题目要求行末不能有多余的空格,一个巧妙的处理方法是:在打印每一行之前,先从右到左找到这一行最后一个需要打印星号的位置。然后,打印循环只进行到那一列就结束,并在最后一个字符后直接打印换行符,从而避免了多余的空格。
当所有星号和空格的行都打印完毕后,最后打印一行 X 轴。
参考代码
#include <cstdio>
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
// 使用一个全局数组来统计每个字母的出现次数
// cnt[0] 对应 'A', cnt[1] 对应 'B', 以此类推
int cnt[30];
int main()
{
// 1. 读取输入并统计频率
// 题目要求读取四行
for (int i = 0; i < 4; ++i) {
string line;
getline(cin, line); // 读取一整行,包含空格
// 遍历该行的每个字符
for (int j = 0; j < line.length(); ++j) {
// 判断是否为大写字母
if (line[j] >= 'A' && line[j] <= 'Z') {
// 如果是,则对应字母的计数加一
// 'C' - 'A' 的结果是 2, 对应 cnt[2]
++cnt[line[j] - 'A'];
}
}
}
// 2. 找到所有字母出现次数的最大值,即柱状图的最大高度
int max_height = 0;
for (int i = 0; i < 26; ++i) {
max_height = max(max_height, cnt[i]);
}
// 3. 从上到下打印柱状图的每一行
// i 代表当前打印的高度层
for (int i = max_height; i >= 1; --i) {
// 为了处理行末空格,先找到这一行最右边需要打印'*'的列
int ed = 25; // ed 代表结束列的索引
for (; ed >= 0; --ed) {
if (cnt[ed] >= i) break; // 从右往左找到第一个高度不小于i的柱子
}
// 从左到右('A'到最右边的'*'列)打印
for (int j = 0; j <= ed; ++j) {
// 如果当前字母的计数(高度)大于等于当前行高i,打印'*',否则打印空格
char to_print = (cnt[j] >= i ? '*' : ' ');
// 如果是该行的最后一列,打印换行符;否则打印空格分隔
printf("%c%c", to_print, j == ed ? '\n' : ' ');
}
}
// 4. 打印最底部的字母行 (X轴)
for (int i = 0; i < 26; ++i) {
// 如果是最后一个字母'Z',打印换行符;否则打印空格分隔
printf("%c%c", 'A' + i, i == 25 ? '\n' : ' ');
}
return 0;
}
getline 与 cin 混用注意事项
当 cin >> 和 getline 混用时,需要特别注意 cin 留下的换行符问题。
int n;
string s;
cin >> n; // 用户输入数字后按下回车,cin 只取走了数字,'\n' 留在了输入缓冲区
getline(cin, s); // getline 立即读到这个'\n',认为一行结束,导致 s 为空
解决方法:在 cin 之后,getline 之前,手动忽略掉这个多余的换行符。
cin >> n;
cin.ignore(); // 忽略掉输入缓冲区中的一个字符(即换行符)
getline(cin, s); // 现在可以正常读取下一行了
与字符串互相 IO
在 C++ 编程中,时常需要在不同数据类型(如整数、浮点数)和字符串之间进行转换。<cstdio> 头文件中提供了两个非常强大且高效的函数:sprintf 和 sscanf,它们可以看做是 printf 和 scanf 的“字符串版本”。
可以将这四个函数类比记忆:
| 函数 | 功能 | 数据流向 |
|---|---|---|
printf |
格式化输出到标准输出(通常是控制台) | 变量 -> 控制台 |
sprintf |
格式化输出到字符串 | 变量 -> 字符串 |
scanf |
从标准输入(通常是键盘)格式化读入 | 控制台 -> 变量 |
sscanf |
从字符串中格式化读入 | 字符串 -> 变量 |
例题:P1957 口算练习题
本题的核心在于处理两种不同的输入格式,并根据要求格式化输出,主要有以下几个关键点:
- 状态记忆:由于省略格式需要沿用上一题的运算类型,必须用一个变量来“记忆”当前的运算类型。
- 格式判断:如何区分一行输入是完整格式还是省略格式?一个有效的方法是读取该行的第一个“词”(以空格分隔的部分),然后检查它的首字符。如果首字符是字母(
a,b,c),则是完整格式;如果是数字,则是省略格式。 - 字符串到整数的转换:当判断为省略格式时,已经将第一个运算数作为字符串读入。需要一种方法将这个字符串转换回整数,以便进行计算,
sscanf函数非常适合这个任务。 - 整数到字符串的转换:在计算出结果后,需要将整个算式(例如:整数
64、字符+、整数46、字符=、整数110)拼接成一个完整的字符串,sprintf函数可以方便地实现这一点。 - 字符串长度计算:在生成算式字符串后,需要计算其长度,
strlen函数可以完成这个任务。
参考代码
#include <cstdio>
#include <cstring>
char s[10]; // s:用于读取每行开头的第一个词(可能是操作符或第一个数字)
char ans[30]; // ans:用于存储格式化后的完整算式字符串
int main()
{
int i;
scanf("%d", &i); // 读取总的题目数量
// op:用于存储当前运算类型,1 加,2 减,3 乘
int op = 0;
while (i--) { // 循环 i 次,处理每一道题
int a, b; // a 和 b 是两个运算数
scanf("%s", s); // 读取行首的第一个词
// 判断输入格式:通过第一个词的首字符来判断
if (s[0] >= 'a') {
// 如果首字符是字母,说明是“操作符 数字 数字”格式
// 读取两个运算数
scanf("%d%d", &a, &b);
op = s[0] - 'a' + 1; // 更新运算类型:'a'->1, 'b'->2, 'c'->3
} else {
// 如果首字符是数字,说明是“数字 数字”格式,沿用上一次的运算类型
// 使用 sscanf 从刚刚读入的字符串 s 中解析出第一个数字 a
sscanf(s, "%d", &a);
scanf("%d", &b); // 读取第二个数字 b
}
// 根据运算类型 op,进行计算并使用 sprintf 格式化输出字符串
if (op == 1) sprintf(ans, "%d+%d=%d", a, b, a + b); // 加法
if (op == 2) sprintf(ans, "%d-%d=%d", a, b, a - b); // 减法
if (op == 3) sprintf(ans, "%d*%d=%d", a, b, a * b); // 乘法
// 打印格式化后的字符串
// 使用 strlen 计算并打印该字符串的总长度
printf("%s\n%d\n", ans, strlen(ans));
}
return 0;
}

浙公网安备 33010602011771号