上一节把字符串当字符数组用:下标、遍历、字符分类。这一节换个看法——把字符串当成数字的另一种写法。同一个值 255,10 进制写作 255,2 进制写作 11111111,16 进制写作 FF。三串字符,一个数。
to_string、sprintf。拼输出、算位数、补前导零。stoi、stoll,或者自己写秦九韶循环。find 找分隔符,substr 切开,reverse 就地翻转。| 组成 | 含义 / 格式要求 |
|---|---|
参数 | 任意算术类型,编译器按实参自动挑重载。 |
返回值 | string。位数就是 .size(),第 i 位字符就是 s[i]。 |
double 版本 | 固定 6 位小数,宽度和位数都管不了——要格式就别用它。 |
效率 | 比 sprintf 慢 2~3 倍,循环 10⁶ 次以上要掂量。 |
int a = 42;
string s1 = to_string(a); // "42",长度 2
string s2 = to_string(123456789012345678LL); // 18 位
cout << s1 + "!"; // 42!:+ 是拼接
cout << s2.size(); // 18:位数 = 长度to_string(1.0) 得到 "1.000000",不是 "1"。
两个结果只能 + 拼接,s1 - s2 编译不过。
| 组成 | 含义 / 格式要求 |
|---|---|
s | 要解析的串,允许前导空格与正负号。 |
pos | 可选。传 &p 就把“停在哪个下标”写进 p。 |
base | 可选,2~36。写 16 就能直接读 "7f" 得 127。 |
停在哪 | 遇到不属于该进制的字符就停,不报错:"123abc" 得 123。 |
抛异常 | 超出 int 范围时抛 std::out_of_range,进程终止,评测显示 RE。 |
string s = " -12345abc";
size_t pos;
int x = stoi(s, &pos); // -12345,pos = 8(停在 'a')
int y = stoi("7f", nullptr, 16); // 127
int z = stoi("11111111", nullptr, 2); // 255只认 int 范围。11 位的 "99999999999" 就抛异常。
怕异常就自己写 x = x * 10 + (c - '0'),绝不会炸。
| 组成 | 含义 / 格式要求 |
|---|---|
三个参数 | 含义与 stoi 一字不差,只是返回值换成 long long。 |
上限 | 9223372036854775807(19 位)。值一旦超过它就抛 out_of_range,所以 19 位也不能乱填。 |
怎么选 | ≤ 9 位用 stoi,10~18 位用 stoll,19 位对照上限看,≥ 20 位不转数字。 |
long long a = stoll("9223372036854775807"); // 19 位,正好 LLONG_MAX
// stoll("9223372036854775808") → 抛 out_of_range
string big = "12345678901234567890"; // 20 位,只能留在 string 里20 位的 18446744073709551615 连 unsigned long long 都装不下。
题目给出 20 位以上的数字,等于明示这道题考字符串。
| 组成 | 含义 / 格式要求 |
|---|---|
区间 | 左闭右开 [first, last)。s.end() 是末字符的下一个位置。 |
返回值 | void。原地修改,不产生新串。 |
数组也能用 | reverse(a, a + n) 反转 int 数组。 |
复杂度 | O(n),每个元素交换一次。 |
string s = "hello";
reverse(s.begin(), s.end()); // s 变成 "olleh"
cout << s; // 先反转,再输出
int a[5] = {1, 2, 3, 4, 5};
reverse(a, a + 5); // 5 4 3 2 1cout << reverse(...) 是编译错误(no match for operator<<),不是 WA。
只给一个迭代器参数也编译不过——它要首尾两个。
| 组成 | 含义 / 格式要求 |
|---|---|
参数 | 子串或单个 char:s.find('.')、s.find("to")。 |
起点 | 从哪个下标开始找;想接着找就传 上次位置 + 1。 |
返回值 | 首次出现的下标;找不到返回 string::npos,不是 -1。 |
npos | size_t 的最大值 2⁶⁴−1。判断写 != string::npos,别写 < 0。 |
string t = " to be or not to be ", w = " to ";
int cnt = 0;
size_t p = t.find(w);
while (p != string::npos) {
++cnt;
p = t.find(w, p + 1); // 从下一位继续,否则原地死循环
}
cout << cnt; // 2漏掉 +1:find 每次返回同一个下标,死循环,评测 TLE。
把结果存进 int 再和 npos 比,npos 截断成 -1,看似能过;换成 < 0 的判断就永远为假。
| 组成 | 含义 / 格式要求 |
|---|---|
pos | 起点下标。pos == size() 合法,得空串。 |
len | 取几个字符;省略就取到结尾,写大了自动截断,不报错。 |
pos 越界 | pos > size() 抛 out_of_range → RE。 |
最常用 | 取 [l, r) 这一段写 s.substr(l, r - l)。 |
string s = "123.456";
size_t p = s.find('.'); // 3
string a = s.substr(0, p); // "123"
string b = s.substr(p + 1); // "456",取到结尾
string c = s.substr(2, 5 - 2); // "3.4"s.substr(p + 1) 在 p 是末字符时返回空串,合法。
第一个参数是起点,不是长度——这是最常见的错法。
| 组成 | 含义 / 格式要求 |
|---|---|
buf | 字符数组,大小自己保证;写越界就是踩内存,表现为 RE 或莫名 WA。 |
fmt | %d 整数、%lld 长整型、%x 小写十六进制、%X 大写、%02d 补两位零、%s 字符串。 |
转 string | char 数组可以直接赋给 string:string s = buf; |
char buf[64];
sprintf(buf, "%03d", 7); // "007",前导零
sprintf(buf, "%X", 255); // "FF"
sprintf(buf, "%lld", 10000000000LL); // long long 必须 %lld
string s = buf;用 %d 打印 long long 是未定义行为:出乱码,且往往只在部分数据上错。
放 20 位数字,char buf[32] 够;做拼接按 len + 16 估。
输入三行:第一行整数 n(2 ≤ n ≤ 16),表示原数的进制;第二行一个 n 进制数,n > 10 时用大写 A~F 表示 10~15,且它的十进制值不超过 10⁹;第三行整数 m(2 ≤ m ≤ 16),表示目标进制。
输出这个数在 m 进制下的写法,m > 10 时同样使用大写 A~F。
n 进制与 m 进制之间没有逐位对应关系(2↔4/8/16 是例外),十进制是唯一的中转站。中转只需要两个操作:
dec = dec * n + 当前位的值,从左到右扫一遍,k 位只做 k 次乘加。dec % m 是当前最低位,dec /= m 砍掉它,最后整体倒序。"0123456789ABCDEF",下标即数值,digit.find(c) 就是反向查表。先手算再写码。FF(16 进制):0×16+15 = 15,15×16+15 = 255。255 转 2 进制:连除 2 得 8 个余数全是 1,商归零停下,倒着读就是 8 个 1。
再用小数字看清“为什么必须倒序”:6 转 2 进制,6 % 2 = 0(这是最低位),3 % 2 = 1,1 % 2 = 1,商为 0 停。余数按 0、1、1 的顺序出来,可答案是 110——余数天生从低位到高位,所以最后那一次 reverse 不是装饰。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m;
string s;
cin >> n >> s >> m; // 三行输入;cin 按“词”读,换行自动跳过
string digit = "0123456789ABCDEF"; // 数码表:下标即数值,够用到 16 进制
// ① n 进制 -> 十进制,秦九韶
long long dec = 0;
for (int i = 0; i < (int)s.size(); ++i) {
int d = (int)digit.find(s[i]); // 'F' 在表里的下标是 15
dec = dec * n + d; // 旧值升一档,再加新的一位
}
// ② 十进制 -> m 进制,除基取余
string res;
if (dec == 0) res = "0"; // 特判:否则 while 一次不进,输出空行
while (dec > 0) {
res += digit[(int)(dec % m)]; // 余数就是当前最低位
dec /= m; // 砍掉最低位,下一位升上来
}
reverse(res.begin(), res.end()); // 低位先产生,倒过来才是正常写法
cout << res << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
cin >> n >> s >> m; | 三行被当成三个“词”。s 是 string,遇空白就停,正好读完整个 n 进制数。 |
string digit = "0123456789ABCDEF"; | 把“数值→字符”做成一张表:digit[15] 就是 'F'。比 if-else 短、比 switch 快;代价是表只有 16 个字符,扩到 36 进制得换表。 |
int d = (int)digit.find(s[i]); | 反向查表:'F' 在表里的下标是 15,即它的数值。find 返回 size_t,赋给 int 要显式转换。 |
dec = dec * n + d; | 秦九韶。乘 n 是把已读部分整体左移一位,加 d 是补上新的最低位。第 i 位最终被乘了 n^(k−1−i) 次,正好等于位权。 |
if (dec == 0) res = "0"; | 输入 0 时循环一次不进,res 保持空串,会输出一个空行。这是本题唯一的数据边界。 |
res += digit[(int)(dec % m)]; | 余数 r = dec mod m 一定是当前最低位,digit[r] 把数值翻回字符,追加到尾部。 |
dec /= m; | 整除砍掉刚取走的最低位,剩下的是「原数除以 m 向下取整」,规模严格变小,循环必停。 |
reverse(res.begin(), res.end()); | 余数从低位到高位产生,答案要从高位写起,所以整体倒序。忘掉这句,6 转 2 进制会输出 011。 |
小数、分数、日期、坐标的本质都一样:一个串被一个分隔符切成两段。find 负责定位,substr 负责切开,reverse 各管自己翻转。
string s = "123.456";
size_t p = s.find('.'); // 3;找不到时是 string::npos
string a = s.substr(0, p); // "123",从 0 取 p 个
string b = s.substr(p + 1); // "456",从 p+1 取到结尾
reverse(a.begin(), a.end()); // "321"
reverse(b.begin(), b.end()); // "654"
cout << a << "." << b; // 321.654顺带记住两条容易被忽略的规则:string 比较用的是字典序("9" < "10" 为假,而数值上 9 < 10),要按数值比就先比长度、再比字典序;to_string 造不出前导零,固定宽度只能靠 sprintf。
还有一个顺序问题:一个串上通常只有一种分隔符,所以按 % → . → / 依次试,命中一个就停。写成几个独立的 if(而不是 else if),同一串会被输出两遍——这是下一题最隐蔽的错法。
reverse,与原串比较;或双指针首尾对扫。to_string(x).size() 得位数,遍历字符串得每个数位。long long 最大 9223372036854775807,19 位。超过它一步,“string → 数字 → string”这条链就整条断掉——20 位的输入必然如此,19 位的也得先和上限比一比。P1143 只保证十进制值 ≤ 10⁹(10 位),所以 long long 很富余;而本节挑战题把长度给到 20 位,同一个算法必须换成纯 string 版本。
但不进数字类型 ≠ 不能算:只要最终要的是余数、位数、某一位这类只依赖低位的信息,秦九韶就能只保留这部分状态往下跑,中间值永远不超范围。所以看到“输入是一个很长的数字串”,第一个问题永远是:我到底需要它的哪一部分信息?
string s = "12345678901234567890"; // 20 位
// long long x = stoll(s); // 抛 out_of_range → RE
int r = 0; // 求这个数 mod 7
for (int i = 0; i < (int)s.size(); ++i)
r = (r * 10 + (s[i] - '0')) % 7; // 中间值最大 6*10+9 = 69
cout << r; // 1进制题的答案没法靠“看起来像”判断,交之前用三个办法自检,每个都不到一分钟:
10)。这四组能打掉九成的 WA。这三招不只对进制题有用:凡是“有明确逆运算”的题(进制互转、加解密、编解码)都能用回环验证。
| 进制 | 数码 | 一位能表示 | 典型场合 |
|---|---|---|---|
| 2 | 0 1 | 2 | 位运算、状压 DP、集合的子集 |
| 10 | 0~9 | 10 | 所有读入输出、日常计算 |
| 16 | 0~9 A~F | 16 | 内存地址 0x7ffd、颜色 #FF8800、哈希 |
| 36 | 0~9 A~Z | 36 | 短链接、编号压缩:位数能省一半 |
最大可能多少位?≤ 9 位 int,≤ 18 位 long long,19 位踮脚,≥ 20 位一律 string。
需要整体数值吗?要就转数字;只要某位、某段、某个余数就全程字符串。
输出有固定宽度、前导零、大小写要求吗?有就用 sprintf 或数码表。
十六进制大小写:数码表写成 "0123456789ABCDEF" 或全小写,一行切换。
固定宽度补零:sprintf(buf, "%05d", x);to_string 做不到这件事。
去掉多余前导零:从下标 0 起跳过 '0',但至少保留一位,否则 0 会变成空串。
把 20 位数喂给 stoll:抛 std::out_of_range,进程直接结束,评测显示 RE,连输出都看不到。
字符忘了减 '0':写成 dec = dec * n + s[i],'7' 的 ASCII 是 55,答案偏到天边。
除基取余忘了 reverse:6 转 2 进制应输出 110,你会输出 011,带前导零,WA。
用 s.find(c) < 0 判断没找到:size_t 无符号,条件永远为假。一律写 != string::npos。
拿 find 的返回值直接当 substr 起点:找不到时 npos + 1 是个天文数字,pos > size() 抛异常 → RE。
在循环里写 res = to_string(i) + res:每轮都要整体搬移整串,10⁵ 轮就是 O(n²),本地跑得动、交上去 TLE。正确做法是往后追加、最后 reverse 一次。
dec == 0 不特判:输入 0 时 res 是空串,输出一个空行 → WA。
关掉页面,把 P1143 的代码从零敲一遍提交。第一组数据用 16 / FF / 2,输出应为 11111111。
再打两个边界:2 / 1011 / 10 应得 11;10 / 0 / 16 应得 0(专打特判)。
改一改:把数码表换成全小写 "0123456789abcdef",输出立刻变小写十六进制;但输入也得跟着变小写——同一张表既要用 find 查字符、又要用下标取字符,大小写必须一致。用 16 / ff / 2 验证。
再改一改:把表扩成 36 个字符 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",代码一行都不用动,输入 36 / Z / 10 就得到 35——这就是表比 if-else 划算的地方。
最后一改:把秦九韶换成“倒着扫、第 i 位重算 n^i”的直白写法,用 18 位输入数一数两种写法各做多少次乘法。
设 n 进制数从高到低是 dk−1, …, d0,它的值就是各位乘自己位权的和:V = dk−1·nk−1 + … + d1·n + d0。
秦九韶把它改写成嵌套乘法:V = ((…(dk−1·n + dk−2)·n + dk−3)…·n + d0。把括号一层层拆开,每一项的系数与 n 的次数都和上式相同,两者等价。
差别只在代价:直白写法每位都要重算一次 ni,乘法 0+1+…+(k−1) = k(k−1)/2 次,k = 18 时是 153 次;秦九韶一次乘加一位,18 次。
除基取余的正确性来自带余除法:V = q·m + r,0 ≤ r < m,所以 r 一定是 m 进制写法里的最低位,q 是“砍掉最低位”后的数。对 q 重复同样的推理,位就从低到高被逐位剥出,因此最后必须倒序。
位数估算:k = ⌊logn V⌋ + 1。十进制 19 位对应 2⁶³−1 = 9223372036854775807;同一个值写成二进制要 63 位,写成十六进制只要 16 位——基数越大,位数越少,36 进制因此能把编号压到一半。
顺手把“19”推导一遍:2¹⁰ = 1024 ≈ 10³,于是 2⁶³ = (2¹⁰)⁶·2³ ≈ 10¹⁸×8 = 8×10¹⁸,精确值是 9.223×10¹⁸,刚好落在 10¹⁹ 以下——所以 long long 装得下 19 位、装不下 20 位;unsigned long long 是 2⁶⁴ ≈ 1.84×10¹⁹,正好 20 位,再多一位也装不下。
最后解释那个例外:2、4、8、16 之间可以直接换,不必经过十进制,因为 16 = 2⁴、8 = 2³——一个十六进制位恰好对应 4 个二进制位。所以 FF 逐位查表就得到 1111 1111。十六进制在底层代码里流行,正因为它对人可读(8 位一组)对机器整齐(4 位一组)。
cout << a + b?把理由压成一句话。"#FF8800" 拆成三个 0~255 的整数,用本节的哪几个函数组合最省事?s == string(s.rbegin(), s.rend()),也可以双指针首尾对比。两者时间、空间差多少?长度 10⁷ 时哪个会 MLE?< 0。给定一个不含负号的实数 s,把它的各个数位反转。s 可能是整数、小数、分数或百分数。
整数:所有数位对调。小数:整数部分、小数部分各自反转,小数点不动。分数:分子、分母各自反转,分数线不动,不约分。百分数:只反转数字部分,% 保留。
反转后的最高位不能是 0(0 本身除外);小数部分末尾的 0 要去掉,若只剩 0 则保留一个 0。
输入格式:一行,一个实数 s,各段长度最多 20 位。输出格式:一行,s 的反转数。样例:600.084 得 6.48,700/27 得 7/72。
四种情况互斥,判别靠串里有没有 %、.、/ 这三个字符,不需要猜数据类型。
整段串里已经有全部信息。先想清楚:能不能把某一段转成数字再反转?数据范围允许吗?
真正要动脑的不是 reverse,而是删零:哪一端是“无意义的 0”,四种情况各不相同。
① 依次 find('%')、find('.')、find('/'),命中哪个就是哪一类,都没命中就是整数。
② 用 substr 在分隔符两侧切开,左右两段各自 reverse。
③ 左段(整数部分、分子、百分数的数字)去掉反转后开头连续的 0,至少留一位;右段若是小数部分,去掉反转后末尾连续的 0,也至少留一位;若是分母,按整数规则去掉开头的 0。
④ 拼回去:a + "%"、a + "." + b、a + "/" + b,整数直接输出 a。全程在 string 里流动,一个字符都不进 long long。
为什么小数部分要删末尾的 0:600.084 的小数段 "084" 反转得 "480",末尾那个 0 由原数的前导 0 变来,是无意义的 0,答案要求 6.48 而不是 6.480。
#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s; // 整行只有一个串,不含空格
size_t p = s.find('%');
if (p != string::npos) { // 百分数:只动数字部分
string a = s.substr(0, p);
reverse(a.begin(), a.end());
int i = 0;
while (i + 1 < (int)a.size() && a[i] == '0') ++i; // 去前导零,至少留一位
cout << a.substr(i) << "%\n";
} else if ((p = s.find('.')) != string::npos) { // 小数:两段各管自己
string a = s.substr(0, p), b = s.substr(p + 1);
reverse(a.begin(), a.end());
reverse(b.begin(), b.end());
int i = 0;
while (i + 1 < (int)a.size() && a[i] == '0') ++i; // 整数段去前导零
int e = (int)b.size() - 1;
while (e > 0 && b[e] == '0') --e; // 小数段去末尾零
cout << a.substr(i) << "." << b.substr(0, e + 1) << "\n";
} else if ((p = s.find('/')) != string::npos) { // 分数:分子分母都按整数
string a = s.substr(0, p), b = s.substr(p + 1);
reverse(a.begin(), a.end());
reverse(b.begin(), b.end());
int i = 0, j = 0;
while (i + 1 < (int)a.size() && a[i] == '0') ++i;
while (j + 1 < (int)b.size() && b[j] == '0') ++j;
cout << a.substr(i) << "/" << b.substr(j) << "\n";
} else { // 整数
reverse(s.begin(), s.end());
int i = 0;
while (i + 1 < (int)s.size() && s[i] == '0') ++i;
cout << s.substr(i) << "\n";
}
return 0;
}| 片段 | 作用 |
|---|---|
size_t p = s.find('%'); | 先单独算一次,后面三个分支复用 p。用 size_t 接,别用 int。 |
(p = s.find('.')) != string::npos | 赋值表达式的值就是 p,少写一行,也保证 p 只在这个分支里有意义。 |
s.substr(0, p) / s.substr(p + 1) | 分隔符左边取 [0, p),右边从 p+1 取到结尾,两段长度互不影响。 |
while (i + 1 < (int)a.size() && a[i] == '0') ++i; | 去前导零,但下标最多走到最后一位:保证 "0" 这类串不会被删成空串。 |
int e = (int)b.size() - 1; while (e > 0 && b[e] == '0') --e; | 小数段规则相反:删末尾的 0,同样留最后一位。e 是最后一个保留字符的下标。 |
b.substr(0, e + 1) | 取 [0, e],长度是 e+1 而不是 e。差一错就在这里。 |
a.substr(i) | 只给起点,取到结尾——删完前导零剩下的正是答案。 |
i + 1 < (int)a.size() | size() 是 size_t(无符号),与 int 比较会有警告,显式转 int 更保险。 |
9223372036854775808,也已超过 LLONG_MAX,直接抛 out_of_range,评测 RE。"0" 反转后还是 "0",无条件删前导零会输出空行 → WA。循环条件里必须有 i + 1 < size()。600.084 会输出 6.480 而不是 6.48。小数段要删的是末尾的 0。cout << a << b 少了小数点,输出 648,只差一个字符,判 WA。