多项式算法2:NTT(快速数论变换)

多项式算法2:NTT(快速数论变换)

前言

算法简介
上次的FFT大量使用浮点数,有精度不好控制的问题,使用可以保证精度的方法又容易超时。这里NTT是在取模的一个前提下找到一个新的且为整数的单位根(在取模意义下),我们记其为 G G G,那么用这个根去替代原来的 ω n \omega_n ωn来进行运算。因为所有运算就会变成整数运算,所以精度误差大大降低。使用NTT也方便按照题目要求进行取模运算,缺点就是只能进行以整数为系数的多项式运算,同时数组长度和每一项的值(如果题目原本不要求取模的话)都有限制。

前置知识


若正整数 a a a p p p互质且满足 p > 1 p>1 p>1,则:
对于 a n ≡ 1 ( m o d    p ) a^n \equiv 1(\mod p ) an1(modp)最小的 n n n,我们称之为 a a a p p p的阶,记作 δ p ( a ) \delta_p(a) δp(a)
对于 i ∈ [ 0 , δ p ( a ) ) i \in [0,\delta_p(a)) i[0,δp(a)),所有的 a i m o d    p a^i \mod p aimodp结果互不相同。
我们假设存在两个整数 j j j k k k满足 a j ≡ a k ( m o d    p ) , 0 ≤ j < k < δ p ( a ) a^j \equiv a^k (\mod p),0 \le j \lt k \lt \delta_p(a) ajak(modp),0j<k<δp(a)则有 a k − j ≡ 1 ( m o d    p ) a^{k-j}\equiv 1 (\mod p) akj1(modp),与阶的定义矛盾。
原根
n n n是正整数, g g g是整数,若 g g g n n n的阶 δ n ( g ) = φ ( n ) \delta_n(g)= \varphi(n) δn(g)=φ(n) gcd ⁡ ( g , n ) = 1 \gcd(g,n) =1 gcd(g,n)=1,则称 g g g n n n的一个原根。
gcd ⁡ ( g , n ) = 1 \gcd(g,n) =1 gcd(g,n)=1 n > 0 n>0 n>0,则 g g g n n n的一个原根的充要条件为: S = { g 1 , g 2 , g 3 , ⋯   , g φ ( n ) } S=\{g^1,g^2,g^3,\cdots,g^{\varphi(n)} \} S={g1,g2,g3,,gφ(n)} n n n的一组简化剩余系(即一组包含所有模 n n n后与 n n n互质且取模后的数两两不相同的数)。
由上文可知若 g g g为原根,则 S S S中任意两个元素在模 n n n的意义下不同余。
又因为 gcd ⁡ ( g , n ) = 1 \gcd(g,n)=1 gcd(g,n)=1,故而 S S S n n n的一组简化剩余系。
FFT
FFT详解

正文

关于原根和模数的选择
由于NTT大量二分数组,我们希望选一些形如 p = q × 2 k + 1 p=q \times 2^k + 1 p=q×2k+1的质数 p p p,其中 q q q为奇自然数, k k k为整数。
我们有以下的原根可以选。

模数原根原根的逆元最大长度
469762049 = 7 × 2 26 + 1 469762049=7 \times 2^{26} + 1 469762049=7×226+13156587350 2 26 2^{26} 226
998244353 = 119 × 2 23 + 1 998244353=119 \times 2^{23} + 1 998244353=119×223+13332748118 2 23 2^{23} 223
2281701377 = 17 × 2 27 + 1 2281701377=17 \times 2^{27} + 1 2281701377=17×227+13760567126 2 27 2^{27} 227
1004535809 = 479 × 2 21 + 1 1004535809=479 \times 2^{21} + 1 1004535809=479×221+13334845270 2 21 2^{21} 221

DFT
通过观察,我们可以发现原根有一些和复数单位根很像的性质:
我们令 n n n为大于 1 1 1 2 2 2的幂, p p p为素数且 n ∣ ( p − 1 ) n |(p-1) n(p1) g g g p p p的一个原根。
我们设 g n = g p − 1 n g_n=g^{\frac{p-1}{n}} gn=gnp1所以 g n n = g n × p − 1 n = g p − 1 g_n^n=g^{n \times \frac{p-1}{n}}=g^{p-1} gnn=gn×np1=gp1 g n n 2 = g p − 1 2 g_n^{\frac{n}{2}}=g^{\frac{p-1}{2}} gn2n=g2p1 g a n a k = g a k ( p − 1 ) a n = g k ( p − 1 ) n = g n k g_{an}^{ak}=g^{\frac{ak(p-1)}{an}}=g^{\frac{k(p-1)}{n}}=g_n^k ganak=ganak(p1)=gnk(p1)=gnk通过观察不难得出: g n n ≡ g p − 1 ≡ 1 ( m o d    p ) g_n^n \equiv g^{p-1} \equiv 1 (\mod p) gnngp11(modp)又因为 ( g n n 2 ) 2 ≡ 1 ( m o d    p ) (g_n^{\frac{n}{2}})^2\equiv 1 (\mod p) (gn2n)21(modp),且同属于一个简化剩余系,不能相等,
g n n 2 ≡ − 1 ( m o d    p ) g_n^{\frac{n}{2}}\equiv -1 (\mod p) gn2n1(modp)同时我们有 ( g n k + n 2 ) 2 ≡ ( g n k ) 2 × g n n ≡ ( g n k ) 2 ( m o d    p ) (g_n^{k + \frac{n}{2}})^2\equiv (g_n^k)^2 \times g_n^n \equiv (g_n^{k})^2 (\mod p) (gnk+2n)2(gnk)2×gnn(gnk)2(modp)又因为同属于一个简化剩余系,不能相等,可得 g n k + n 2 ≡ − g n k ( m o d    p ) g_n^{k + \frac{n}{2}} \equiv -g_n^{k} (\mod p) gnk+2ngnk(modp)
总而言之,单位根在FFT中用到的性质原根都有。
g n n ≡ 1 ( m o d    p ) g_n^n \equiv 1 (\mod p) gnn1(modp)
g a n a k ≡ g n k ( m o d    p ) g_{an}^{ak} \equiv g_n^k(\mod p) ganakgnk(modp)
g n n 2 ≡ − 1 ( m o d    p ) g_n^{\frac{n}{2}}\equiv -1 (\mod p) gn2n1(modp)
g n k + n 2 ≡ − g n k ( m o d    p ) g_n^{k + \frac{n}{2}} \equiv -g_n^{k} (\mod p) gnk+2ngnk(modp)
所以在DFT的过程中带入 g n k g_n^k gnk g n k + n 2 g_n^{k+\frac{n}{2}} gnk+2n实质上和带入 ω n k \omega_n^k ωnk ω n k + n 2 \omega_n^{k+\frac{n}{2}} ωnk+2n一样。
IDFT
和FFT一样,用矩阵来辅助思考:
[ g n 0 g n 0 g n 0 ⋯ g n 0 g n 0 g n 1 g n 2 ⋯ g n n − 1 g n 0 g n 2 g n 4 ⋯ g n 2 n − 2 g n 0 g n 3 g n 6 ⋯ g n 3 n − 3 ⋮ ⋮ ⋮ ⋱ ⋮ g n 0 g n n − 1 g n 2 n − 2 ⋯ g n ( n − 1 ) 2 ] ∗ [ a 0 a 1 a 2 a 3 ⋮ a n − 1 ] = [ F 0 F 1 F 2 F 3 ⋮ F n − 1 ] \left[ \begin{array}{ccccc} g_{n}^{0} &g_{n}^{0}&g_{n}^{0} & \cdots & g_{n}^{0} \\ g_{n}^{0}&g_{n}^{1}&g_{n}^{2} & \cdots & g_{n}^{n-1} \\ g_{n}^{0} & g_{n}^{2} & g_{n}^{4} & \cdots &g_{n}^{2n-2} \\ g_{n}^{0} &g_{n}^{3} & g_{n}^{6} &\cdots & g_{n}^{3n-3} \\ \vdots & \vdots & \vdots &\ddots & \vdots \\ g_{n}^{0} &g_{n}^{n-1} &g_{n}^{2n-2} &\cdots &g_{n}^{(n-1)^2}\\ \end{array} \right] * \left[ \begin{array}{ccccc} a_0\\ a_1\\ a_2\\ a_3\\ \vdots\\ a_{n-1}\\ \end{array} \right] = \left[ \begin{array}{ccccc} F_0\\ F_1\\ F_2\\ F_3\\ \vdots\\ F_{n-1}\\ \end{array} \right] gn0gn0gn0gn0gn0gn0gn1gn2gn3gnn1gn0gn2gn4gn6gn2n2gn0gnn1gn2n2gn3n3gn(n1)2a0a1a2a3an1=F0F1F2F3Fn1
和FFT不太一样,乘以单位根的共轭复数的过程将变成乘以原根在模 p p p意义下的逆元,最后再乘以长度 n n n p p p下的逆元即可。
证明如下(以下运算均在模 p p p的条件下进行):
g n k = g ( p − 1 n ) k g_n^k=g^{(\frac{p-1}{n})k} gnk=g(np1)k
如上矩阵, F ( k ) = ∑ j = 0 n − 1 a ( j ) ( g n i ) j k F(k)=\sum_{j=0}^{n-1}a(j) (g_n^{ i })^{jk} F(k)=j=0n1a(j)(gni)jk
逆变换为:
1 n ∑ k = 0 n − 1 F ( k ) ( g n i ) − j k = 1 n ∑ k = 0 n − 1 ( ∑ l = 0 n − 1 a ( l ) ( g n i ) l k ) ( g n i ) − j k = 1 n ∑ k = 0 n − 1 ( ∑ l = 0 n − 1 a ( l ) ( g n i ) ( l − j ) k ) \frac{1}{n} \sum_{k=0}^{n-1}F(k) (g_n^{ i })^{ -jk }=\frac{1}{n} \sum_{k=0}^{n-1}( \sum_{l=0}^{n-1}a(l)(g_n^{ i })^{ lk } )(g_n^{ i })^{- jk }=\frac{1}{n} \sum_{k=0}^{n-1}( \sum_{l=0}^{n-1}a(l)(g_n^{ i })^{ (l-j)k } ) n1k=0n1F(k)(gni)jk=n1k=0n1(l=0n1a(l)(gni)lk)(gni)jk=n1k=0n1(l=0n1a(l)(gni)(lj)k)
式子 = 1 n ∑ k = 0 n − 1 ( a ( 0 ) ( g n i ) ( 0 − j ) k + a ( 1 ) ( g n i ) ( 1 − j ) k + ⋯ + a ( n − 1 ) ( g n i ) ( ( n − 1 ) − j ) k ) =\frac{1}{n} \sum_{k=0}^{n-1}( a(0)(g_n^{ i })^{ (0-j)k} + a(1)(g_n^{ i })^{ (1-j)k} + \cdots + a(n-1)(g_n^{ i })^{ ((n - 1)-j)k} ) =n1k=0n1(a(0)(gni)(0j)k+a(1)(gni)(1j)k++a(n1)(gni)((n1)j)k)
展开 k k k的求和式得
= 1 n [ a ( 0 ) ( ( g n i ) ( 0 − j ) 0 + ( g n i ) ( 0 − j ) 1 + ⋯ + ( g n i ) ( 0 − j ) ( n − 1 ) ) + a ( 1 ) ( ( g n i ) ( 1 − j ) 0 + ( g n i ) ( 1 − j ) 1 + ⋯ + ( g n i ) ( 1 − j ) ( n − 1 ) ) + ⋯ + a ( n − 1 ) ( ( g n i ) ( ( n − 1 ) − j ) 0 + ( g n i ) ( ( n − 1 ) − j ) 1 + ⋯ + ( g n i ) ( ( n − 1 ) − j ) ( n − 1 ) ) ] =\frac{1}{n}[ a(0)((g_n^{ i })^{ (0-j)0 }+(g_n^{ i })^{ (0-j)1 } + \cdots + (g_n^{ i })^{ (0-j)(n-1) }) + \\ a(1)((g_n^{ i })^{ (1-j)0 }+(g_n^{ i })^{ (1-j)1 } + \cdots + (g_n^{ i })^{ (1-j)(n-1) }) + \\ \cdots + \\ a(n-1)((g_n^{ i })^{ ((n-1)-j)0 }+(g_n^{ i })^{ ((n-1)-j)1 } + \cdots + (g_n^{ i })^{ ((n-1)-j)(n-1) }) ] =n1[a(0)((gni)(0j)0+(gni)(0j)1++(gni)(0j)(n1))+a(1)((gni)(1j)0+(gni)(1j)1++(gni)(1j)(n1))++a(n1)((gni)((n1)j)0+(gni)((n1)j)1++(gni)((n1)j)(n1))]
由等比数列的求和公式得:
a ( x ) ( ( g n i ) ( x − j ) 0 + ( g n i ) ( x − j ) 1 + ⋯ + ( g n i ) ( x − j ) ( n − 1 ) ) = a ( x ) 1 − ( ( g n i ) n ( x − j ) ) n 1 − ( g n i ) ( x − j ) a(x)((g_n^{ i })^{ (x-j)0 }+(g_n^{ i })^{ (x-j)1 } + \cdots + (g_n^{ i })^{ (x-j)(n-1) })=a(x) \frac{1- ((g_n^{ i })^{{n}(x-j)})^n}{1- (g_n^{ i })^{(x-j)} } a(x)((gni)(xj)0+(gni)(xj)1++(gni)(xj)(n1))=a(x)1(gni)(xj)1((gni)n(xj))n j ≠ x j \neq x j=x,可得: a ( x ) 1 − ( ( g n i ) ( x − j ) ) n 1 − ( g n i ) ( x − j ) = a ( x ) 1 − ( g i ) ( x − j ) 1 − ( g n i ) ( x − j ) = a ( x ) 1 − 1 ( x − j ) 1 − ( g n i ) ( x − j ) = 0 a(x) \frac{1- ((g_n^{ i })^{(x-j)})^n}{1- (g_n^{ i })^{(x-j)} }=a(x) \frac{1- (g^{ i })^{(x-j)} }{1- (g_n^{ i })^{(x-j)} }=a(x) \frac{1- 1^{ (x-j)}}{1- (g_n^{ i })^{(x-j)} }=0 a(x)1(gni)(xj)1((gni)(xj))n=a(x)1(gni)(xj)1(gi)(xj)=a(x)1(gni)(xj)11(xj)=0
j = x j = x j=x,可得: a ( x ) ( ( g n i ) ( x − j ) 0 + ( g n i ) ( x − j ) 1 + ⋯ + ( g n i ) ( x − j ) ( n − 1 ) ) = n a ( x ) a(x)((g_n^{ i })^{ (x-j)0 }+(g_n^{ i })^{ (x-j)1 } + \cdots + (g_n^{ i })^{ (x-j)(n-1) })=na(x) a(x)((gni)(xj)0+(gni)(xj)1++(gni)(xj)(n1))=na(x)
因而: 1 n ∑ k = 0 n − 1 F ( k ) ( g n i ) − j k = 1 n ∑ k = 0 n − 1 ( ∑ l = 0 n − 1 a ( l ) ( g n i ) l k ) ( g n i ) − j k = n a ( x ) \frac{1}{n} \sum_{k=0}^{n-1}F(k) (g_n^{ i })^{ -jk }=\frac{1}{n} \sum_{k=0}^{n-1}( \sum_{l=0}^{n-1}a(l)(g_n^{ i })^{ lk } )(g_n^{ i })^{- jk }=na(x) n1k=0n1F(k)(gni)jk=n1k=0n1(l=0n1a(l)(gni)lk)(gni)jk=na(x)
该矩阵的逆矩阵得证。
模板题
贴上DFT与IDFT结合起来的代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#define ll long long
using namespace std;
const int N = 1 << 22;
const int g = 3 , gi = 332748118 , mod = 998244353;//不加const时间翻四倍 
ll qw( ll a , ll b ) {
	ll ans = 1;
	while ( b ) {
		if( b & 1 ) {
			ans = ans * a % mod;
		}
		a = a * a % mod;
		b >>= 1;
	}
	return ans;
}
int rev[N];
int n , m , l;
ll a[N] , b[N];
void pre( int bit ) {
	for ( int i = 0 ; i < ( 1 << bit ) ; ++i ) {
		rev[i] = (rev[i>>1]>>1)|((i&1)<<(bit - 1));
	}
}
void NTT( ll *F , int len , int on ) {
	for ( int i = 0 ; i < len ; ++i ) {//把递归的底层交换好
		if ( i < rev[i] ) {
			swap( F[i] , F[rev[i]] );
		}
	}
	for ( int i = 2 ; i <= len ; i <<= 1 ) {//枚举步长,从递归的下面往上走 
		ll gn = qw( on ? g : gi , ( mod - 1 ) / ( i ) );
		for ( int j = 0 ; j <= len - 1 ; j += i ) {//走一遍步长 
			ll gg = 1;
			for ( int k = j ; k < j + i / 2 ; ++k ) {//枚举每块区间内的每一个元素
				ll u = F[k];
				ll t = gg * F[k + i / 2] % mod;
				F[k] = (u + t) % mod;
				F[k + i / 2] = ( u - t  + mod ) % mod;
				gg = gg * gn % mod;
			}
		}
	}
	return;
}

int main () {
	int len = 0;
	scanf("%d%d",&n,&m);
	for ( int i = 0 ; i <= n ; ++i ) {
		scanf("%lld",a + i);
	}
	for ( int i = 0 ; i <= m ; ++i ) {
		scanf("%lld",b + i);
	}
	len = n + m;
	int tim = 1;
	l = 0;
	while( tim <= len ) {
		tim <<= 1;
		l++;
	}
	len = tim;
	pre(l);
	NTT( a , len , 1 );
	NTT( b , len , 1 );
	for ( int i = 0 ; i <= len - 1 ; ++i ) {
		a[i] = a[i] * b[i] % mod;
	}
	NTT( a , len , 0 );
	ll inv = qw( len , mod - 2 );
	for ( int i = 0 ; i <= n + m ; ++i ) {
		printf("%lld ",a[i] * inv % mod);
	}
	return 0;
}

任意模数的坑以后再填吧QWQ。

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值