ACM 一个月学习计划 · DAY 09

Day 9 · 字符串进阶:数字、进制与格式处理

数字不只是拿来算的,它还是一串字符 —— 学会「按字符思考」,你就能处理小数、分数、百分数和任意进制

今日目标:掌握数字与字符串互转、手写数字反转、进制转换,以及「格式敏感」类题目的分类讨论套路
预计用时:约 3 小时(含做题)  难度:入门 ~ 普及-
题目来源:洛谷 P1307 / P1553 / P1200 / P2084
← Day 08 · 字符与字符串基础

⏱今日时间安排

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 读进来, 只能先当字符串读,再看里面有什么符号,分情况处理。

1数字 ⇄ 字符串:两套工具,各管一段

先想清楚一个问题:这个数,你要「算」还是要「看」?

同一个数字,在你脑子里的身份可以完全不一样:

你关心的是怎么存典型题目
它的大小:加、减、比大小、求和int / long long求平均值、找最大值
它的每一位长什么样:反转、回文、数数码stringP1307、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"

工具二:自己手写拆位(第 2 节详细讲)

如果要处理的是纯整数,而且只在乎「数位」,那么用 % 10 和 / 10 手写反而更快、更省内存。 但今天的重头戏 P1553 里全是符号,字符串才是正解。

⚠️ 用 cin >> s 读字符串,别用 getline(今天这 4 题都不用)

cin >> s 会跳过开头的空白,一直读到空白为止 —— 正好适合读 600.084 这种「一整块没有空格的输入」。

而 getline(cin, s) 会把整行连空格一起读进来。Day 8 的 P5015 标题统计、P1765 手机,才需要它(因为里面有空格)。
今天 4 道题的输入都没有空格,所以一律 cin >> s 最省事。

为什么不能什么都用 long long 硬扛?

类型能存多大够用吗
int约 ±2.1 × 10⁹,最多 10 位数P1307(|N| ≤ 10⁹)刚好够
long long约 ±9.2 × 10¹⁸,最多 19 位数P1553 的 20 位整数 ❌ 装不下
string想多长有多长(内存允许的话)P1553 ✅
🚨 这就是 P1553 必须用字符串的根本原因

题面写着:整数部分「不大于 20 位」,百分数分子「不大于 19 位」。

20 位的整数,比如 12345678901234567890,已经远远超过 long long 的上限(9223372036854775807,也才 19 位)。 你用 long long 读它就爆了 —— 所以只能当字符串,逐位处理。

看到「位数不超过 xx 位」这种描述,就先警惕一下:是不是超过了 19 位?超了就必须用字符串。

2数字反转:三个必须处理的细节

这是今天的主线。P1307 是它的入门版,P1553 是加了三种「格式」的完整版。

细节一:最高位不能是 0(末尾的 0 要丢掉)

反转 1200,机械地倒过来是 0021,但题目要的是 21。

好消息是:用算术方法写,前导 0 会自动消失,你什么都不用做。

rev = rev * 10 + n % 10;      // ★ 关键的一行

为什么?因为构造新数的时候,如果这一次拿到的是 0,那么 rev * 10 + 0 等于把 rev 后面「补一个 0」。 但如果此时 rev 本身还是 0,那么 0 * 10 + 0 仍然是 0 —— 0 乘任何数都是 0,前导 0 就这么被吃掉了。

走一遍:1200 是怎么变成 21 的

轮次nn % 10rev(新数)
开始1200—0
1120000×10+0 = 0
212000×10+0 = 0
31220×10+2 = 2
4112×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。 直接算下去,中间会出现一堆负的位数,非常容易写错。
把符号「摘出去」,处理完再「装回去」—— 这个思路在处理正负号、小数点、百分号时都通用。

细节三:用 long long,别用 int

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 就是反转后的数
⚠️ n = 0 时循环一次都不执行

如果输入是 0,while(n > 0) 直接跳过,rev 保持 0 —— 输出 0,答案正好是对的。 P1307 允许 N = 0,这个「巧合」帮了你。但如果题目要求反转后去掉前导零、且 0 要特殊处理,你就得留意了。

3进制转换:两个方向,两套模板

方向一:十进制 → M 进制(除基取余,倒序输出)

把十进制的 13 转成二进制:不停地除以 2,把余数记下来,最后把余数倒着读。

步骤除以 2 的商余数
13 ÷ 261
6 ÷ 230
3 ÷ 211
1 ÷ 201

余数依次是 1、0、1、1,倒过来读就是 1101 —— 也就是 (13)10 = (1101)2。

方向二:M 进制 → 十进制(秦九韶,逐位累乘)

🎯 一个公式搞定,不用算幂

ans = ans * M + 这一位的数字,从最高位往最低位走一遍。

比如把 (1101)2 转回十进制:

读到的位计算ans
开始—0
10×2+11
11×2+13
03×2+06
16×2+113 ✅

为什么对?拿 1101 展开看看:1×2³ + 1×2² + 0×2¹ + 1×2⁰。 每读一位就 ×2,相当于把前面所有已经读过的位的权重整体抬高一级 —— 跟你手算时从高位往下推是一模一样的思路。 这个方法叫「秦九韶算法」(也叫霍纳法则),后面处理超大整数、进制转换都会用到。

今天 P2084 要的是第三种:把式子「原样打出来」

注意 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 都是这个套路)。

4「格式敏感」题目的通用套路:先分类,再分块

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 一直取到末尾」,这也是合法的写法。

5四道例题详解

下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译、实际运行过,输出是你看到的真实结果。

P1307 [NOIP 2011 普及组] 数字反转 普及- · 取位反转

题意:给一个整数 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 取负再输出。

样例 1 输入
123
样例 1 输出
321
样例 2 输入
-380
样例 2 输出
-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(反转后不变)✅

易错点

  • 忘了负号:直接把负数丢进 while,n % 10 会出负数,结果一团乱。一定要先摘符号。
  • 以为要手动删前导 0:不用!rev * 10 + 0 在 rev 还是 0 的时候等于 0,前导 0 自动没了。 如果你自己再写一段去零代码,反而容易写错。
  • 用 string 反转也行(reverse + 去前导 0),但那样要处理负号字符,比算术法麻烦。这题用算术法最省事。
  • 输出别忘换行 —— 这题的输出只有一行,但习惯上还是 cout << rev << endl;。
P1553 数字反转(升级版) 普及- · 分类讨论 + 字符串

题意:还是数字反转,但这次的「数」有四种长相:整数、小数、分数、百分数。 规则是:每种格式的各个数字块内部各自反转,格式符号(小数点、斜杠、百分号)的位置完全不动。

输入是什么怎么反转输出
5087462整数整串反转,去掉前导 02647805
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)。 因为题目说「分数与整数翻转相关规定见上」「百分数见与整数翻转相关内容」—— 也就是说它们都按整数规矩来。

样例 1 输入
5087462
样例 1 输出
2647805
样例 2 输入
600.084
样例 2 输出
6.48
样例 3 输入
700/27
样例 3 输出
7/72
样例 4 输入
8670%
样例 4 输出
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% 一致

另外我把每种情况的边界都跑了一遍(真实输出):

输入输出考察点
00整数 0
10001末尾三个 0 全丢
0.00.0小数的两段都是 0,不能被删空
0.50.5整数部分是 0
230.032.0整数部分去前导 0,小数部分保留 0
1.0011.1小数部分去末尾 0(100 → 1)
100.0011.1两段同时去各自方向的 0
0/70/7分子是 0
1200/3021/3分子分母都去前导 0
120%21%百分数去前导 0
1000000000000000000%1%19 位,验证 long long 装不下时照样正确
1234567890.1234567890987654321.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。
  • 以为要交换分子分母:题面明说「不交换」——分子还是分子,只是内部反转。
P1200 [USACO1.1] 你的飞碟在这儿 Your Ride Is Here 入门 · 字符乘积取模

题意:把彗星名和小组名各转换成一个数字:每个字母按 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。

样例 1 输入
COMETQ
HVNGAT
样例 1 输出
GO
样例 2 输入
ABSTAR
USACO
样例 2 输出
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。输出字符串要一个字母一个字母对着题面抄。
  • 本题名字顺序是「先彗星、后小组」,但因为两边都要算、只比余数,谁先谁后不影响结果。读的时候按输入顺序读就行。
P2084 进制转换 入门 · 按位输出格式

题意:给一个 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 为例,走一遍:

in[i]幂次动作已输出
0'1'4first,不加 +,输出 1*2^41*2^4
1'0'3系数为 0,跳过1*2^4
2'1'2先加 +,再输出 1*2^21*2^4+1*2^2
3'0'1跳过1*2^4+1*2^2
4'1'0先加 +,再输出 1*2^01*2^4+1*2^2+1*2^0
样例 1 输入
2 10101
样例 1 输出
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 1000001*2^5(只有一项,前面没有多余的 +)
3 10201021*3^6+2*3^4+1*3^2+2*3^0(0 的位置被跳过,幂次仍然连续不重复)
9 9999999*9^5+9*9^4+9*9^3+9*9^2+9*9^1+9*9^0(仔细看最后一项,9^0 也要打出来)

易错点

  • 把 N 读成整数:最多 1000 位,读进去就废了。必须 string n。
  • 幂次算错:是 len - 1 - i,不是 i,也不是 len - i。 最高位是 len-1 次,最低位是 0 次 —— 一定要在心里验算一个 5 位的例子。
  • 加号加在项后面 → 末尾多一个 +。要用 first 标记,加号加在项前面。
  • 系数为 0 的项还输出 → 多出 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;

✓自测:你真的会了吗

第 1 题:反转 1050,你写的代码会输出什么?

答: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 在中间,不属于「最高位」。

第 2 题:小数 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 —— 错。

第 3 题:为什么 P1200 可以「边乘边取模」?

答:因为 (a × b) mod m = ((a mod m) × (b mod m)) mod m。

先取模只是「提前把多余的 47 的倍数扔掉」,剩下的余数再乘下一个数,扔掉的倍数部分仍然是 47 的倍数,不会影响最终余数。 这样做的好处是数字永远很小(小于 26 × 47),绝对不会溢出。

第 4 题(手写代码):写一个函数,把整数 n 反转(n 可以是负数),要求用字符串实现。
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;
      ...输出内容...
  }

→明天预告 · Day 10

函数:把代码写成模块

今天其实已经在不知不觉地「用函数」了 —— P1553 里的 stripLeft、stripRight、revInt、revDec 都是我们自己写的函数;P1200 里的 nameToNumber 也是。 它们让 main 变得干净好读:一眼就能看出「先分类,再各自处理」。

明天就把这件事学明白:函数三要素(返回类型、函数名、参数列表)、值传递和引用传递的区别、为什么大数组要开在全局而不是 main 里面, 以及竞赛里出现频率最高的两个函数模板 —— 判断素数和判断闰年。正好对应明天的 P5736 和 P5737。

预习建议:随便翻一个你之前写过的最长的代码,看看能不能把其中重复的部分抽成一个函数。