把输入数按源进制读成数位数组,再反复对目标进制做长除法收集余数,逆序拼出目标进制表示。
OJ: roj
题目 ID: 3028
难度:普及+/提高-
标签:进制转换高精度字符串python
创建: 2026-10-01 11:03
更新: 2026-10-07 13:50
形式化题目
数位集合是 62 个字符 0-9A-Za-z,其权值依次为 0,除非 0)。它代表的整数是
要求对每组求出唯一的数位串 0,
除非
即把同一个整数从 0。
数据中最长的输入是 10 进制 109 位、62 进制 62 位;数值本身最大约
正解
思路
朴素想法:把整个数按“每位乘以 long long,所以这条路在 C++ 里走不通;
换成任意精度整数又等于绕开了本题要练的东西。于是只能把整数本身存成数位数组,
在数组上做算术。
关键观察一:整道题只需要一个原子操作。 输入串本身就是
关键观察二:中间量不会溢出。 归纳可知始终有 int 就够用。
这也是本题不需要写完整高精度库的原因——只需要一次
关键观察三:第一次除法得到的是最低位。 由
算法流程:
- 把输入串逐字符查表,得到源进制下的数位数组
digits(高位在前); - 循环:
digits, rem = divide(digits, b, c),把rem收进结果; digits变空时停止(说明已经除尽),把结果翻转拼接;若结果为空则原数是 0,输出0。
样例推演:取第 1 组 62 2 abcdefghiz,数位是
[36, 37, 38, 39, 40, 41, 42, 43, 44, 61]。下表是第一次除以 x = rem*62 + a,商位为 x // 2,新进位为 x % 2):
| 第 1 步: |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 61 |
|---|---|---|---|---|---|---|---|---|---|---|
| 进位 |
0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
| 36 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 61 | |
| 商位 |
18 | 18 | 50 | 19 | 51 | 20 | 52 | 21 | 53 | 30 |
| 新进位 |
0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
(表中“进位
第一次除法结束时进位为 [18,18,50,19,51,20,52,21,53,30],第二轮继续同样地扫一遍。共做 59 轮,把 59 个余数
从后往前读,得到样例输出中的
11011100000100010111110010010110011111001001100011010010001注意当商位为 2 3 0)第一轮商就为空,直接输出 0。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 11:03
# update_at: 2026-10-01 11:03
import sys
DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" # 62 个数位,下标即权值
VALUE = {ch: v for v, ch in enumerate(DIGITS)}
def divide(digits: list[int], base: int, divisor: int) -> tuple[list[int], int]:
"""对源进制数位表做一次长除法,返回 (商的高位到低位, 余数)。
手算长除法:把下落进位 rem 与当前位拼成 x = rem*base + a,商位取 x // divisor。
rem < divisor ≤ 62 且 base ≤ 62,所以 x < 3900,C++ 里用 int 也不会溢出。
"""
quotient: list[int] = []
rem = 0
for a in digits:
x = rem * base + a
digit = x // divisor
if quotient or digit: # 忽略前导零;商为空说明已经除尽
quotient.append(digit)
rem = x % divisor
return quotient, rem
def convert(digits: list[int], base: int, target: int) -> str:
"""把源进制数位表写成目标进制字符串:反复除 target,余数就是低位到高位。"""
out: list[str] = []
while digits:
digits, rem = divide(digits, base, target)
out.append(DIGITS[rem]) # 余数必落在 [0, target),正是目标进制的一位
return "".join(reversed(out)) or "0" # 商被除空说明原数是 0
def solve() -> None:
# 三列里混着非数值的数位串,不能整体 map(int),所以只对数值位置套 int(next(data))
data = iter(sys.stdin.buffer.read().split())
out: list[str] = []
T = int(next(data)) # 组数
for _ in range(T):
src, dst = int(next(data)), int(next(data)) # 源进制、目标进制
raw = next(data) # 源进制数位串,保持 bytes 直接当数位表用
digits = [VALUE[chr(ch)] for ch in raw] # 输入数自己就是源进制的数位表
out += [f"{src} {raw.decode()}", f"{dst} {convert(digits, src, dst)}", ""]
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 14:55
* update_at: 2026-10-06 14:55
*/
// main.cpp:把 62 进制数位串在任意两种进制之间转换。
// 数值最大约 62^62,装不进 long long,所以把整数存成数位数组,
// 只需要一种原子操作:数位的长除法(除以目标进制 c,同时得到商和余数)。
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
typedef long long ll;
const ll MAXL = 200; // 输入串最长约 109 位,开 200 足够
// 62 个数位字符:'0'-'9' 权值 0-9,'A'-'Z' 权值 10-35,'a'-'z' 权值 36-61
const char DIGITS[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
ll T;
ll base; // 源进制 b
ll target; // 目标进制 c
ll digits[MAXL + 5]; // 当前数位数组(高位在前),每次长除法后变成商
ll q[MAXL + 5]; // 长除法的临时商数组
ll len; // digits 当前的位数
std::string out; // 收集到的余数,即目标进制的数位(从低到高)
// 按字符查它在 62 个数位里的权值,不在集合里返回 -1
ll char_value(char ch) {
for (ll i = 0; i < 62; i++)
if (DIGITS[i] == ch) return i;
return -1;
}
// 把数位 digits(共 len 位,基数为 base)除以 div,就地更新为商,
// 返回余数;除完跳过商的前导零,len 变成商的实际位数。
ll divide_small(ll div) {
ll rem = 0; // 从高位往低位下落的进位,始终小于 div <= 62
for (ll i = 1; i <= len; i++) {
ll x = rem * base + digits[i]; // x 最大 61*62+61 = 3843,int 都够
q[i] = x / div;
rem = x % div;
}
// 找商的第一个非零位;商可能全零(此时余数就是原数本身取模)
ll first = 1;
while (first <= len && q[first] == 0) first++;
ll new_len = len - (first - 1); // 商的位数(可能为 0)
for (ll i = 1; i <= new_len; i++)
digits[i] = q[first + i - 1];
len = new_len;
return rem;
}
// 反复长除法:每轮余数是目标进制的最低一位,逆序拼出答案
std::string convert() {
out.clear();
while (len > 0) {
ll rem = divide_small(target);
out += DIGITS[rem];
}
std::string res;
for (ll i = (ll)out.size() - 1; i >= 0; i--) // 余数从低到高,翻转
res += out[i];
if (res.empty()) res = "0"; // 原数就是 0
return res;
}
void solve() {
scanf("%lld", &T);
while (T--) {
char str[MAXL + 5];
scanf("%lld %lld %s", &base, &target, str);
len = strlen(str);
for (ll i = 0; i < len; i++) // 高位在前,直接查表得到数位
digits[i + 1] = char_value(str[i]);
std::string ans = convert();
printf("%lld %s\n", base, str); // 第一行:源进制与原数
printf("%lld %s\n\n", target, ans.c_str()); // 第二行:目标进制与新数,第三行为空行
}
}
int main() {
solve();
return 0;
}divide 只回答“这张数位数组除以 convert 只回答“把余数
从低到高收集起来是什么”,solve 只负责读入三列、原样回显输入数字、按三行一组输出。
源进制 base 只在 divide 内部用作拼接基数,除数是目标进制 target,两者不要写反。
复杂度
设输入数在源进制下有
- 时间:
。因为 , 即 ,与“手写高精度 + 逐位除法”的经典 做法同阶;最坏情形是 、 ,此时 可达 左右。 - 空间:数位数组、商数组与输出串各
,合计 。
实测 84 组数据(含 62 进制 62 位、10 进制 109 位的最坏用例)总耗时
总结
本题的解法只有一句话:要转成
由此可以推广出两条实用的经验:
- 判断高精度题是否需要完整的高精度库——如果全程只用到“乘以
加一位”和 “除以某个小常数取余”,那么两个 的手写循环就够了,不必实现乘除法。 - 读入(Horner 求值)与输出(反复取余)是一对逆过程;凡是“把一个数从一种基换到
另一种基”的问题,输入按基展开、输出按基取余,一般都能把复杂度压到
。
图示解析
下图给出从题面到输出的完整主线:左侧是输入提供的原始信息,中间是唯一需要自己实现的 一次长除法,右侧是收集与拼接的顺序。
输入:b, c, s = s_1 s_2 ... s_n
│ s_j 查表得权值 a_j,直接得到 b 进制的数位数组(不必求值)
▼
digits = [a_1, a_2, ..., a_n] 高位在前
│
│ 循环 m 次 ──────────────────────────────────────────┐
│ rem = 0 │
│ for a in digits: │
│ x = rem*b + a # x ≤ 61*62+61 = 3843 │
│ q.append(x // c) # 商位,跳过商的前导零 │
│ rem = x % c # 新的下落进位 │
│ digits = q │
│ out.append(DIGITS[rem]) # 余数 = c 进制的一位 │
│ ──────────────────────────────────────────────────┘
│ digits 为空 ⇒ 已除尽
▼
"".join(reversed(out)) 或 "0"(out 为空时)关键在中间那个循环体:它同时产出两样东西——商回到 digits 继续下一轮,
余数进入 out 成为结果的低位。因此 out 天然是逆序的,最后一步翻转是必需的;
而 out 为空则说明原数就是