P1303 A*B Problem
P1303 A*B Problem
题目背景
高精度乘法模板题。
题目描述
给出两个非负整数,求它们的乘积。
输入格式
输入共两行,每行一个非负整数。
输出格式
输出一个非负整数表示乘积。
输入输出样例 #1
输入 #1
1
2
输出 #1
2
说明/提示
每个非负整数不超过 \(10^{2000}\)。
解题方法
方法一:基础高精度(逐位乘法)
思想
模拟竖式乘法的过程:
- 把两个数字倒序存入数组,低位在前,每一位存 \(0\sim 9\)。
- 双重循环,把两位的乘积累加到结果数组的对应位置上。
- 统一处理进位,把每个位置的值规范到 \(0\sim 9\)。
- 去掉前导零,倒序输出。
时间复杂度
\(O(len_a \times len_b)\),空间复杂度 \(O(len_a + len_b)\)。
C++ 代码
#include <iostream>
#include <vector>
using namespace std;
vector<int> mul(const vector<int>& A, const vector<int>& B) {
vector<int> C(A.size() + B.size(), 0);
// 1. 逐位相乘并累加
for (size_t i = 0; i < A.size(); ++i) // 【格式使用size_t 规范化】
for (size_t j = 0; j < B.size(); ++j)
C[i + j] += A[i] * B[j];
// 2. 统一处理进位
int carry = 0;
for (size_t i = 0; i < C.size(); ++i) {
carry += C[i];
C[i] = carry % 10;
carry /= 10;
}
// 3. 去除前导零
while (C.size() > 1 && C.back() == 0)
C.pop_back();
return C;
}
int main() {
string a, b;
cin >> a >> b;
if (a == "0" || b == "0") {
cout << 0 << endl; // 【0特判,提前返回】
return 0;
}
vector<int> A, B;
A.reserve(a.size()); // 【避免 vector 扩容带来的重复分配】
B.reserve(b.size());
// 倒序存储,方便从低位运算
for (int i = a.size() - 1; i >= 0; --i)
A.push_back(a[i] - '0');
for (int i = b.size() - 1; i >= 0; --i)
B.push_back(b[i] - '0');
vector<int> C = mul(A, B);
// 倒序输出
for (int i = C.size() - 1; i >= 0; --i)
cout << C[i];
cout << endl;
return 0;
}
方法二:压位高精度
思想
传统做法每个 int 只存 0∼90∼9 的一位数字,严重浪费 4 字节空间。
压位的核心是让每个 int 多存几位,比如存 4 位(0∼9999)或 9 位(0∼999999999),这样数字长度直接缩小为原来的 1/4 或 1/9,运算量和复杂度系数成倍缩减。
步骤
-
存储方式:从低位到高位,每 9 位数压入一个
int。 -
乘法运算:逐位相乘后累加到对应位置,
C[i+j] += A[i] * (long long)B[j](必须用long long防止溢出)。 -
进位处理:模数为
10^9,即C[i+1] += C[i]/BASE; C[i] %= BASE;。 -
输出:最高位直接输出,其余每个数字需要补齐前导 0 到 9 位(用
printf("%09d", num)或cout << setw(9) << setfill('0'))。
时间复杂度
\(O((len_a/9) \times (len_b/9))\),常数远小于方法一。
C++ 代码(\(10^9\) 进制)
#include <iostream>
#include <vector>
#include <iomanip>
#include <string>
using namespace std;
const int BASE = 1000000000; // 10^9
const int WIDTH = 9;
// 将字符串转化为压位数组,低位在前
vector<int> to_vector(const string& s) {
vector<int> res;
// 从字符串末尾开始,每 WIDTH 位切一段
for (int i = s.size() - 1; i >= 0; i -= WIDTH) {
int start = max(0, i - WIDTH + 1);
string sub = s.substr(start, i - start + 1);
res.push_back(stoi(sub)); // 子串长度 <= WIDTH,int 安全
}
return res;
}
// 高精度乘法(压位,边乘边进位,安全不溢出)
vector<int> multiply(const vector<int>& A, const vector<int>& B) {
// 用 long long 存储中间结果,确保累加不溢出
vector<long long> C(A.size() + B.size(), 0);
for (size_t i = 0; i < A.size(); ++i) {
for (size_t j = 0; j < B.size(); ++j) {
// 累加乘积到对应位
C[i + j] += 1LL * A[i] * B[j];
// 立即处理当前位的进位,保持 C[i+j] 始终 < BASE
C[i + j + 1] += C[i + j] / BASE;
C[i + j] %= BASE;
}
}
// 由于边乘边进位,可能最高位仍有残留进位未处理,统一扫尾
for (size_t i = 0; i < C.size() - 1; ++i) { // 【要注意为什么要多-1,为了防止溢出】
C[i + 1] += C[i] / BASE;
C[i] %= BASE;
}
// 去除前导零(结果可能比实际需要的长一位全零)
while (C.size() > 1 && C.back() == 0)
C.pop_back();
// 转为 int 返回
vector<int> res(C.begin(), C.end());
return res;
}
int main() {
// 加速 IO(可选)
ios::sync_with_stdio(false);
cin.tie(nullptr);
string a, b;
cin >> a >> b;
// 乘数中有 0 的情况直接输出 0
if (a == "0" || b == "0") {
cout << 0 << '\n';
return 0;
}
vector<int> A = to_vector(a);
vector<int> B = to_vector(b);
vector<int> C = multiply(A, B);
// 输出:最高位直接输出,其余位补足前导零到 WIDTH 位
cout << C.back();
for (int i = C.size() - 2; i >= 0; --i) // 【注意为什么这里不使用size_t格式,因为这里如果无符号-1到0下,会变为无穷大】
cout << setw(WIDTH) << setfill('0') << C[i];
cout << '\n';
return 0;
}
说明
-
字符串 → 每 9 位切一段 → 倒序压入
vector<int>(低位在前) -
结果数组开
A.size() + B.size()个long long -
双重循环:
C[i+j] += A[i] * B[j],然后立即C[i+j+1] += C[i+j] / BASE; C[i+j] %= BASE; -
最后扫一遍进位 + 去前导零
-
倒序输出,最高位不补零,其余
setw(9) << setfill('0')
方法三:Karatsuba 分治(课外阅读)
思想
普通乘法复杂度 \(O(n^2)\),Karatsuba 算法利用分治策略将复杂度降至 \(O(n^{\log_2 3})\approx O(n^{1.585})\)。
将大数 \(X,Y\) 各分为两半:
则:
只需计算三次乘法 \(ac\)、\(bd\)、\((a+b)(c+d)\),配合加减法即可。递归到底层数字较小时换用基础乘法。
适用场景
-
当数字长度超过某一阈值时递归使用 Karatsuba,低于阈值切换为普通 $$N^2$$ 乘法(或压位乘法)。
-
在某些 OJ 或大型项目中,Karatsuba 与 FFT/NTT 结合使用,相得益彰。
方法四:FFT / NTT 加速(进阶)
思想
高精度乘法与多项式乘法完全等价:
把数字看作多项式 \(P(x)=\sum a_i x^i\)(\(a_i\) 是一位或压位系数),两个多项式相乘的系数就是高精度乘法的结果。
直接计算卷积是 \(O(n^2)\),但用快速傅里叶变换(FFT) 可以在 \(O(n\log n)\) 时间内完成。
FFT 存在浮点精度问题,所以通常用快速数论变换(NTT) 在模大质数下精确计算,适合超大整数运算。
适用场景
数字长度达到 \(10^5\) 及以上,普通压位也无法满足时限要求时,必须使用 FFT/NTT。
洛谷 P4245 是任意模数 NTT 的模板题,适合后续挑战。
总结与学习路径
| 方法 | 时间复杂度 | 空间复杂度 | 适用数据规模 | 学习阶段 |
|---|---|---|---|---|
| 基础逐位乘法 | \(O(n^2)\) | \(O(n)\) | \(n \le 2000\) | 入门,必会 |
| 压位高精度 | \(O((n/k)^2)\) | \(O(n/k)\) | \(n \le 10^4\) | 提高,建议收入模板 |
| Karatsuba | \(O(n^{1.585})\) | \(O(n)\) | \(n \ge 10^5\) | 省选/ACM,理解分治 |
| FFT / NTT | \(O(n\log n)\) | \(O(n)\) | \(n \ge 10^5\) | 高阶,多项式运算核心 |
建议学习顺序:
先彻底掌握基础逐位乘法(方法一),
然后将其优化为压位版本(方法二)并作为自己的常用模板,
之后逐步了解 Karatsuba 和 FFT/NTT 的原理与应用场景。
技巧补充
-
循环顺序与缓存
在双层循环中,让第一层循环遍历较短的数组,可以减少内层循环的长度,对缓存友好。 -
size_t回绕风险for (size_t i = n.size() - 1; i >= 0; --i) // 死循环!应改成
int i或for (size_t i = n.size(); i--; )。 -
快速字符转数字
n[i] ^ 48与n[i] - '0'等效,前者在某些编译器上略快。 -
输出格式化
压位输出时使用printf("%0*d", width, num)或cout << setw(width) << setfill('0')保证补齐前导零。
Python 版本参考
a = int(input().strip())
b = int(input().strip())
print(a * b)
因为 Python 自带大整数,直接乘即可。若想手写,参考基础逐位乘法的结构即可。
重要问题
(1)循环交换与缓存友好性
你的原始代码:
for (size_t i = 0; i < A.size(); ++i)
for (size_t j = 0; j < B.size(); ++j)
C[i + j] += A[i] * B[j];
为什么可以交换?
因为加法与乘法满足交换律与分配律,两层循环遍历所有 (i, j) 的组合,最终累加到 C[i+j] 的总和是相同的。
交换后的代码:
for (size_t j = 0; j < B.size(); ++j)
for (size_t i = 0; i < A.size(); ++i)
C[i + j] += A[i] * B[j];
这里固定 j,内层 i 遍历 A 和 C 的连续片段。
为什么对缓存友好?
- 内存中数组
A的元素是连续存放的(vector保证连续存储)。 - 当
j固定时,内层循环按i=0,1,2,...顺序访问A[i],这正是顺序访问,CPU 的缓存预取机制可以提前把下一块数据加载到高速缓存,大幅减少 cache miss。 - 同时
C[i+j]也是连续的:C[0+j], C[1+j], C[2+j]...,写入地址同样连续,能利用写缓存合并(write combining)。
反观原始代码:
- 固定
i,内层j变化,访问B[j]是连续的,但A[i]在每一次内层循环中是同一个值,这倒还好。 - 但
C[i+j]的访问是跨步的(i固定,j变化,地址间隔一个int),连续性不如后者。
当然,在本题中数据规模较小,这两种写法几乎看不出性能差异;但养成关注内存访问模式的习惯,在处理大矩阵、图像处理、科学计算时,会是决定程序快慢的关键因素。
(2)reserve() 的意义与字符串长度
问题回顾:
A.reserve(n.size());
如果输入 n = "100",数字是 100,为何 n.size() 是 3?
解释:
n是string类型,存储的是输入数字的字符串表示。"100"包含三个字符:'1','0','0',所以n.size()返回的是位数,而不是数值大小。- 同理,输入数字有 2000 位时,
size()就是 2000。
reserve 做了什么?
vector是一个动态数组,内部有一块堆内存来存元素。当我们不断push_back时,如果已分配的空间不够,它会:- 重新分配一块更大的内存(通常是原来的 2 倍或 1.5 倍)
- 把原有元素拷贝或移动到新内存
- 释放旧内存
- 这个过程叫“扩容”,多次扩容会带来不小的开销。
reserve(n.size())一次性预分配足够的容量,保证在 push_back 不超过这个数量时,一次扩容都不会发生,从而提高效率。
举例:
vector<int> A;
A.reserve(100); // 现容量 ≥ 100
for (int i = 0; i < 100; ++i)
A.push_back(i); // 这100次都不会触发重新分配
在你的代码里,预分配 n.size() 个位置保证了把字符串逐位 push_back 时没有额外内存操作。这是一种常数级优化,也是工程上的好习惯。
(3)压位高精度:如何确定 int 能存几位?
这是设计高精度压位模板的核心数学基础。我们一步步推导。
3.1 存储类型与运算类型
- 我们通常用
int数组存储压位后的每一位(值 ∈ \([0, \text{BASE}-1]\))。 - 乘法时,需要计算两个这样的“数字”的乘积,并可能加上进位。
- 最大乘积为 \((\text{BASE}-1)^2\),如果有进位累积,还可能加上 \((\text{BASE}-1)\) 左右的额外值(进位通常小于 BASE)。
- C++ 标准保证
int至少是 16 位,但在现代平台(包括洛谷的评测环境)都是 32 位,最大值为 \(2^{31}-1 \approx 2.147\times 10^{9}\)。显然,\((\text{BASE}-1)^2\) 很容易超过这个值,所以乘法时必须用long long(64 位,最大约 \(9.22\times 10^{18}\))来保存中间结果。
3.2 安全不等式
设基数为 \(B\),我们要求两个“数字”乘积累加进位后不溢出 long long 的正值范围(实际上保证不超过 \(2^{63}-1\))。
最大可能累加到某一位 C[k] 的值为:
- 来自乘积的和:最多有 \(\min(len_A, len_B, k+1)\) 对
(i,j)的乘积会落到该位置。 - 保守做法是保证中间变量
prod可以安全存储两个数字的乘积,且后续进位加法不溢出。通常我们控制单次prod = A[i] * B[j]不溢出long long,因为进位是单独处理的。
基本条件:
如果只用 long long 存乘积,且不同时加入巨大的累加(可在内层循环中即时处理进位,避免累积过大),这个条件就足够了。
解不等式:
所以 \(B\) 最大可以取 \(3\times 10^9\)(即 3,000,000,000)。但为了输出方便,通常取 \(10^k\) 形式,并且要留足进位余量。
例如:
- \(B = 10^9\):\((10^9-1)^2 \approx 10^{18} - 2\times 10^9 + 1 < 9.22\times 10^{18}\),安全。
- \(B = 10^{10}\):\((10^{10})^2 = 10^{20} > 9.22\times 10^{18}\),会溢出
long long,不行。 - 所以 \(10^9\) 是安全且舒适的极限。
3.3 如果有进位累加的情况?
一些实现会把累加和进位一起做,比如:
long long sum = (long long)C[i+j] + (long long)A[i] * B[j] + carry;
此时 C[i+j] 可能已经很大(上一轮累加的结果,可能接近几个 \(B\)),所以需要更大的安全范围。若采用逐次进位(每次乘法后立刻将当前位的值处理到 \([0, B-1]\),将进位传到下一位),C[i+j] 就保持在一个很小的范围内,那么条件 \((B-1)^2 + (B-1)\) 仍然在 \(9.22\times 10^{18}\) 以内即可:
依然安全。
3.4 为什么不是 \(10^5\) 或 \(10^6\)?
- 基数越大,数组越短,循环次数越少,整体运算越快。
- 但基数受限于相乘不溢出和输出方便性。用 \(10^9\) 可以直接用
printf的%09d补零输出,非常方便。
📝 总结确定 BASE 的步骤
- 确定暂存乘积的类型(通常是
long long)。 - 确保 \((BASE-1)^2\) 加上可能的最大进位(通常不超过 \(BASE\))小于该类型的最大值。
- 优先选 \(10^k\) 以便输出时按位补零。
- 对于 64 位有符号整型,\(10^9\) 是最常用、最安全的选择。
(4)n[i] ^ 48 为什么等效且略快?
字符编码背景
字符 '0' 到 '9' 在 ASCII 表中的编码值十进制是 48 到 57。
二进制表示如下:
'0' = 48 = 0011 0000
'1' = 49 = 0011 0001
'2' = 50 = 0011 0010
...
'9' = 57 = 0011 1001
48 的二进制是 0011 0000,高四位 0011,低四位 0000。
n[i] - '0' 的原理
用 ASCII 码值减去 '0' 的码值(48),得到 0~9。这是标准的整型减法。
n[i] ^ 48 的原理
异或运算(XOR)的规则:相同为 0,不同为 1。
n[i]的高四位是0011,与48的高四位0011相同,异或结果的高四位变成0000。n[i]的低四位恰好就是它代表的数字的二进制值(0~9),例如'5'的低四位是0101。48的低四位是0000,任何数与0000异或,保持不变。- 所以
n[i] ^ 48的效果:保留低四位(正好是 0~9),高四位清零。
结果:'0'^48=0, '1'^48=1, ..., '9'^48=9,完美等价于减 48。
为什么“略快”?
- 位运算(异或)在 CPU 中是非常基础的逻辑门操作,延迟极低,通常只需 1 个时钟周期。
- 减法需要做借位传递,硬件上比异或复杂一些,虽然现代编译器可能会将
- 48优化成某种快速操作,但不一定是位运算。 - 在某些古老或简单的嵌入式编译器中,
^48确实会生成更紧凑的代码。 - 在竞赛环境中(O2 优化开启),差距微乎其微,两者都可以认为是等价的。
实践中,这种“奇技淫巧”了解即可,用 - '0' 可读性更好,更适合团队协作。但在高强度竞赛中,许多选手仍会保留 ^48 的习惯,作为一种速度上的“心理安慰”和对底层世界的致敬。
判断是否需要高精度,核心就看一件事:运算结果会不会超出语言内置整数类型的范围。以下是 C++ 的实用判断标准,Python 用户可以直接跳过,因为 Python 的 int 自动支持任意大数。
1. 先记下 C++ 内置类型的上限
| 类型 | 最大值 | 大约范围 | 能安全存储的十进制位数 |
|---|---|---|---|
int (32位) |
\(2^{31}-1\) | \(2.147\times 10^9\) | 9 位 |
unsigned int |
\(2^{32}-1\) | \(4.295\times 10^9\) | 9 位 |
long long (64位) |
\(2^{63}-1\) | \(9.223\times 10^{18}\) | 18 位 |
unsigned long long |
\(2^{64}-1\) | \(1.844\times 10^{19}\) | 19 位 |
规律:数值的位数 > 对应类型的安全位数,就必须用高精度。
2. 加法、减法、乘法的具体判断
✅ 加法(A + B)
- 只看 两个加数的位数。
- 如果任一加数的位数 ≥ 19 位(即字符串长度 ≥ 19),
unsigned long long也存不下,直接上高精度。 - 如果位数都 ≤ 18,可以直接用
long long计算,但要注意相加可能进位多一位,所以严格说:
若max(lenA, lenB) ≤ 18,通常可以用long long安全相加;若其中一个是 18 位且最高位较大,稳妥起见用unsigned long long,否则转高精度。
简记:输入数字的字符串长度 ≥ 19 → 必用高精度。长度 ≤ 18 → 用
long long加法即可(注意开long long)。
✅ 减法(A - B)
- 前提是结果非负(或手动判负号)。规则与加法几乎一致。
- 输入数字字符串长度 ≥ 19 → 高精度。
- 长度 ≤ 18 →
long long直接减,但要保证不出现负数越界(题目一般保证 A ≥ B)。
✅ 乘法(A × B)
乘法增长极快,判断标准更严格:两个数的位数之和决定结果大小。
- 乘积的位数 ≈
lenA + lenB(或lenA + lenB - 1)。 long long最多安全存储 18 位结果。- 因此:如果
lenA + lenB > 18,就必须用高精度(或压位)。 - 如果
lenA + lenB ≤ 18,可以用long long直接乘(注意中间乘积也要用long long承接)。
简记:两个数字符串长度相加超过 18 → 必用高精度。否则看情况。
3. 实战速查表
| 场景 | 字符串长度条件 | 能否用 long long |
|---|---|---|
| A + B | max(lenA, lenB) ≤ 18 | ✅ 可以(注意进位可能到 19 位,但极少) |
| A - B | max(lenA, lenB) ≤ 18 | ✅ 可以 |
| A × B | lenA + lenB ≤ 18 | ✅ 可以 |
| 以上任一条件不满足 | 超出 | ❌ 必须高精度 |
特例提醒:
- 哪怕乘数很小,例如 \(10^{10} \times 10^{10}\) 位数之和 = 20,超出
long long,必须高精度。 - 阶乘类(如 P1009)几乎必然超出,因为阶乘增长极快,n=13 时 13! 就达到 62 亿,超出 32 位
int;n=21 时 21! 超过long long范围。所以阶乘题目 直接上高精度,不必犹豫。
4. Python 用户的一句提醒
a = int(input())
b = int(input())
print(a + b) # 永远不溢出
Python 的 int 自带高精度,不需要考虑范围。只有当你刻意练习高精度原理时,才需要像 C++ 那样手动模拟。

浙公网安备 33010602011771号