目录
1 Tomohiko Sakamoto 算法
1.1 代码
int dow(int y, int m, int d) {
static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y -= m < 3;
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}
Tomohiko Sakamoto 还发布了另外一个更简洁的版本:
dow(m,d,y) { y-=m<3; return(y+y/4-y/100+y/400+"-bed=pen+mad."[m]+d)%7; }
1.2 代码赏析
想象一下,你正在策划一场穿越时空的旅行,需要精确地计算出历史上的某一天是星期几,这时候 int dow(int y, int m, int d)
函数就像一位可靠的时空向导,带你穿梭在年、月、日的维度中,精准地找到对应的星期几。
static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
这个静态数组 t
,就像是一个神秘的魔法盒子,里面装着每个月的偏移量“魔法药剂”,这些药剂是 Tomohiko Sakamoto 算法精心调配的,为后续的计算提供关键的辅助能量。
y -= m < 3;
这行代码就像是一位机智的时空调整师,当月份是 1 月或 2 月时,它巧妙地将年份减去 1,把它们视为上一年的 13 月和 14 月,为后续的计算铺平道路。
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
这是算法的核心魔法咒语,将年份的修正值、月份的偏移量、日期等因素汇聚在一起,经过神秘的模 7 运算后,就能变出 0 到 6 的数字,分别代表着星期日到星期六,仿佛打开了一扇通往星期几世界的大门。
再看 dow(m,d,y) { y-=m<3; return(y+y/4-y/100+y/400+"-bed=pen+mad."[m]+d)%7; }
这个版本,它就像是一位精简主义魔法师,用更简洁的咒语实现了相同的功能。"-bed=pen+mad."[m]
这部分,如同从魔法书中摘取的神秘字符,利用字符串中字符的 ASCII 码作为偏移量,巧妙地替代了数组,让代码变得更加简洁灵动,仿佛给算法施加了一层隐形的魔法外衣,使其在代码世界中更加轻盈飘逸。
1.3 算法优势
-
简洁性:就像是一首精炼的诗,寥寥数行代码就勾勒出了计算星期几的完整轮廓,让人一读就懂,减少了在代码丛林中迷路的风险,也方便在不同的程序花园中快速移植和栽培。
-
高效性:时间复杂度是 O(1),如同一位瞬间移动的忍者,无论面对多么庞大复杂的日期迷宫,都能在眨眼间找到答案,不会因为日期的遥远或接近而有丝毫迟疑。
-
易于实现:它使用的数学运算都是基础的加减乘除和取模,就像是用简单的积木搭建起了一座精巧的城堡,即使是没有太多数学魔法基础的初学者,也能轻松上手,按照步骤搭建出自己的星期几计算器。
-
不需要额外的数据结构:除了一个小小的静态数组(在第一个版本中),它几乎不依赖任何额外的存储空间,就像是一位身怀绝技的侠客,只带着最基本的行囊就能走遍天下,大大节省了存储资源。
-
适用性广泛:无论是古老的历史遗迹中的日期,还是遥远未来的某个纪念日,只要输入的年、月、日是合法的,它都能准确无误地计算出对应的星期几,不依赖于特定的日期库或系统时间函数,就像是一位全能的时空通,能在各种平台和环境下施展魔法。
-
性能稳定:由于时间复杂度是常数,它的性能就像是一条平稳的河流,不会因为输入日期的波动而产生湍流,即使在大量调用它的程序中,也能保持稳定的输出,不会成为程序性能的瓶颈,确保整个程序的运行如同在光滑的轨道上飞驰。
1.4 算法应用
#include <stdio.h>
int dow(int y, int m, int d) {
static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y -= m < 3;
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}
int main() {
int year = 2025;
int month = 1;
int day = 15;
int result = dow(year, month, day);
// 输出结果,0 表示星期日,1 表示星期一,以此类推
printf("The day of the week for %d-%d-%d is %d\n", year, month, day, result);
return 0;
}
这段代码就像是一场精彩的时空探险之旅的起点。dow
函数是我们的时空探测器,它用存储每个月偏移量的静态数组 t
作为探测工具,对 1 月和 2 月进行特殊处理,将它们视为上一年的 13 月和 14 月,然后结合年份的闰年调整、月份的偏移量、日期和取模 7 运算,精准地探测出结果,为我们揭晓 2025 年 1 月 15 日是星期几的秘密。
总之,Tomohiko Sakamoto 算法就像是一位多面手的魔法师,在各种需要计算日期是星期几的奇幻场景中都能大显身手,从简单的日历魔法阵到复杂的日程管理魔法塔,都能凭借其简洁、高效、易于实现和广泛的适用性,帮助魔法师们(开发者)节省大量的时间和精力,让程序运行得更加顺畅,仿佛给程序注入了灵动的魔法能量。
2 线性求逆元
2.1 代码
for (int i = 2; i < MAXN; i++)
inv[i] = mul(inv[mod%i], mod - mod / i, mod);
2.2 代码赏析
这段代码就像是在神秘的数论迷宫中,用一根神奇的线性魔杖,以 O(n) 的时间复杂度,快速点亮了 1 到 MAXN - 1 对模 mod 的逆元宝藏。
实现原理就像是在解开一个古老的数论封印。对于任意整数 a 和 m,如果 a 在模 m 意义下存在逆元 x,则满足 ax ≡ 1 (mod m),这就像是一道神秘的数论符文,可以表示为 ax = 1 + km(k 为整数),即 ax - km = 1。扩展欧几里得算法就像是古老的数论密钥,可以求解这个线性同余方程,得到 a 的逆元 x。
而这里的递推公式 inv[i] = mul(inv[mod%i], mod - mod / i, mod);
则像是一个巧妙的数论魔法阵。inv
是存储逆元的魔法容器,inv[i]
表示 i 对模 mod 的逆元。mod % i
得到 i 对 mod 取余的结果,这是在迷宫中找到下一个线索的关键步骤。mod - mod / i
是计算过程中的一个神秘中间值。通过假设 mod = k * i + r
(其中 k = mod / i
,r = mod % i
),就能推导出 inv[i]
可以通过 inv[mod % i]
来计算,仿佛在迷宫中找到了一条通往宝藏的隐藏通道。
2.3 算法优势
-
时间复杂度优势:复杂度为 O(n),就像是一辆高速列车,在数论的轨道上飞驰而过,快速地计算出大量逆元,节省了宝贵的时间。
-
代码简洁性:核心代码就像是一句简洁的魔法咒语,
for (int i = 2; i < MAXN; i++) inv[i] = mul(inv[mod%i], mod - mod / i, mod);
这样简洁的结构,让人一眼就能看懂,只需要维护一个数组inv
存储逆元结果,并结合一个乘法取模函数mul
就能完成计算,减少了代码的复杂性和出错的可能性,就像是一本简洁明了的魔法手册,方便魔法师们(开发者)随时查阅和使用。 -
批量计算便利性:在许多数论和组合数学的魔法仪式中,常常需要对多个数求模 m 的逆元。例如在计算组合数对模 m 取模时,需要计算 n!、k! 和 (n - k)! 的逆元。使用线性求逆元算法,可以提前计算出 1 到 max(n, k) 的逆元数组,然后直接从数组中获取所需的逆元,而不需要多次调用扩展欧几里得算法进行计算,提高了计算组合数取模的效率,就像是一位聪明的魔法师提前准备好了所有需要的魔法材料,使得魔法仪式能够顺利进行。
-
预处理能力:可以预先计算并存储逆元数组,以便在后续的计算中重复使用。例如在动态规划的魔法迷宫中,如果需要频繁使用到不同数字的逆元,通过预处理得到逆元数组可以提高程序的运行速度。例如,在解决一些涉及分数取模的动态规划问题时,如
dp[i] = (dp[i - 1] * a[i]) % mod
,若a[i]
可能为分数,需要对其求逆元,提前存储的逆元数组可以避免重复计算逆元,就像是一位经验丰富的魔法师提前准备好了各种魔法药剂,随时可以拿来使用,大大提高了魔法施展的效率。 -
适用范围广:适合固定模数,当模数 m 固定时,线性求逆元算法可以快速计算出在该模数下的多个逆元,对于解决同余方程、求解线性方程组、计算多项式系数等数论魔法问题非常有用。在密码学的魔法领域中,例如 RSA 算法的一些实现和计算中,需要计算大质数 p 和 q 下的多个逆元,当 p 和 q 确定时,使用线性求逆元算法可以提高计算效率,就像是一位专门破解密码锁的魔法专家,有了这把神奇的钥匙,就能轻松打开各种密码锁。
-
算法稳定性:确定性和可预测性,线性求逆元算法不依赖于随机化或递归,因此在不同的运行环境(魔法场景)下,结果是稳定的。对于需要在多平台或多语言中实现的代码,其结果的一致性更有保障,就像是一位可靠的魔法使者,无论在哪个魔法王国,都能准确无误地传递魔法信息。
总之,线性求逆元算法就像是一位强大的数论魔法师,在涉及大量求模逆元运算的神秘领域中,凭借其简洁的代码、较低的时间复杂度、批量计算能力和预处理便利性等优势,能够快速、准确地完成各种复杂的数论魔法任务,在数论、组合数学、密码学、算法竞赛等魔法战场上都有着广泛的应用,是每一位数论魔法师不可或缺的得力助手。
3 求最大公因数(辗转相除法)
3.1 代码
int gcd(int x, int y){
return y? gcd(y, x%y) : x;
}
3.2 代码赏析
这段代码就像是一个神奇的数论魔法阵,定义了一个名为 gcd
的魔法函数,用于计算两个整数 x 和 y 的最大公因数(Greatest Common Divisor,简称 GCD),它是数论世界中的一个重要魔法工具。
实现原理就像是在施展一个古老的数论魔法——欧几里得算法(也称为辗转相除法)。这个魔法函数接收两个整数 x 和 y 作为魔法材料。
y? gcd(y, x%y) : x;
这是一个神奇的条件魔法咒语,其含义如下:
-
如果 y 不为 0,那么就继续施展魔法,将 y 作为新的 x,将 x 除以 y 的余数(x % y)作为新的 y,递归调用
gcd
函数,就像是一位魔法师在不断调整魔法材料,寻找最终的魔法答案。 -
如果 y 为 0,那么就返回 x 作为最大公因数,因为根据最大公因数的魔法定义,当 y 为 0 时,x 就是 x 和 y 的最大公因数,就像是找到了魔法宝藏的最终入口。
递归过程就像是在迷宫中不断探索,每次递归调用都像是在迷宫中前进了一步,x 变成 y,y 变成 x % y。由于 x % y 的结果总是小于 y,并且每次调用都会使其中一个参数减小,最终会达到 y 为 0 的情况,就像是在迷宫中找到了出口,此时的 x 就是 a 和 b 的最大公因数,就像是找到了隐藏在迷宫深处的魔法宝石。
3.3 辗转相除法的优势
-
高效性:时间复杂度为 O(log(min(x, y))),其中 x 和 y 是输入的两个数。这意味着,即使对于非常大的整数 x 和 y,这个魔法算法也能在相对较少的步骤内计算出它们的最大公因数。例如,当 x 和 y 是 100 位的大整数时,相比其他魔法算法,辗转相除法所需的步骤数量增长相对缓慢,这在处理大数运算的魔法世界中非常重要,就像是在一场漫长的魔法旅程中,能够快速找到目的地,节省了大量的时间和精力。
-
简洁性和易于实现:这个魔法函数只用几行代码就实现了计算最大公因数的功能,不需要额外的魔法材料(数据结构)或复杂的魔法咒语(逻辑),易于理解和维护,就像是一本简洁明了的魔法手册,即使是初学者也能轻松掌握。
-
通用性:适用于各种数据类型,只要数据类型支持取模运算和比较操作,就可以使用这个魔法算法。无论是整数、多项式还是其他具有除法和取模概念的数据类型,都可以使用这个魔法算法的基本思想来求最大公因数。例如,在多项式环的魔法世界中,也可以用类似的思想求两个多项式的最大公因式,就像是这个魔法算法具有广泛的适用性,能够在不同的魔法领域中施展魔法。
-
理论基础与扩展性:数学理论支持,辗转相除法有着坚实的数学理论基础,基于数论的基本原理。这个魔法算法可以方便地扩展到求解多个数的最大公因数,例如计算 n 个数 a1, a2,..., an 的最大公因数,可以先计算 gcd(a1, a2),然后将结果与 a3 求最大公因数,以此类推,就像是这个魔法算法具有强大的扩展性,能够应对各种复杂的魔法任务。
-
稳定性:稳定的结果,无论输入数字的大小和顺序如何,辗转相除法都能正确计算出最大公因数。例如,计算 gcd(10, 5) 和 gcd(5, 10) 都会得到相同的结果,保证了魔法算法的正确性和稳定性,就像是一位可靠的魔法使者,无论在何种情况下,都能准确无误地完成魔法任务。
-
应用广泛:在数论与算法的魔法世界中,辗转相除法是许多魔法算法的基础,如求解线性同余方程、中国剩余定理等。在密码学的魔法领域中,用于 RSA 算法的密钥生成和加密解密过程中,需要计算大整数的最大公因数,辗转相除法可以有效地完成此任务。在分数化简的魔法场景中,可以用它来化简分数的分子和分母,将分数表示为最简形式,就像是一位魔法裁缝,能够将复杂的分数裁剪成最简洁的形式,方便后续的魔法操作。
3.4 应用示例
#include <stdio.h>
// 辗转相除法求最大公因数
int gcd(int x, int y){
return y? gcd(y, x%y) : x;
}
// 求多个数的最大公因数
int gcd_n(int arr[], int n) {
int result = arr[0];
for (int i = 1; i < n; i++) {
result = gcd(result, arr[i]);
}
return result;
}
// 化简分数
struct Fraction {
int numerator;
int denominator;
};
void simplify(Fraction& f) {
int divisor = gcd(f.numerator, f.denominator);
f.numerator /= divisor;
f.denominator /= divisor;
}
int main() {
// 求两个数的最大公因数
int a = 54, b = 24;
int result = gcd(a, b);
printf("The GCD of %d and %d is %d\n", a, b, result);
// 求多个数的最大公因数
int arr[] = {12, 18, 30};
int n = sizeof(arr) / sizeof(arr[0]);
int gcd_result = gcd_n(arr, n);
printf("The GCD of ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("is %d\n", gcd_result);
// 化简分数
Fraction f = {12, 18};
simplify(f);
printf("The simplified fraction is %d/%d\n", f.numerator, f.denominator);
return 0;
}
在这个魔法示例中:
-
gcd
函数使用辗转相除法计算两个数的最大公因数,就像是在施展一个基础的数论魔法。 -
gcd_n
函数利用gcd
函数计算多个数的最大公因数,就像是在施展一个更复杂的数论魔法,能够处理多个魔法材料。 -
simplify
函数使用gcd
函数将分数化简为最简形式,就像是在施展一个分数魔法,将复杂的分数裁剪成最简洁的形式。
辗转相除法以其高效、简洁、通用、稳定和广泛的应用,成为计算最大公因数的经典魔法算法,是许多数学和计算机科学魔法领域的重要基础魔法,每一位魔法师都应该熟练掌握这个强大的魔法工具。
4 树状数组
4.1 代码
修改:
void add(int i, int x){
for (;i <= n; i += i & -i)
tree[i] += x;
}
查询:
int sum(int i){
int ret = 0;
for(; i; i -= i & -i)ret +=tree[i];
return ret;
}
4.2 代码赏析
这段代码就像是在构建一个神奇的魔法树——树状数组(Binary Indexed Tree,也称为 Fenwick Tree),它具有单点修改和区间求和的魔法功能。
实现细节:
-
add
函数:-
用于更新树状数组中第 i 个位置的值,就像是在魔法树上施加一个魔法咒语,更新某个节点的魔法能量。
-
i & -i
是一个非常重要的魔法操作,它可以得到 i 的最低位的 1 及其后面的 0 组成的数,这个操作在树状数组中被称为lowbit
操作,就像是在魔法树上找到某个节点的魔法路径。 -
循环中,将 i 不断加上
i & -i
,并更新tree[i]
的值,直到 i 超过 n。这样的操作会更新树状数组中所有与 i 相关的父节点,就像是在魔法树上沿着路径更新所有相关的魔法节点。
-
-
sum
函数:-
用于计算前 i 个元素的前缀和,就像是在魔法树上计算某个节点及其所有子节点的魔法能量总和。
-
同样使用
i & -i
操作,在循环中,将 i 不断减去i & -i
,并累加上tree[i]
的值,直到 i 为 0。这样的操作会将所有与 i 相关的子节点的值累加到ret
中,就像是在魔法树上沿着路径收集所有子节点的魔法能量。
-
代码示例:
#include <iostream>
#include <vector>
using namespace std;
const int n = 100;
vector<int> tree(n + 1, 0);
// 单点修改
void add(int i, int x){
for (;i <= n; i += i & -i)
tree[i] += x;
}
// 前缀和查询
int sum(int i){
int ret = 0;
for(; i; i -= i & -i)
ret += tree[i];
return ret;
}
int main() {
// 对第 5 个位置的元素加 10
add(5, 10);
// 计算前 8 个元素的和
int s = sum(8);
cout << "The sum of the first 8 elements is: " << s << endl;
return 0;
}
代码解释:
-
初始化一个大小为 n + 1 的树状数组,初始值都为 0,就像是在魔法森林中种下了一棵初始的魔法树。
-
调用
add
函数,将第 5 个位置的元素增加 10。具体来说,它会更新tree[5]
以及与 5 相关的父节点的值,就像是在魔法树上施加了一个魔法咒语,更新了某个节点及其所有父节点的魔法能量。 -
调用
sum
函数,计算前 8 个元素的和。它会从tree[8]
开始,不断减去lowbit
,并累加tree[i]
的值,直到 i 为 0,就像是在魔法树上沿着路径收集所有子节点的魔法能量。
使用说明:
-
初始化树状数组:将
tree
数组初始化为 0,就像是在魔法森林中种下了一棵初始的魔法树。 -
单点修改:使用
add
函数修改某个位置的值,如add(5, 10)
表示将第 5 个位置的元素增加 10,就像是在魔法树上施加了一个魔法咒语,更新了某个节点及其所有父节点的魔法能量。 -
区间求和:使用
sum
函数计算前缀和,如sum(8)
计算前 8 个元素的和,就像是在魔法树上沿着路径收集所有子节点的魔法能量。
时间复杂度:
-
单点修改:O(log n),因为
add
函数中,i 每次加上i & -i
,最多会进行 log n 次操作。 -
区间求和:O(log n),因为
sum
函数中,i 每次减去i & -i
,最多会进行 log n 次操作。
4.3 算法优势
-
时间复杂度:高效的操作,单点修改和区间求和的时间复杂度都是 O(log n),在需要频繁进行单点修改和区间查询的魔法场景中,比朴素的数组操作(单点修改 O(1) 但区间求和 O(n))或前缀和数组(单点修改 O(n) 但区间求和 O(1))更具优势,特别是在处理大规模数据的魔法任务中,性能提升显著,就像是在一场大规模的魔法战斗中,能够快速施展各种魔法,占据优势。
-
空间复杂度:节省空间,树状数组的空间复杂度为 O(n),仅需要存储 n + 1 个元素的数组
tree
,与原始数组大小相同,不多用额外的一字节存储空间。这使得它在空间使用上非常高效,对于内存有限的魔法场景或者大规模数据的存储是非常有利的,就像是在魔法背包中节省了宝贵的空间,可以携带更多的魔法材料。 -
实现简洁:代码实现非常简洁,核心的
add
和sum
函数都可以用几行代码实现。这种简洁性不仅易于理解和实现,而且降低了代码出错的可能性,同时方便维护和扩展,就像是有一本简洁明了的魔法手册,方便魔法师们随时查阅和使用。void add(int i, int x){ for (;i <= n; i += i & -i) tree[i] += x; } int sum(int i){ int ret = 0; for(; i; i -= i & -i) ret += tree[i]; return ret; }
-
易于维护:动态修改支持,树状数组支持动态修改元素,在修改元素后,仍能快速计算区间和,而无需像前缀和数组那样重新计算整个前缀和数组。这对于在线算法(边修改边查询)非常重要,比如在一些动态更新元素并需要频繁查询区间和的魔法场景,如在线评测系统中更新分数并计算排名等,就像是在一场动态的魔法比赛中,能够随时调整魔法策略并快速得到结果。
-
应用广泛:多种应用场景,除了基本的单点修改和区间求和,树状数组还可以用于求解逆序对问题。通过将元素按一定顺序插入树状数组,并计算比当前元素大或小的元素个数,可以得到逆序对的数量,就像是在魔法世界中,能够解决各种复杂的魔法问题。还可以用于计算动态维护的频率数组,比如统计元素出现的频率并进行区间频率的查询,就像是在魔法森林中,能够随时统计各种魔法生物的出现频率。
4.4 算法应用
以下是一个使用树状数组求解实际问题的例子,例如求解一个数组的前缀和:
#include <iostream>
#include <vector>
using namespace std;
const int n = 10;
vector<int> arr = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21};
vector<int> tree(n + 1, 0);
// 构建树状数组
void build() {
for (int i = 1; i <= n; ++i) {
add(i, arr[i - 1]);
}
}
// 单点修改
void add(int i, int x){
for (;i <= n; i += i & -i)
tree[i] += x;
}
// 前缀和查询
int sum(int i){
int ret = 0;
for(; i; i -= i & -i)
ret += tree[i];
return ret;
}
// 区间求和 [l, r]
int query(int l, int r) {
return sum(r) - sum(l - 1);
}
int main() {
build();
// 查询 [3, 7] 的区间和
int s = query(3, 7);
cout << "The sum of elements from 3 to 7 is: " << s << endl;
return 0;
}
代码解释:
-
build
函数用于将原数组arr
的元素更新到树状数组中,就像是在魔法森林中,将各种魔法材料收集到魔法树中。 -
query
函数用于计算[l, r]
的区间和,通过sum(r) - sum(l - 1)
实现,就像是在魔法树上计算某个区间内所有节点的魔法能量总和。
树状数组在处理单点修改和区间求和问题上具有高效性,其时间复杂度为 O(log n),空间复杂度为 O(n),在算法竞赛、数据结构设计等魔法领域有广泛的应用,特别是对于需要频繁进行单点修改和区间查询的魔法场景,它可以有效地提高计算效率,就像是在一场复杂的魔法战斗中,能够快速施展各种魔法,占据优势。
5 快速傅里叶变换
5.1 代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int mod = 998244353; // 常用的模数,满足 p = k * 2^n + 1 的形式
const int g = 3; // 原根
// 快速幂运算,计算 (a^b) % mod
int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1LL * res * a % mod;
a = 1LL * a * a % mod;
b >>= 1;
}
return res;
}
// 反转二进制位
int reverse(int num, int len) {
int res = 0;
for (int i = 0; i < len; ++i) {
if (num & (1 << i)) res |= 1 << (len - 1 - i);
}
return res;
}
// 数论变换 (NTT)
void ntt(vector<int>& a, bool inv) {
int n = a.size();
int logn = 0;
while ((1 << logn) < n) ++logn;
// 位逆序置换
for (int i = 0; i < n; ++i) {
int j = reverse(i, logn);
if (i < j) swap(a[i], a[j]);
}
for (int s = 1; s <= logn; ++s) {
int m = 1 << s;
int wm = qpow(g, (mod - 1) / m); // 单位根
if (inv) wm = qpow(wm, mod - 2); // 逆变换时使用逆元
for (int k = 0; k < n; k += m) {
int w = 1;
for (int j = 0; j < m / 2; ++j) {
int u = a[k + j], t = 1LL * w * a[k + j + m / 2] % mod;
a[k + j] = (u + t) % mod;
a[k + j + m / 2] = (u - t + mod) % mod;
w = 1LL * w * wm % mod;
}
}
}
if (inv) {
int invn = qpow(n, mod - 2); // 逆变换时乘以 n 的逆元
for (int i = 0; i < n; ++i) {
a[i] = 1LL * a[i] * invn % mod;
}
}
}
// 多项式乘法
vector<int> polyMultiply(vector<int> a, vector<int> b) {
int n = 1;
int deg = a.size() + b.size() - 1;
while (n < deg) n <<= 1;
a.resize(n);
b.resize(n);
ntt(a, false);
ntt(b, false);
vector<int> c(n);
for (int i = 0; i < n; ++i) {
c[i] = 1LL * a[i] * b[i] % mod;
}
ntt(c, true);
c.resize(deg);
return c;
}
int main() {
vector<int> a = {1, 2, 3}; // 第一个多项式的系数
vector<int> b = {4, 5, 6}; // 第二个多项式的系数
vector<int> c = polyMultiply(a, b);
for (int x : c) {
cout << x << " ";
}
cout << endl;
return 0;
}
5.2 代码赏析
首先,选择一个合适的模数 p
和原根 g
,满足 p = k * 2^n + 1
的形式,其中 k
是一个正整数,n
是 2 的幂次。这就像是一位魔法导师在选择合适的魔法材料和魔法工具,为后续的魔法仪式做好准备。
初始化原根 g
的幂次表,即计算 g^i mod p
对于不同的 i
的值。这就像是一位魔法学徒在准备魔法药剂的配方,确保每一步的魔法操作都有精确的指导。
进行位逆序置换,将输入序列按照二进制位反转的顺序重新排列。这就像是一位魔法阵大师在调整魔法阵的布局,确保魔法能量能够顺畅地流动。
进行蝶形运算,利用原根的性质进行迭代计算,将多项式乘法转换为点值表示,然后再转换回来。这就像是一位高级魔法师在施展复杂的魔法咒语,通过一系列精妙的操作,将复杂的魔法问题转化为简单的形式,再还原回去。
实现快速幂运算 (a^b) % mod
,通过二进制分解 b
并利用模运算的性质,将指数 b
转换为二进制表示,当 b
的某一位为 1 时累乘 a
的相应次幂,同时将 a
平方。这就像是一位魔法数学家在进行快速的魔法计算,确保每一步的计算都精确无误。
对一个 num
进行二进制位的反转操作,将 num
的二进制表示反转,结果长度为 len
。这就像是一位魔法编码师在调整魔法代码的顺序,确保魔法信息能够正确传递。
首先进行位逆序置换,将输入序列 a
中的元素按照二进制位反转的顺序重新排列。这就像是一位魔法阵大师在调整魔法阵的布局,确保魔法能量能够顺畅地流动。
进行蝶形运算,对于不同的 s
(表示合并的段数),计算单位根 wm
,并更新 a
中元素的值,根据原根的性质将多项式转换为点值表示。这就像是一位高级魔法师在施展复杂的魔法咒语,通过一系列精妙的操作,将复杂的魔法问题转化为简单的形式。
对于逆变换(inv
为 true
),最后需要乘以 n
的逆元,将点值表示转换回系数表示。这就像是一位魔法导师在完成魔法仪式的最后一步,将魔法能量还原为最初的形态。
首先将两个多项式 a
和 b
的长度扩展到大于等于 a.size() + b.size() - 1
的最小的 2 的幂次。这就像是一位魔法材料准备师在确保所有的魔法材料都准备齐全,以便进行下一步的魔法操作。
对 a
和 b
进行 NTT 变换。这就像是一位魔法阵大师在启动魔法阵,将多项式从系数表示转换为点值表示。
对变换后的 a
和 b
进行逐点相乘。这就像是一位魔法工匠在将两个魔法物品进行融合,创造出新的魔法物品。
对结果进行逆 NTT 变换,得到多项式乘法的结果。这就像是一位魔法导师在完成魔法仪式的最后一步,将新的魔法物品还原为最初的形态。
使用说明:
-
你可以修改
mod
和g
,但要确保mod
是p = k * 2^n + 1
的形式,g
是mod
的原根。这就像是一位魔法导师在选择合适的魔法材料和魔法工具,确保魔法仪式能够顺利进行。 -
对于输入的多项式,使用
vector<int>
存储其系数。这就像是一位魔法学徒在准备魔法材料,确保每一步的魔法操作都有精确的指导。 -
调用
polyMultiply
函数进行多项式乘法,它会返回结果多项式的系数。这就像是一位魔法工匠在完成魔法物品的制作,创造出新的魔法物品。
NTT 利用了数论的性质,将傅里叶变换中的复数运算替换为模 mod
下的整数运算,避免了浮点数运算带来的精度问题,适用于解决多项式乘法、大数乘法等问题,尤其在算法竞赛和一些需要精确计算的魔法场景中非常有用。需要注意的是,选择合适的 mod
和 g
对于 NTT 的性能和正确性至关重要,就像是选择合适的魔法材料和魔法工具对于魔法仪式的成功至关重要。此外,在处理大模数和大次数多项式时,需要注意溢出问题,代码中使用了 1LL
来避免乘法溢出,就像是在魔法仪式中使用了特殊的魔法符文,防止魔法能量溢出。
5.3 算法优势
-
避免浮点数误差:精确计算,NTT 使用的是数论中的模运算,在整数环上进行计算,避免了传统快速傅里叶变换(FFT)中使用复数进行计算所带来的浮点数运算的精度误差。在需要精确结果的魔法场景中,如密码学、精确多项式乘法计算等,这是一个显著的优势,就像是在一场需要精确控制魔法能量的魔法战斗中,能够确保每一步的魔法操作都精确无误。
-
时间复杂度:高效性,与传统的多项式乘法的 O(n^2) 时间复杂度相比,NTT 的时间复杂度为 O(n log n),其中 n 是多项式的次数。这使得 NTT 可以高效地处理大规模多项式乘法问题,通过将多项式从系数表示转换为点值表示,利用原根的性质进行快速计算,在 n 较大时,相比暴力多项式乘法,性能提升显著,就像是在一场大规模的魔法战斗中,能够快速施展各种复杂的魔法,占据优势。
-
适用范围:整数环上的运算,NTT 特别适用于处理在有限域或整数环上的多项式乘法,对于许多涉及整数运算的魔法算法和应用,如密码学中的多项式操作、编码理论等,NTT 提供了一个很好的工具。在一些密码学算法中,如 RSA 算法的优化、一些基于多项式的加密方案等,NTT 可以在保持精度的同时实现高效的计算,就像是在密码学的魔法领域中,能够快速破解各种复杂的密码锁。
-
可并行性:并行计算潜力,NTT 的蝶形运算可以并行化,在现代多核处理器或 GPU 计算中,可以利用并行计算能力加速 NTT 的计算。蝶形运算中,不同的迭代层次和分组可以独立进行,这使得 NTT 具有良好的并行性,可以通过并行编程技术(如 OpenMP、CUDA 等)来提高计算速度,就像是在一场大规模的魔法战斗中,能够同时施展多个魔法咒语,大大提高战斗效率。
-
稳定性:结果的确定性,对于相同的输入和模数,NTT 的结果是确定的,不会因为浮点数运算的不确定性而产生不同的结果,这在一些需要可重复性和稳定性的魔法场景中非常重要,如在加密算法中保证结果的一致性,就像是在一场需要精确控制魔法能量的魔法战斗中,能够确保每一步的魔法操作都精确无误,保证魔法仪式的成功。
5.4 算法应用
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int mod = 998244353; // 常用的模数,满足 p = k * 2^n + 1 的形式
const int g = 3; // 原根
// 快速幂运算,计算 (a^b) % mod
int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = 1LL * res * a % mod;
a = 1LL * a * a % mod;
b >>= 1;
}
return res;
}
// 反转二进制位
int reverse(int num, int len) {
int res = 0;
for (int i = 0; i < len; ++i) {
if (num & (1 << i)) res |= 1 << (len - 1 - i);
}
return res;
}
// 数论变换 (NTT)
void ntt(vector<int>& a, bool inv) {
int n = a.size();
int logn = 0;
while ((1 << logn) < n) ++logn;
// 位逆序置换
for (int i = 0; i < n; ++i) {
int j = reverse(i, logn);
if (i < j) swap(a[i], a[j]);
}
for (int s = 1; s <= logn; ++s) {
int m = 1 << s;
int wm = qpow(g, (mod - 1) / m); // 单位根
if (inv) wm = qpow(wm, mod - 2); // 逆变换时使用逆元
for (int k = 0; k < n; k += m) {
int w = 1;
for (int j = 0; j < m / 2; ++j) {
int u = a[k + j], t = 1LL * w * a[k + j + m / 2] % mod;
a[k + j] = (u + t) % mod;
a[k + j + m / 2] = (u - t + mod) % mod;
w = 1LL * w * wm % mod;
}
}
}
if (inv) {
int invn = qpow(n, mod - 2); // 逆变换时乘以 n 的逆元
for (int i = 0; i < n; ++i) {
a[i] = 1LL * a[i] * invn % mod;
}
}
}
// 多项式乘法
vector<int> polyMultiply(vector<int> a, vector<int> b) {
int n = 1;
int deg = a.size() + b.size() - 1;
while (n < deg) n <<= 1;
a.resize(n);
b.resize(n);
ntt(a, false);
ntt(b, false);
vector<int> c(n);
for (int i = 0; i < n; ++i) {
c[i] = 1LL * a[i] * b[i] % mod;
}
ntt(c, true);
c.resize(deg);
return c;
}
int main() {
vector<int> a = {1, 2, 3}; // 第一个多项式的系数
vector<int> b = {4, 5, 6}; // 第二个多项式的系数
vector<int> c = polyMultiply(a, b);
for (int x : c) {
cout << x << " ";
}
cout << endl;
return 0;
}
代码解释:
-
qpow
函数:实现快速幂运算,用于计算原根的幂次和n
的逆元,就像是在进行快速的魔法计算,确保每一步的魔法操作都精确无误。 -
reverse
函数:对数字进行二进制位反转,用于位逆序置换,就像是在调整魔法阵的布局,确保魔法能量能够顺畅地流动。 -
ntt
函数:-
进行 NTT 变换,通过位逆序置换和蝶形运算将多项式从系数表示转换为点值表示,对于逆变换,会乘以
n
的逆元,就像是在完成魔法仪式的最后一步,将魔法能量还原为最初的形态。 -
蝶形运算中,利用原根的性质进行高效计算,就像是在施展复杂的魔法咒语,通过一系列精妙的操作,将复杂的魔法问题转化为简单的形式。
-
-
polyMultiply
函数:-
将两个多项式
a
和b
扩展到合适的长度(最小的 2 的幂次),就像是在准备魔法材料,确保每一步的魔法操作都有精确的指导。 -
进行 NTT 变换,逐点相乘,再进行逆 NTT 变换得到结果多项式,就像是在完成魔法物品的制作,创造出新的魔法物品。
-
通过利用 NTT 的上述优势,我们可以在许多领域实现高效、精确的多项式乘法和相关的数论计算,尤其在处理大规模数据和需要精确结果的魔法场景中展现出强大的性能,就像是在一场大规模的魔法战斗中,能够快速施展各种复杂的魔法,占据优势。
希望这些有趣的描述能让你更好地理解和欣赏这些算法的魅力!
往期精彩
数据科学与SQL:如何计算Teager能量算子(TEO)?| 基于SQL实现
SQL进阶技巧:如何取时间序列最新完成状态的前一个状态并将完成状态的过程进行合并?