1 条题解
-
0
思路与算法
把两个十进制整数的数位倒序,分别作为多项式A和B的系数。乘积多项式系数c[k]表示所有下标之和为k的数位乘积之和。先精确求卷积,再从低位向高位做十进制进位,最后反向输出,即可避免逐位竖式乘法的平方级复杂度。
这里使用模数998244353、原根3的数论变换NTT。取变换长度L为不小于lenA+lenB-1的最小二次幂,补零后正变换、逐点相乘、逆变换。两数各最多199999位,每个原始数位至多9,因此每个真实卷积系数不超过81*199999=16199919,严格小于模数。单模结果就是未取模的真实系数,无需CRT,也不存在浮点舍入误差。最大变换长度524288,能整除模数减一。
进位从0开始,当前位置value=c[k]+carry,输出数位value%10,后续carry=value/10。处理完系数后继续输出剩余进位,去掉最高多余零,反向即为普通十进制表示。参考程序也规范化前导零。
正确性说明
十进制展开的乘法分配律给出c[k]=sum(a[i]*b[j]),其中i+j=k,所以先卷积再进位与整数乘法等价。补零长度至少为lenA+lenB-1,循环卷积不会发生首尾混叠。
模数经逐个试除至平方根验证为质数,模数减一为2^23717,原根3对不同素因子2、7、17的幂检查均不为1,因此所用二次幂长度存在本原单位根。NTT正逆变换利用单位根的正交关系互逆,逐点相乘对应模域卷积。每个真实系数非负且严格小于模数,因此逆变换给出的剩余类唯一对应真实整数系数。
进位循环保持已经输出的低位,加上尚未处理的系数和进位,表示原乘积不变。每次除以10把该位分离,直到全部系数和进位消耗完,就得到精确乘积。删除高位多余零只改变表示、不改变数值,故最终输出正确。
复杂度
令两数数位总长为D,变换长度L是覆盖D-1的二次幂,故L=O(D)。构造并补零两个数组O(L),位逆序置换O(L),三个变换各O(L log L),逐点相乘O(L),进位、去零与输出O(D)。模幂求各层单位根至多O(log L*log p),p固定,包含在上述总界内。总时间O(L log L),额外空间O(L),包括两个变换数组、输入及结果字符串。初始化数组的O(L)开销已计入。
最大L=524288;两个int数组共4MiB左右,原题128MiB内实际验证。蝶形加法小于2p且小于有符号32位上界;乘法以long long完成,p²小于有符号64位上界。最终标程须按原1秒限制验证,不能缩短最大位数冒充完整范围。
易错点
- 本题是最多199999位,不能套用百位或2000位版本的竖式乘法数据。
- 卷积系数还不是十进制数位,必须逐位进位并继续处理最后的carry。
- 变换长度覆盖lenA+lenB-1,不能少一个有效高位;逆变换须乘L的模逆。
- 单模可行依赖系数上界严格小于模数。本范围已证明,不可无条件推广到更大进制或更大长度。
- 处理前导零后仍须保留至少一个字符。输入声明为正整数,测试不加入非法零值。
- 覆盖最大长度全9长进位、双随机最大长度、不同长度、二次幂边界和长前导零。独立答案来自Python任意精度整数,最大全9还以闭式公式再次验证,数据非官方原版。
参考代码(C++11)
#include <algorithm> #include <iostream> #include <string> #include <vector> using namespace std; const int MOD = 998244353, ROOT = 3; int power(int a, int n) { int r = 1; while (n) { if (n & 1) r = (long long)r * a % MOD; a = (long long)a * a % MOD; n >>= 1; } return r; } void ntt(vector<int> &a, bool invert) { int n = a.size(); for (int i = 1, j = 0; i < n; ++i) { int bit = n >> 1; while (j & bit) { j ^= bit; bit >>= 1; } j ^= bit; if (i < j) swap(a[i], a[j]); } for (int len = 2; len <= n; len <<= 1) { int root = power(ROOT, (MOD - 1) / len); if (invert) root = power(root, MOD - 2); for (int i = 0; i < n; i += len) { int w = 1; for (int j = 0; j < len / 2; ++j) { int u = a[i + j], v = (long long)a[i + j + len / 2] * w % MOD; int sum = u + v; if (sum >= MOD) sum -= MOD; int diff = u - v; if (diff < 0) diff += MOD; a[i + j] = sum; a[i + j + len / 2] = diff; w = (long long)w * root % MOD; } } } if (invert) { int inv = power(n, MOD - 2); for (int &x : a) x = (long long)x * inv % MOD; } } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); string s, t; cin >> s >> t; s.erase(0, min(s.find_first_not_of('0'), s.size() - 1)); t.erase(0, min(t.find_first_not_of('0'), t.size() - 1)); if (s == "0" || t == "0") { cout << "0\n"; return 0; } int size = 1; while (size < (int)(s.size() + t.size() - 1)) size <<= 1; vector<int> a(size), b(size); for (int i = 0; i < (int)s.size(); ++i) a[i] = s[s.size() - 1 - i] - '0'; for (int i = 0; i < (int)t.size(); ++i) b[i] = t[t.size() - 1 - i] - '0'; ntt(a, false); ntt(b, false); for (int i = 0; i < size; ++i) a[i] = (long long)a[i] * b[i] % MOD; ntt(a, true); string out; long long carry = 0; int count = s.size() + t.size() - 1; for (int i = 0; i < count || carry; ++i) { long long value = carry + (i < count ? a[i] : 0); out.push_back('0' + value % 10); carry = value / 10; } while (out.size() > 1 && out.back() == '0') out.pop_back(); reverse(out.begin(), out.end()); cout << out << '\n'; }
- 1