数字不只是拿来算的,它还是一串字符 —— 学会「按字符思考」,你就能处理小数、分数、百分数和任意进制
| 0 - 25 min | 数字 ⇄ 字符串的两套工具(第 1 节) |
| 25 - 50 min | 数字反转的三个细节(第 2 节)——今天的主线 |
| 50 - 75 min | 进制转换的两种方向(第 3 节) |
| 75 - 160 min | 做 4 道题,每题限时 30 分钟 |
| 160 - 180 min | 对照本文题解补错题 + 做自测 |
Day 8 你学会了「把一串字符当成数组来用」:s[i]、s.size()、toupper、凯撒密码的取模移位。
今天要往前一步:不再把输入死板地当成「一个整数」或「一个字符串」,而是根据题目的需要自由切换。
比如 P1553 的输入可能长成 600.084、700/27、8670% —— 你根本没法用一个 int 读进来,
只能先当字符串读,再看里面有什么符号,分情况处理。
同一个数字,在你脑子里的身份可以完全不一样:
| 你关心的是 | 怎么存 | 典型题目 |
|---|---|---|
| 它的大小:加、减、比大小、求和 | int / long long | 求平均值、找最大值 |
| 它的每一位长什么样:反转、回文、数数码 | string | P1307、P1553 |
| 它里面混着别的符号:小数点、斜杠、百分号 | string(没得选) | P1553 |
只要题目在描述「数位的形状」(第几位是谁、顺序反过来、末尾有没有 0、几位数),就把它当字符串处理。
因为对一个 int 来说,380 就是三百八十这一个数值;
但对字符串 "380" 来说,它是 '3'、'8'、'0' 三个独立的字符。反转、去零这些操作,本质是在字符上做手脚。
| 想干什么 | 写法 | 需要 include | 例子 |
|---|---|---|---|
| 整数 → 字符串 | to_string(x) | <string> | to_string(123) = "123" |
| 字符串 → 整数 | stoi(s) | <string> | stoi("123") = 123 |
| 字符串 → 长整数 | stoll(s) | <string> | stoll("1000000000000") |
| 字符串 → 小数 | stod(s) | <string> | stod("3.14") = 3.14 |
| 取长度 | s.size() | — | "380".size() = 3 |
| 反转整串 | reverse(s.begin(), s.end()) | <algorithm> | "abc" → "cba" |
如果要处理的是纯整数,而且只在乎「数位」,那么用 % 10 和 / 10 手写反而更快、更省内存。
但今天的重头戏 P1553 里全是符号,字符串才是正解。
cin >> s 读字符串,别用 getline(今天这 4 题都不用)
cin >> s 会跳过开头的空白,一直读到空白为止 —— 正好适合读 600.084 这种「一整块没有空格的输入」。
而 getline(cin, s) 会把整行连空格一起读进来。Day 8 的 P5015 标题统计、P1765 手机,才需要它(因为里面有空格)。
今天 4 道题的输入都没有空格,所以一律 cin >> s 最省事。
| 类型 | 能存多大 | 够用吗 |
|---|---|---|
int | 约 ±2.1 × 10⁹,最多 10 位数 | P1307(|N| ≤ 10⁹)刚好够 |
long long | 约 ±9.2 × 10¹⁸,最多 19 位数 | P1553 的 20 位整数 ❌ 装不下 |
string | 想多长有多长(内存允许的话) | P1553 ✅ |
题面写着:整数部分「不大于 20 位」,百分数分子「不大于 19 位」。
20 位的整数,比如 12345678901234567890,已经远远超过 long long 的上限(9223372036854775807,也才 19 位)。
你用 long long 读它就爆了 —— 所以只能当字符串,逐位处理。
看到「位数不超过 xx 位」这种描述,就先警惕一下:是不是超过了 19 位?超了就必须用字符串。
这是今天的主线。P1307 是它的入门版,P1553 是加了三种「格式」的完整版。
反转 1200,机械地倒过来是 0021,但题目要的是 21。
好消息是:用算术方法写,前导 0 会自动消失,你什么都不用做。
rev = rev * 10 + n % 10; // ★ 关键的一行
为什么?因为构造新数的时候,如果这一次拿到的是 0,那么 rev * 10 + 0 等于把 rev 后面「补一个 0」。
但如果此时 rev 本身还是 0,那么 0 * 10 + 0 仍然是 0 —— 0 乘任何数都是 0,前导 0 就这么被吃掉了。
| 轮次 | n | n % 10 | rev(新数) |
|---|---|---|---|
| 开始 | 1200 | — | 0 |
| 1 | 1200 | 0 | 0×10+0 = 0 |
| 2 | 120 | 0 | 0×10+0 = 0 |
| 3 | 12 | 2 | 0×10+2 = 2 |
| 4 | 1 | 1 | 2×10+1 = 21 |
| 结束 | 0 | — | 21 ✅ |
看第 1、2 轮:rev 一直是 0,两个 0 就这么自然地被丢掉了。
反转 -380,负号要留着,但 0 要去掉(答案是 -83)。
最简单的写法:先把符号单独记下来,把 n 变成正数,按正数反转,最后再把符号补回去。
bool neg = (n < 0); // 1. 记住是不是负数 if(neg) n = -n; // 2. 变成正数再处理 // ……中间是反转,不管符号…… if(neg) rev = -rev; // 3. 符号补回来
因为负数取余的结果在 C++ 里是负数:-380 % 10 == 0、-38 % 10 == -8。
直接算下去,中间会出现一堆负的位数,非常容易写错。
把符号「摘出去」,处理完再「装回去」—— 这个思路在处理正负号、小数点、百分号时都通用。
P1307 的范围是 |N| ≤ 10⁹,反转后的数一定也在 int 范围内(因为 10 位数只可能是 1000000000 本身,反转后是 1)。
但 P1553 就完全不一样了,所以养成习惯:数字反转一律用 long long,省得算半天范围。
long long rev = 0;
while(n > 0){
rev = rev * 10 + n % 10; // ★ 取末位接到新数末尾
n /= 10; // 丢掉末位
}
// 循环结束时 n 被吃成 0,rev 就是反转后的数
如果输入是 0,while(n > 0) 直接跳过,rev 保持 0 —— 输出 0,答案正好是对的。
P1307 允许 N = 0,这个「巧合」帮了你。但如果题目要求反转后去掉前导零、且 0 要特殊处理,你就得留意了。
把十进制的 13 转成二进制:不停地除以 2,把余数记下来,最后把余数倒着读。
| 步骤 | 除以 2 的商 | 余数 |
|---|---|---|
| 13 ÷ 2 | 6 | 1 |
| 6 ÷ 2 | 3 | 0 |
| 3 ÷ 2 | 1 | 1 |
| 1 ÷ 2 | 0 | 1 |
余数依次是 1、0、1、1,倒过来读就是 1101 —— 也就是 (13)10 = (1101)2。
ans = ans * M + 这一位的数字,从最高位往最低位走一遍。
比如把 (1101)2 转回十进制:
| 读到的位 | 计算 | ans |
|---|---|---|
| 开始 | — | 0 |
| 1 | 0×2+1 | 1 |
| 1 | 1×2+1 | 3 |
| 0 | 3×2+0 | 6 |
| 1 | 6×2+1 | 13 ✅ |
为什么对?拿 1101 展开看看:1×2³ + 1×2² + 0×2¹ + 1×2⁰。
每读一位就 ×2,相当于把前面所有已经读过的位的权重整体抬高一级 —— 跟你手算时从高位往下推是一模一样的思路。
这个方法叫「秦九韶算法」(也叫霍纳法则),后面处理超大整数、进制转换都会用到。
注意 P2084 只要输出形如 1*2^4+0*2^2+1*2^0 的式子,
并不需要真的算出十进制结果,也不需要去做除法。它其实是「按位展开」+ 格式控制。
设字符串长度为 len,第 i 个字符(从 0 开始数)的权重就是 len - 1 - i:
n = "10101" len = 5 i=0 '1' 权重 5-1-0 = 4 → 1*2^4 i=1 '0' 权重 3 → 跳过(系数为 0 不输出) i=2 '1' 权重 2 → 1*2^2 i=3 '0' 权重 1 → 跳过 i=4 '1' 权重 0 → 1*2^0 连接起来: 1*2^4+1*2^2+1*2^0
如果每输出一项就补一个 +,你最后会得到 1*2^4+1*2^2+1*2^0+ —— 多一个加号,WA。
正确做法是用一个 bool first 标记:「不是第一项,才在前面补加号」。
这个「第一项不补分隔符」的技巧,输出用空格隔开的题目里也一样用(今天的 P5736、P5737、昨天的 P1554 都是这个套路)。
P1553 是典型的「细节题」—— 算法一点不难,难的是把每种情况都想全。这类题有个万能套路。
| 找到什么 | 说明这是 | 怎么处理 |
|---|---|---|
末尾有 % | 百分数 | 反转前面那段数字,% 原地不动 |
里面有 . | 小数 | 小数点前、后两段各自反转,. 不动 |
里面有 / | 分数 | 分子、分母各自反转,/ 不动 |
| 都没有 | 整数 | 整段反转 |
题面反复强调「不交换整数部分与小数部分」「不交换分子与分母」——
意思是 600.084 处理完还是 6.48(左边还是整数部分),
700/27 处理完是 7/72(左边还是分子)。
你只需要把标志符号切开的两块,各自内部反转,符号留在原地。
反转后会多出 0,但整数部分要去的是前导 0,小数部分要去的是末尾 0 —— 方向正好相反!
| 处理哪一段 | 去哪种 0 | 例子 |
|---|---|---|
| 整数部分、分子、分母、百分数 | 去掉前导 0 | "007" → "7" |
| 小数部分 | 去掉末尾 0 | "480" → "48" |
string stripLeft(string s){ // 去前导 0,全 0 时保留一个 "0"
int i = 0;
while(i < (int)s.size() - 1 && s[i] == '0') i++;
return s.substr(i);
}
string stripRight(string s){ // 去末尾 0,全 0 时保留一个 "0"
int j = (int)s.size() - 1;
while(j > 0 && s[j] == '0') j--;
return s.substr(0, j + 1);
}
-1 和 > 0 是干什么的
是防止把整个串删空。如果输入是 0.0,反转后加起来全是 0:
· stripLeft("0"):循环条件 i < size()-1 保证至少留下 1 个字符 → 返回 "0" ✅
· stripRight("0"):循环条件 j > 0 保证至少留下 1 个字符 → 返回 "0" ✅
如果不加这个保护,就会返回空字符串,输出 . 这种见不得人的东西。
字符串的 find 和 substr 就是干这个的:
int p = s.find('.'); // 找到小数点的位置
string left = s.substr(0, p); // 从 0 开始,长度 p:整数部分
string right = s.substr(p + 1); // 从 p+1 开始到末尾:小数部分
substr(起点, 长度) 的第二个参数是长度,不是终点
s.substr(0, p) 意思是「从下标 0 开始,取 p 个字符」——正好是小数点前面那一段。
忘了第二个参数(s.substr(p+1))就表示「从 p+1 一直取到末尾」,这也是合法的写法。
下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译、实际运行过,输出是你看到的真实结果。
题意:给一个整数 N(可能是负数),把它的各位数字顺序倒过来组成新数。 新数必须是「正常的整数写法」:除非原数就是 0,否则反转后最高位不能是 0 —— 说白了就是要把末尾的 0 丢掉。
−1,000,000,000 ≤ N ≤ 1,000,000,000(包含负数和 0)。
· N 的绝对值最大 10⁹,int 装得下,但用 long long 更保险;
· 必须处理负号——样例 2 就是负数;
· 必须处理 0——输入 0 时输出也得是 0。
三步走,正好对应第 2 节的三个细节:
① 摘符号:用 bool neg 记住是不是负数,然后把 n 取绝对值变成正数。
② 反转:rev = rev * 10 + n % 10; n /= 10; 循环到 n == 0。
这个写法会自动丢掉末尾的 0(前导 0 被 0 * 10 吃掉)。
③ 补符号:如果原来是负数,把 rev 取负再输出。
123
321
-380
-83
样例 2 的 -380 里,那个 0 就是「反转后变成前导 0」的那个 0 —— 必须丢掉,所以是 -83 而不是 -083。
#include <iostream>
using namespace std;
int main(){
long long n;
cin >> n;
bool neg = (n < 0); // 先记住符号,把负数变成正数再处理
if(neg) n = -n;
long long rev = 0;
while(n > 0){
rev = rev * 10 + n % 10; // 取末位,接到新数的末尾
n /= 10; // 丢掉末位
}
if(neg) rev = -rev;
cout << rev << endl;
return 0;
}
官方样例 1 123 → 321 完全一致
官方样例 2 -380 → -83 完全一致
额外边界测试(都是真实跑出来的):
· 0 → 0(while 不进循环,rev 保持 0)✅
· 1000000000 → 1(9 个 0 全被丢掉)✅
· -1000000000 → -1 ✅
· 999999999 → 999999999(反转后不变)✅
n % 10 会出负数,结果一团乱。一定要先摘符号。rev * 10 + 0 在 rev 还是 0 的时候等于 0,前导 0 自动没了。
如果你自己再写一段去零代码,反而容易写错。string 反转也行(reverse + 去前导 0),但那样要处理负号字符,比算术法麻烦。这题用算术法最省事。cout << rev << endl;。题意:还是数字反转,但这次的「数」有四种长相:整数、小数、分数、百分数。 规则是:每种格式的各个数字块内部各自反转,格式符号(小数点、斜杠、百分号)的位置完全不动。
| 输入 | 是什么 | 怎么反转 | 输出 |
|---|---|---|---|
| 5087462 | 整数 | 整串反转,去掉前导 0 | 2647805 |
| 600.084 | 小数 | 整数部分、小数部分各自反转 (小数部分还要去末尾 0) | 6.48 |
| 700/27 | 分数 | 分子、分母各自反转(不约分、不交换) | 7/72 |
| 8670% | 百分数 | 只反转数字部分 | 768% |
· 整数:不大于 20 位
· 小数:整数部分和小数部分各不大于 10 位
· 分数:分子和分母各不大于 10 位
· 百分数:分子不大于 19 位
· 数据不存在负数
20 位的整数超过 long long(最多 19 位),所以这题必须用字符串按位处理,不能读成整数再算。
另外题面保证:小数部分「末尾没有多余的 0」(是正常小数写法),分母不为 0,分数不约分。 也就是说 输入是「规范」的,你只需要负责把输出的 0 处理干净。
① 读成字符串,然后看它里面有什么符号,分成四类:
末尾是 '%' → 百分数 其中有 '.' → 小数 其中有 '/' → 分数 都没有 → 整数
注意判断顺序:先判 %,因为百分数里只会出现 %,不会同时出现别的符号。
然后判 . 和 /(题面保证不会同时出现)。
② 写两个去零函数。这是本题最容易出错的地方:
· 整数部分反转后可能带前导 0(600 → 006 → 要变成 6)→ 用 stripLeft
· 小数部分反转后可能带末尾 0(084 → 480 → 要变成 48)→ 用 stripRight
方向是反的!记混了这题就全错。为什么方向不同?因为小数点前是「整数」,整数不能有前导 0; 小数点后是「小数」,小数不能有多余的末尾 0 —— 它们各自遵守自己那一侧的书写规范。
③ 每种情况拼回去:
| 情况 | 输出怎么拼 |
|---|---|
| 整数 | revInt(整串) |
| 小数 | revInt(小数点前) + "." + revDec(小数点后) |
| 分数 | revInt(斜杠前) + "/" + revInt(斜杠后) |
| 百分数 | revInt(百分号前) + "%" |
注意分数和百分数的两段都用 revInt(去前导 0),只有小数的后半段用 revDec(去末尾 0)。
因为题目说「分数与整数翻转相关规定见上」「百分数见与整数翻转相关内容」—— 也就是说它们都按整数规矩来。
5087462
2647805
600.084
6.48
700/27
7/72
8670%
768%
#include <iostream> #include <string> #include <algorithm> using namespace std; // 去掉前导 0;如果全是 0,就保留一个 "0" string stripLeft(string s){ int i = 0; while(i < (int)s.size() - 1 && s[i] == '0') i++; return s.substr(i); } // 去掉末尾 0;如果全是 0,就保留一个 "0" string stripRight(string s){ int j = (int)s.size() - 1; while(j > 0 && s[j] == '0') j--; return s.substr(0, j + 1); } // 整数部分的反转:整体反转 + 去掉前导 0 string revInt(string s){ reverse(s.begin(), s.end()); return stripLeft(s); } // 小数部分的反转:整体反转 + 去掉末尾 0 string revDec(string s){ reverse(s.begin(), s.end()); return stripRight(s); } int main(){ string s; cin >> s; int n = s.size(); if(s[n - 1] == '%'){ // 百分数:只反转数字部分,% 留在最后 cout << revInt(s.substr(0, n - 1)) << "%" << endl; } else if(s.find('.') != string::npos){ // 小数:整数部分、小数部分各自反转,小数点不动 int p = s.find('.'); cout << revInt(s.substr(0, p)) << "." << revDec(s.substr(p + 1)) << endl; } else if(s.find('/') != string::npos){ // 分数:分子、分母各自反转,斜杠不动 int p = s.find('/'); cout << revInt(s.substr(0, p)) << "/" << revInt(s.substr(p + 1)) << endl; } else{ // 整数 cout << revInt(s) << endl; } return 0; }
四个官方样例全部通过,逐字节一致(实际运行输出):
· 5087462 → 2647805 一致
· 600.084 → 6.48 一致
· 700/27 → 7/72 一致
· 8670% → 768% 一致
另外我把每种情况的边界都跑了一遍(真实输出):
| 输入 | 输出 | 考察点 |
|---|---|---|
| 0 | 0 | 整数 0 |
| 1000 | 1 | 末尾三个 0 全丢 |
| 0.0 | 0.0 | 小数的两段都是 0,不能被删空 |
| 0.5 | 0.5 | 整数部分是 0 |
| 230.0 | 32.0 | 整数部分去前导 0,小数部分保留 0 |
| 1.001 | 1.1 | 小数部分去末尾 0(100 → 1) |
| 100.001 | 1.1 | 两段同时去各自方向的 0 |
| 0/7 | 0/7 | 分子是 0 |
| 1200/30 | 21/3 | 分子分母都去前导 0 |
| 120% | 21% | 百分数去前导 0 |
| 1000000000000000000% | 1% | 19 位,验证 long long 装不下时照样正确 |
| 1234567890.1234567890 | 987654321.0987654321 | 两段各 10 位,注意小数部分保留前导 0 |
最后一行特别值得看:小数部分 1234567890 反转成 0987654321,
这里的 0 必须留着(它在小数部分,属于合法数字),如果错用了去前导 0 的函数就会变成 987654321.987654321,答案就错了。
long long 读输入 → 20 位整数直接溢出。必须当字符串读。stripLeft 会把 .0987654321 变成 .987654321,错。
记住口诀:点前点后都反转,点前去头、点后去尾。while(s[i]=='0') i++; 遇到 "0" 会返回空串。
必须加 i < size()-1 的保护。s[n-1] == '%'),否则可能被别的分支抢走。1200/30 就是 21/3,不要自作聪明化简成 7/1。题意:把彗星名和小组名各转换成一个数字:每个字母按 A=1、B=2 …… Z=26 换成分值,然后全部乘起来。
如果两个数字除以 47 的余数相同,输出 GO,否则输出 STAY。
两个名字都是不超过 6 个字符的大写字母串(输入格式写的是「长度为 1 到 6」),没有空格和标点。
· 字母最多 6 个,最大乘积是 26⁶ = 308,915,776,比 int 上限(约 2.1 × 10⁹)小,所以这题其实不会溢出;
· 但养成习惯:乘积类题目一律边乘边取模,这样无论字母多少都不会溢出。
① 字母 → 分值:s[i] - 'A' + 1。因为 'A' - 'A' = 0,加 1 就是 1;'Z' - 'A' = 25,加 1 就是 26。✅
② 边乘边取模:
res = res * (s[i] - 'A' + 1) % 47;
因为「取余」和「乘法」可以换位置:(a × b) mod m == ((a mod m) × (b mod m)) mod m。
直觉上想:一个数除以 47 的余数,只由它「去掉若干个 47 之后剩下的部分」决定,
再乘上下一个数,多出来的那些 47 的倍数还是 47 的倍数,不影响余数。
所以每乘一次就取一次模,结果和全乘完再取模完全相同,但数字永远不会变大。
③ 比一下余数,相等输出 GO,不等输出 STAY。
COMETQ HVNGAT
GO
ABSTAR USACO
STAY
样例 1 手算核对:COMETQ = 3×15×13×5×20×17 = 994500,994500 mod 47 = 27;
HVNGAT = 8×22×14×7×1×20 = 344960,344960 mod 47 = 27。相等 → GO ✅
样例 2:ABSTAR = 13680,13680 mod 47 = 3;USACO = 17955,17955 mod 47 = 1。不相等 → STAY ✅
#include <iostream> #include <string> using namespace std; // 把一个名字转换成数字:所有字母的积,A=1, Z=26 // 边乘边取模,避免数字变大(取模可以和乘法交换顺序) int nameToNumber(string s){ int res = 1; for(int i = 0; i < (int)s.size(); i++){ res = res * (s[i] - 'A' + 1) % 47; } return res; } int main(){ string comet, group; cin >> comet >> group; // 第 1 行彗星名,第 2 行小组名 if(nameToNumber(comet) == nameToNumber(group)) cout << "GO" << endl; else cout << "STAY" << endl; return 0; }
官方样例 1 COMETQ / HVNGAT → GO 完全一致
官方样例 2 ABSTAR / USACO → STAY 完全一致
编译时 -Wall 没有任何警告。cin >> comet >> group 能自动跨行读取(第一行读完读到行末空白就停下,接着读第二行),所以两行输入用一次 cin 就能搞定。
s[i] - 'A':这样 A 会变成 0,整串乘积变成 0,全错。别忘了 + 1。s[i] - 'a' + 1:输入是大写字母,用小写公式会得到负数('A'-'a' = -32)。看准大小写。res 要初始化为 1:乘积的初始值必须是 1,写成 0 的话结果永远是 0。GO / STAY 全大写,不是 Go、go。输出字符串要一个字母一个字母对着题面抄。题意:给一个 M 进制(1 < M < 10)的数 N,
把它按「十进制展开式」的样子打印出来:每位的数字 × M 的该位次幂,系数为 0 的项要省略,项与项之间用 + 连接。
注意:题目只要打印这个式子,不要你真的算出十进制结果,也不要输出等号。
1 < M < 10,N 的位数不超过 1000,且 M、N 均为正整数。
· 1000 位!这又是一个「必须用字符串」的信号 —— 别说 long long,什么整数类型都装不下 1000 位;
· M 只有 2~9,所以每一位就是一个数字字符,不会出现 A、B 这种字母;
· 用字符串存 N,读取长度就是位数,幂次 = 位数 − 1 − 当前下标。
① 用 string 存 N,因为可能有 1000 位。
② 从左往右扫每一位,设字符串长度为 len,下标为 i(从 0 开始),那么这一位的幂次是:
幂次 = len - 1 - i
比如 "10101" 长度 5:下标 0 是最高位,幂次 4;下标 4 是最低位,幂次 0。✅
③ 系数是 0 就跳过:if(n[i] == '0') continue;。题目明确说「当系数为 0 时,该单项式要省略」。
④ 处理加号:用一个 bool first 标记。是第一个要输出的项,前面不加 +;否则先输出一个 +。
以样例 2 10101 为例,走一遍:
| i | n[i] | 幂次 | 动作 | 已输出 |
|---|---|---|---|---|
| 0 | '1' | 4 | first,不加 +,输出 1*2^4 | 1*2^4 |
| 1 | '0' | 3 | 系数为 0,跳过 | 1*2^4 |
| 2 | '1' | 2 | 先加 +,再输出 1*2^2 | 1*2^4+1*2^2 |
| 3 | '0' | 1 | 跳过 | 1*2^4+1*2^2 |
| 4 | '1' | 0 | 先加 +,再输出 1*2^0 | 1*2^4+1*2^2+1*2^0 |
2 10101
1*2^4+1*2^2+1*2^0
#include <iostream>
#include <string>
using namespace std;
int main(){
int m; // 进制
string n; // 位数可能到 1000 位,必须用字符串存
cin >> m >> n;
int len = n.size();
bool first = true; // 是不是第一个要输出的单项式
for(int i = 0; i < len; i++){
if(n[i] == '0') continue; // 系数为 0,整项省略
if(!first) cout << "+"; // 不是第一项,前面补一个加号
first = false;
cout << n[i] << "*" << m << "^" << (len - 1 - i);
}
if(first) cout << 0; // 保险:万一这个数全是 0(题面数据不会出现)
cout << endl;
return 0;
}
官方样例 2 10101 → 1*2^4+1*2^2+1*2^0 完全一致
另外几组真实输出:
| 输入 | 输出 |
|---|---|
| 2 100000 | 1*2^5(只有一项,前面没有多余的 +) |
| 3 1020102 | 1*3^6+2*3^4+1*3^2+2*3^0(0 的位置被跳过,幂次仍然连续不重复) |
| 9 999999 | 9*9^5+9*9^4+9*9^3+9*9^2+9*9^1+9*9^0(仔细看最后一项,9^0 也要打出来) |
string n。len - 1 - i,不是 i,也不是 len - i。
最高位是 len-1 次,最低位是 0 次 —— 一定要在心里验算一个 5 位的例子。+。要用 first 标记,加号加在项前面。0*2^3 这种,格式错。题面明确要求省略。endl 结尾最稳妥。| 错误写法 | 后果 | 正确写法 |
|---|---|---|
| long long n; cin >> n; 处理 20 位的数 | 溢出,读进来的值完全不对,全 WA | string s; cin >> s; 按位处理 |
| 反转负数时不摘符号,直接 while(n > 0) | n 是负数,循环一次都不执行,输出 0 | bool neg = n < 0; if(neg) n = -n; |
| 小数部分用了去前导 0 的函数 | .0987654321 变成 .987654321,丢了一位有效数字 | 点前去头、点后去尾 |
| 去零函数写成 while(s[i]=='0') i++; | 输入 "0" 时返回空串,输出只剩一个 "." 或 "%" | 加保护:i < s.size() - 1 |
| 边输出边加 "+",最后不撤销 | 式子末尾多一个加号,格式错 | 用 bool first,加号加在项前面 |
| P2084 用 int 读 N | 1000 位的数,直接废掉 | string n,幂次 = len-1-i |
| s[i] - 'A' 当字母分值 | A 变成 0,乘积全变 0 | s[i] - 'A' + 1 |
| 乘积变量初始化成 0 | 乘出来永远是 0 | int res = 1; |
答:501。
过程:rev = 0×10+0 = 0 → rev = 0×10+5 = 5 → rev = 5×10+0 = 50 → rev = 50×10+1 = 501。 第一个 0(n 的末位)被吃掉了,但第二个 0 已经是 rev 的末位,会被保留 —— 这是正确的,因为 501 的 0 在中间,不属于「最高位」。
3.140 反转后应该输出什么?用错函数会怎样?答:小数部分 140 反转成 041,去掉末尾 0 还是 041……等等,这里要注意!
题面保证输入不会有「末尾多余的 0」,所以 3.140 这种输入不会出现(是 3.14)。
真正会遇到的是:小数部分反转后产生了末尾 0,比如 600.084 → 084 反转成 480,
末尾的 0 要去掉 → 48,答案是 6.48。
如果错用了去前导 0 的函数:480 的前导不是 0,所以原样输出 480,答案是 6.480 —— 错。
答:因为 (a × b) mod m = ((a mod m) × (b mod m)) mod m。
先取模只是「提前把多余的 47 的倍数扔掉」,剩下的余数再乘下一个数,扔掉的倍数部分仍然是 47 的倍数,不会影响最终余数。 这样做的好处是数字永远很小(小于 26 × 47),绝对不会溢出。
long long reverseNumber(long long n){
if(n == 0) return 0; // 0 单独处理
bool neg = (n < 0);
if(neg) n = -n;
string s = to_string(n); // 数字 → 字符串
reverse(s.begin(), s.end()); // 反转
int i = 0;
while(i < (int)s.size() - 1 && s[i] == '0') i++; // 去前导 0
long long res = stoll(s.substr(i)); // 字符串 → 数字
return neg ? -res : res;
}
★ 这个版本顺带演示了今天的转换函数:to_string 和 stoll。注意它不能处理 20 位的数(会溢出),
所以 P1553 里我们全程用字符串,最后也没转回数字 —— 能不转就不转,是最安全的选择。
【数字 ⇄ 字符串】 to_string(x) // 整数 → 字符串 stoi(s) / stoll(s) // 字符串 → 整数 / 长整数 stod(s) // 字符串 → 小数 reverse(s.begin(), s.end()) // 反转整串(需 <algorithm>) // 判断:位数超过 19 位 → 必须用 string 【数字反转(算术法)】 bool neg = (n < 0); if(neg) n = -n; long long rev = 0; while(n > 0){ rev = rev * 10 + n % 10; // 前导 0 自动消失 n /= 10; } if(neg) rev = -rev; 【去零两兄弟(字符串法)】 stripLeft : 去前导 0,全 0 保留一个 "0" while(i < size()-1 && s[i]=='0') i++; stripRight: 去末尾 0,全 0 保留一个 "0" while(j > 0 && s[j]=='0') j--; // 整数/分子/分母/百分数 → stripLeft // 小数部分 → stripRight 【按符号分类(格式敏感题)】 s[n-1] == '%' → 百分数 s.find('.') != npos → 小数 s.find('/') != npos → 分数 都不是 → 整数 【分段取子串】 int p = s.find('.'); s.substr(0, p) // 前一段(长度 p) s.substr(p + 1) // 后一段(到末尾) 【进制转换】 十进制 → M 进制:反复 %M 取余、/M,余数倒序读 M 进制 → 十进制:ans = ans * M + 该位数字 // P2084 只要打式子:幂次 = len - 1 - i 【输出分隔符】 bool first = true; for(...){ if(!first) cout << "+"; // 或 " " first = false; ...输出内容... }
函数:把代码写成模块
今天其实已经在不知不觉地「用函数」了 —— P1553 里的 stripLeft、stripRight、revInt、revDec
都是我们自己写的函数;P1200 里的 nameToNumber 也是。
它们让 main 变得干净好读:一眼就能看出「先分类,再各自处理」。
明天就把这件事学明白:函数三要素(返回类型、函数名、参数列表)、值传递和引用传递的区别、为什么大数组要开在全局而不是 main 里面,
以及竞赛里出现频率最高的两个函数模板 —— 判断素数和判断闰年。正好对应明天的 P5736 和 P5737。
预习建议:随便翻一个你之前写过的最长的代码,看看能不能把其中重复的部分抽成一个函数。