数学算法汇总
数学算法介绍
主要介绍一些数学相关的算法。
多项式
- DFT:离散傅里叶变换,方式有两种, FFT 和 NTT。
- IDFT:逆离散傅里叶变换。
FFT 多项式乘法
最开始学这玩意的时候感觉非常迷,后面数学水平上去了其实也不难。
多项式有两种表示方法,一种是系数法,另一种是点值法,总所周知 \(n\) 个不同点唯一确定一个 \(n-1\) 次多项式。
原理:多项式的两个系数表达式相乘是 \(O(n^2)\) 的,但是其点值表达式相乘却是 \(O(n)\) 的,所以考虑将系数表达式转成点值表达式然后相乘。
事实上,点值表达式和系数表达式的互相转化,如果点值取特殊点,可以做到 \(O(n\log n)\),即使是任意点,即多项式多点求值和多项式多点插值也可以做到 \(O(n \log^2n)\)
设最终多项式次数为 \(n-1\),我们进行多项式乘法时选择的点值叫单位根,即 \(x^n=1\) 在复数域上的所有根。
这玩意有一些性质,不过我们得先把次数变为 \(n=2^k\) 形式。
无法想象发明这个东西的人是怎么想到的,可能这就是被记在历史书上的人的水平。
以下内容如果将坐标系视为极坐标系会更好理解 $$ \[\begin{align} W_n^i &= -W_n^{i+\frac{n}{2}}\\ W_{\frac{n}{2}}^{i} &= W_{n}^{2i}\\ \end{align}\] $$ 考虑这样一个问题,对于一个多项式 \(a_0+a_1x+a_2x^2 \cdots a_{n-1}x^{n-1}\) ,我们需要同时求出它在 \(W_n^{0},W_n^{1}\cdots W_n^{n-1}\) 处的取值。发现由于第一个性质,貌似可以偷个懒,因为后 \(\frac{n}{2}\) 个 \(W_i\) 就是前 \(\frac{n}{2}\) 个数的相反数。
相反数的性质,奇数次幂的符号要改变。考虑对系数按奇偶性分类,式子变成了这个样子。
\((a_0+a_2x^2+\cdots +a_{n-2}x^{n-2})+x(a_1+a_3x^2+\cdots + a_{n-1}x^{n-2})\)
然后考虑前后两个部分,都是一个多项式,需要对他们各自求 \(x=W_\frac{2}{n}^0,W_\frac{2}{n}^1\cdots W_\frac{2}{n}^{\frac{n}{2}-1}\) 处的取值,本质上是求 \(x^2=W_{\frac{2}{n}}^0,W_{\frac{2}{n}}^2\cdots W_{\frac{2}{n}}^{n-2}\) 处的取值,结合第二个性质,woc,就是两个子问题,解决之后就可以 \(O(n)\) 得到原问题的解,边界显然是 \(n=1\)。
复杂度 \(T(n)=2T(\frac{n}{2}) + O(n) = O(n\log n)\)
1 | void FFT(int now,com *a,int op) |
但是如果递归的话,常数会比较拉跨。因为递归必然需要复制数组重新弄成一个下标 \(1-n\) 的问题,无论用什么办法解决,你的高速缓存都会表示意见很大,所以考虑迭代写法。
本质上递归是一层一层合并了两个数组,那么能不能直接模拟这个合并的过程呢,答案是可以的。
观察发现本质上是将下标二进制 reverse
之后逐层合并的,我们也这么做就行。
求 reverse
可以 \(O(n)\),如下(如果你不了解运算顺序,请老老实实打括号)
这个原理很简单,不看最后一位,其它位先
reverse
,然后处理一下最后一位就行。
1 | for(int i=1;i<1<<lim;i++)res[i]=res[i>>1]>>1|(i&1)<<1-lim; |
下面是迭代写法代码,本质是模拟了递归合并的过程。
1 | void FFT(com a[],com b[],int op) |
搞完了系数转点值,接下来是点值转系数。
前人告诉我们只需要将单位根改为 \(W_n^{0},W_n^{-1}\cdots W_n^{-n+1}\),再做一遍系数转点值的过程就可以得到系数,但是系数会变成原来的 \(n\) 倍,除掉就行。
给出简要证明,\(i\) 次项的系数为 \(a_i\),转一次点值
之后变为 \(b_i\),再做一次变成 \(c_i\) \[ \begin{eqnarray} c_x &=& \sum\limits_{i=0}^{n-1}b_iW_n^{-ix} \\ &=& \sum\limits_{i=0}^{n-1} W_n^{-ix}\sum\limits_{j=0}^{n-1}a_jW_n^{ij}\\ &=& \sum\limits_{i=0}^{n-1} \sum\limits_{j=0}^{n-1}a_jW_n^{i(j-x)}\\ \end{eqnarray} \] 对于 \(j=x\),贡献显然为 \(\sum\limits_{i=0}^{n-1}a_xW_n^{i\times0} = na_x\)
对于 \(j\neq x\) 贡献为 \(a_j \sum\limits_{i=0}^{n-1}(W_n^{j-x})^{i}\)
对这个式子的求和用等比数列求和公式有贡献为 \(\dfrac{W_n^{n(j-x)}-1}{W_n^{j-x}-1}\)
显然分子为 \(0\),分母不为 \(0\),所以贡献是 \(0\),所以结果就是 \(na_x\)
搞定。
NTT 多项式乘法
FFT 多项式乘法是由缺陷的,由于浮点数精度和运算速度问题,FFT 可能并不能很好的解决一些问题,所以引入了 NTT,NTT 从有限整数域中找到了这样一组具有同样优秀性质的 \(W_n\),即 \(g\),也就是原根。
原根的内容可以参考数学证明总结中的介绍。
注意,和 FFT 一样,NTT 也需要严格的按照 \(2^k\) 取次数,因为我们利用了 \(W_n^{2i} = W_{\frac{n}{2}}^{i}\) 这一重要性质
所以能取出较大的 \(2^k\) 作为阶的质数才可以作为 NTT 的模数,常见的 NTT 模数是 \(998244353=2^{23}\times 7\times 17 +1\) ,我们可以取它的原根 \(g=3\) 作为基本单位根带入,实际上如果要找到一个应用于 \(n\) 的单位根 \(W_n\),需要取 \(W_n=g^{\frac{p-1}{n}}\)。这样它就满足了我们在 FFT 证明中用到的一切性质。
然后照着 FFT 打一遍就行,只是基本运算这些换为模 \(p\) 意义下的运算就行。
1 | void NTT(int *a,int type) |
其它多项式乘法和一些优化
FFT 三次变两次
FFT 三次变两次,把 \(b\) 扔到 \(a\) 的虚部去,变成了 \(A(x) = (a_0+b_0i) + (a_1+b_1i)x+ \cdots + (a_{n-1}+b_{n-1}i)x^{n-1}\)
然后求 \(A^2(x)\),得到的系数表达式的虚部就是 \(2ab\)。
证明显然,会比 NTT 略快。
任意模数 NTT
NTT 解决的是模意义下的乘法问题,当然,如果确保值域不超出模数,那么模意义下的结果就是正确结果。
如果模数不是 NTT 模数,就需要写任意模数 NTT。
实现方式有两种,其一是拆系数 FFT,其二是多模数 NTT 后用 CRT 合并。
多模数 NTT
一般选取 998244353 1004535809 469726049
作为 NTT
模数,它们的原根都是 3
。分别进行 NTT
后,可以得到在模它们意义下的结果,用中国剩余定理合并,得到一个模约 \(5\times 10^{26}\)
的数的结果,一般来说任意的模数不会超过 \(10^9\),所以乘起来的结果不超过 \(10^{24}\),直接用合并的结果就行。
提一下实现细节。
一种方式是写个 Num 类,里面放三个 int,重写一下加减乘除,正常做。
另一种方式是写个 Poly 类,封装个乘法,用 for 做三次,都比较阳间。
用 CRT 时记得 __int128。或者用一个科技
一共三个方程,考虑这样合并。
其中 \(mod_1^{-1}\) 表示 \(mod_1\) 在 \(mod_2\) 意义下的逆元。 \[ x = x_1 \pmod {mod1} \\ x = x_2 \pmod {mod2} \\ k_1 mod_1 + x_1 = x_2 \pmod{mod2}\\ x = (x_2-x_1)mod_1^{-1}mod_1 + x_1 \pmod {mod_2*mod_1} \]
发现这样第二次合并的时候可以直接在模需要的模数 \(p\) 下进行计算。
注意,务必考虑好取模的顺序,计算第二个时不能先对 p 取模,因为可能结果还需要先模上 \(mod_1*mod_2*mod_3\),
怎样避免 __int128 ?
把 \((x_2-x_1)mod_1^{-1}\) 对 \(mod_2\) 取模即可,注意处理正负号,保证任意时刻结果在 [0,对应模数) 内。
拆系数 FFT
FFT 精度不高,一般来说 double 跑个 \(10^{14}\) 问题不大(最后除掉 \(n\) 以后的结果,也就是乘出来的多项式的系数,举个例子就是如果跑 \(n=10^4\),那需要保证 \(a_i,b_i\leq 10^5\)),但是跑 \(10^{24}\) 次方, FFT 表示我做不到,用 long double 也不行。
所以考虑拆系数,把每个数写成 \(a_1\times2^{15} + a_2\) 的形式。
然后乘法就变成了 \(a_1b_1*2^{30}+(a_1b_2+a_2b_1)*2^{15}+a_2b_2\)。
设新的四个多项式为 \(A_1,A_2,B_1,B_2\)
一共 8 次 FFT,但可以优化,考虑复多项式 \[ P=B_1+B_2i\\ T_1=P\times A_1 \\ T_2=P\times A_2 \]
发现需要的系数在 \(T_1,T_2\) 的实部和虚部,对这俩做 IDFT,收工。
一共 5 次 DFT。
也可以这样:
先考虑 \[ P=A_1+A_2i\\ P'=A_1-A_2i\\ Q=B_1+B_2i \]
再考虑: \[ T_1=PQ=(A_1B_1-A_2B_2)+(A_1B_2+A_2B_1)i\\ T_2=P'Q=(A_1B_1+A_2B_2)+(A_1B_2-A_2B_1)i \] 最后: \[ T_1+T_2=2(A_1B_1+A_1B_2i)\\ T_1-T_2=-2(A_2B_2-A_2B_1i) \]
弄出 \(T_1,T_2\) 的系数表示法就行。其实复系数多项式乘法和普通系数多项式乘法没有什么区别。
这种 5 次的思路还有其它方式,不一一介绍了。
听说有种方法可以把两次实数域多项式的 DFT 和 IDFT 整成一次复数域的,但我觉得没啥必要会。
DFT 思路是构造一个多项式 \(P=A+Bi\),对其做 DFT,然后线性求 \(Q=A-Bi\) 的 DFT 结果。
IDFT 思路不会。
更推荐第一种写法,不会出现精度问题,如果效率不够再考虑改成第二种