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 对。因此,算法分为两步:

  1. 计算基准值:首先,计算并记录字符串在不做任何改变的情况下,已经包含了多少个 VK 子串。
  2. 寻找改进机会:其次,检查是否存在一种单字符的改动,能够使 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 位数字。这意味着不能使用标准的整型(如 intlong 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 普及组] 统计单词数

这道题要求在一个文章字符串中,查找一个特定单词出现的次数和首次出现的位置。关键点有两个:

  1. 匹配时不区分大小写。
  2. 必须是“独立单词”匹配,而不是子串匹配。

为了方便比较,最简单的方法是将目标单词和文章都转换为全小写(或全大写)。

处理“独立单词”匹配是本题的核心难点,如果直接在文章中查找单词,可能会出现子串匹配的情况。例如,在文章 "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 >> sscanf("%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;
}

getlinecin 混用注意事项

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> 头文件中提供了两个非常强大且高效的函数:sprintfsscanf,它们可以看做是 printfscanf 的“字符串版本”。

可以将这四个函数类比记忆:

函数 功能 数据流向
printf 格式化输出标准输出(通常是控制台) 变量 -> 控制台
sprintf 格式化输出字符串 变量 -> 字符串
scanf 标准输入(通常是键盘)格式化读入 控制台 -> 变量
sscanf 字符串格式化读入 字符串 -> 变量

例题:P1957 口算练习题

本题的核心在于处理两种不同的输入格式,并根据要求格式化输出,主要有以下几个关键点:

  1. 状态记忆:由于省略格式需要沿用上一题的运算类型,必须用一个变量来“记忆”当前的运算类型。
  2. 格式判断:如何区分一行输入是完整格式还是省略格式?一个有效的方法是读取该行的第一个“词”(以空格分隔的部分),然后检查它的首字符。如果首字符是字母(a, b, c),则是完整格式;如果是数字,则是省略格式。
  3. 字符串到整数的转换:当判断为省略格式时,已经将第一个运算数作为字符串读入。需要一种方法将这个字符串转换回整数,以便进行计算,sscanf 函数非常适合这个任务。
  4. 整数到字符串的转换:在计算出结果后,需要将整个算式(例如:整数 64、字符 +、整数 46、字符 =、整数 110)拼接成一个完整的字符串,sprintf 函数可以方便地实现这一点。
  5. 字符串长度计算:在生成算式字符串后,需要计算其长度,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;
}
posted @ 2023-07-25 12:59  RonChen  阅读(97)  评论(0)    收藏  举报