0%

数学算法汇总

数学算法介绍

主要介绍一些数学相关的算法。

多项式

  • 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$ 形式。

无法想象发明这个东西的人是怎么想到的,可能这就是被记在历史书上的人的水平。

以下内容如果将坐标系视为极坐标系会更好理解

考虑这样一个问题,对于一个多项式 $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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FFT(int now,com *a,int op)
{
if(now==0)
return;
com a1[1<<now],a2[1<<now];
for(i=0;i<1<<now;i+=2)
{
a1[i/2]=a[i];
a2[i/2]=a[i+1];
}
FFT(now-1,a1,op);
FFT(now-1,a2,op);
com w0=(com{cos(2.0*Pi/(1<<now)),op*sin(2.0*Pi/(1<<now))}),w=(com){1,0};
for(i=0;i<1<<now-1;i++,w=w*w0)
{
a[i]=a1[i]+w*a2[i];
a[i+(1<<(now-1))]=a1[i]-w*a2[i];
}
}

但是如果递归的话,常数会比较拉跨。因为递归必然需要复制数组重新弄成一个下标 $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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FFT(com a[],com b[],int op)
{
for(i=0;i<1<<maxn;i++)
if(r[i]>i)swap(a[r[i]],a[i]),swap(b[r[i]],b[i]);
for(i=1;i<=maxn;i++){
for(j=0;j<1<<maxn;j+=1<<i){
com w0=(com){cos(2.0*Pi/(1<<i)),op*sin(2.0*Pi/(1<<i))},w=(com){1,0};
for(k=0;k<1<<i-1;k++,w=w*w0){
com x=a[j+k],y=a[j+k+(1<<i-1)]*w;
a[j+k]=x+y;
a[j+k+(1<<i-1)]=x-y;

x=b[j+k],y=b[j+k+(1<<i-1)]*w;
b[j+k]=x+y;
b[j+k+(1<<i-1)]=x-y;
}
}
}
}

搞完了系数转点值,接下来是点值转系数。

前人告诉我们只需要将单位根改为 $W_n^{0},W_n^{-1}\cdots W_n^{-n+1}$,再做一遍系数转点值的过程就可以得到系数,但是系数会变成原来的 $n$ 倍,除掉就行。

给出简要证明,$i$ 次项的系数为 $a_i$,转一次点值

之后变为 $b_i$,再做一次变成 $c_i$

对于 $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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void NTT(int *a,int type)
{
for(i=0;i<1<<s;i++)
if(rk[i]>i)swap(a[rk[i]],a[i]);
for(int len=1;len<=s;len++)
{
int w=1,wn=quick(g,mod-1>>len);
if(type==-1)wn=quick(wn,mod-2);
for(j=0;j+(1<<len)<=1<<s;j+=1<<len,w=1)
{
for(k=j;k<j+(1<<len-1);k++,w=1ll*w*wn%mod)
{
int x=a[k],y=a[k+(1<<len-1)];
a[k]=(x+1ll*w*y%mod)%mod,a[k+(1<<len-1)]=(x-1ll*w*y%mod)%mod;
}
}
}
}

其它多项式乘法和一些优化

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$ 意义下的逆元。

发现这样第二次合并的时候可以直接在模需要的模数 $p$ 下进行计算。

注意,务必考虑好取模的顺序,计算第二个时不能先对 p 取模,因为可能结果还需要先模上 $mod_1mod_2mod_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_12^{30}+(a_1b_2+a_2b_1)2^{15}+a_2b_2$。

设新的四个多项式为 $A_1,A_2,B_1,B_2$

一共 8 次 FFT,但可以优化,考虑复多项式

发现需要的系数在 $T_1,T_2$ 的实部和虚部,对这俩做 IDFT,收工。

一共 5 次 DFT。

也可以这样:

先考虑

再考虑:

最后:

弄出 $T_1,T_2$ 的系数表示法就行。其实复系数多项式乘法和普通系数多项式乘法没有什么区别。

这种 5 次的思路还有其它方式,不一一介绍了。

听说有种方法可以把两次实数域多项式的 DFT 和 IDFT 整成一次复数域的,但我觉得没啥必要会。

DFT 思路是构造一个多项式 $P=A+Bi$,对其做 DFT,然后线性求 $Q=A-Bi$ 的 DFT 结果。

IDFT 思路不会。

更推荐第一种写法,不会出现精度问题,如果效率不够再考虑改成第二种