FFT,NTT与FWT

FFT

  • 用处:

快速计算多项式的卷积。

  • 做法:

我们知道多项式有两种表示:系数表示与点值表示。系数表示就是最常见的表示法,就是拿一个向量来表示多项式的各项系数。而点值表示则是拿几个点 ( x i , y i ) (x_i,y_i) xiyi来表示。把系数表示的多项式转成点值的过程叫做求值,反之叫插值。下面比较一下这两种方法。
对于多项式加法,点值与系数表示都是 O ( n + m ) O(n+m) On+m n n n m m m分别是原多项式的次数),但是点值的话必须选的 x x x都一样。
对多项式乘法,系数是 O ( n m ) O(nm) Onm的,而点值还是 O ( n + m ) O(n+m) On+m的(只需要把对应点乘上去)。由此可见,如果在算乘法时,表示法是点值的,那么就可以很快算出。而如果用最朴素的一个点一个点算,还是 O ( n m ) O(nm) Onm的,而 F F T FFT FFT则是快速的将多项式转成点值,然后再快速将点值转成系数的方法,其时间复杂度为 O ( n l o g n ) O(nlogn) Onlogn的。其中,将系数换成点值的过程叫做 D F T DFT DFT,反之叫做 I D F T IDFT IDFT

  • DFT的实现:

首先,设 w n k = e k 2 π i n w_n^k=e^{k \frac{2 \pi i }{n}} wnk=ekn2πi,即 n n n次方程的 k k k次单位根。我们有如下几个极其重要的性质:
(1)相消引理
对于任何整数 n ≥ 0 , k ≥ 0 , d > 0 n\geq 0,k\geq 0,d>0 n0,k0,d>0,有 w d n d k = w n k w_{dn}^{dk}=w_n^k wdndk=wnk
(2)折半引理
如果 n > 0 n>0 n>0且为偶数,则 ( w n k + n 2 ) 2 = ( w n k ) 2 (w_n{k+\frac{n}{2}})^2 = (w_n^k)^2 (wnk+2n)2=(wnk)2
(2)求和引理
任意整数 n ≥ 1 n\geq 1 n1和不能被 n n n整除的非零整数 k k k,有 ∑ i = 0 n − 1 ( w n k ) i = 0 \sum _{i=0}^{n-1}(w_n^k)^i=0 i=0n1(wnk)i=0
那么有了这三条引理,就可以进行 F F T FFT FFT了。 F F T FFT FFT的方法是:将原来的数列奇偶分类得到两个新的 A 0 A0 A0 A 1 A1 A1。其中 A 0 A0 A0放偶数位置,A1放奇数位置的,这样子,我们可以得到一个式子: A ( w n k ) = A 0 ( ( w n k ) 2 ) + ( w n k ) A 1 ( ( w n k ) 2 ) A(w_n^k)=A_0((w_n^k)^2)+(w_n^k)A_1((w_n^k)^2) A(wnk)=A0((wnk)2)+(wnk)A1((wnk)2),这样子可以写出最基本的 F F T FFT FFT
就是我们对于一个 n = 2 q n=2^q n=2q,对于一个 i < n 2 i<\frac{n}{2} i<2n,有
A ( w n i ) = A 0 ( ( w n i ) 2 ) + w n i A 1 ( ( w n i ) 2 ) A(w_n^i)=A_0((w_n^i)^2)+w_n^iA_1((w_n^i)^2) A(wni)=A0((wni)2)+wniA1((wni)2)
A ( w n i + n 2 ) = A 0 ( ( w n i + n 2 ) 2 ) − w n i A 1 ( ( w n i + n 2 ) 2 ) A(w_n^{i+\frac{n}{2}})=A_0((w_n^{i+\frac{n}{2}})^2)-w_n^iA_1((w_n^{i+\frac{n}{2}})^2) A(wni+2n)=A0((wni+2n)2)wniA1((wni+2n)2)
所以只用算 n 2 \frac{n}{2} 2n次就可以了。
我们还有一种叫做蝴蝶操作的方法降低复杂度。这个是迭代版FFT用到的,具体就是只用一个数组计算。具体的,我也不是很明白还是看代码。
代码
(递归版,常数大,空间大,好记忆)


void FFT(com *A,int len,int fl)
{
	if (len==1) return;
	com A0[len],A1[len];
	for (int i=0;i<(len/2);i++)
	{
		A0[i] = A[2*i];
		A1[i] = A[2*i+1]; 
	}
	FFT(A0,len/2,fl);FFT(A1,len/2,fl);
	com wn(cos(2*pi/len*fl),sin(2*pi/len*fl)) , w(1,0);
	for (int i=0;i<(len/2);i++)
	{
		A[i] = A0[i] + w * A1[i];
		A[i+(len/2)] = A0[i] - w * A1[i];
		w = w * wn; 
	}
}

(非递归版,常数,空间都小,难记忆)

void FFT2(com c[],int len,int fl)
{
    int i,j,k,clen;
    for (i=(len>>1),j=1;j<len;j++)
    {
        if (i<j) swap(c[i],c[j]);
        for (k=(len>>1);i&k;k>>=1) i^=k;
        i^=k;
    }
    for (clen=2;clen<=len;clen<<=1)
    {
        com wn=com(cos(2.0*pi/clen*fl),sin(2.0*pi/clen*fl));
        for (j=0;j<len;j+=clen)
        {
            com w=com(1,0);
            for (k=j;k<j+(clen>>1);k++)
            {
                com tmp1=c[k],tmp2=c[k+(clen>>1)]*w;
                c[k]=tmp1+tmp2;c[k+(clen>>1)]=tmp1-tmp2;
                w=w*wn;
            }
        }
    }
    if (fl==-1)
    {
        for (i=0;i<len;i++) c[i]=c[i]/len;
    }
}
  • IDFT

之前不是传进了一个 f l fl fl吗?当 f l fl fl 1 1 1的时候,就是 F F T FFT FFT,是 − 1 -1 1的时候,就是 I D F T IDFT IDFT。至于怎么证明就是这样,你相信就是了,可以参见其他dalao的博客。最后算出来的结果还要除以一个 l e n len len。(这个在非递归版中已经除了,但在递归版中写不了,所以要在外面那个地方再除一次)。
NTT

N T T NTT NTT F F T FFT FFT的方法类似,但是我们这次要选择原根。那么原根满足之前的那三条引理吗?可以证明是满足的,所以可以直接搞。
这里补一下代码:

const ll p = 998244353;
const ll g = 3;
const ll invg = 332748118;
void NTT(ll *a,int len,int fl)
{
	int i,j,k;
	for (i=(len>>1),j=1 ; j<len ; j++)
	{
		if (i < j) swap(a[i],a[j]);
		for (k = (len>>1) ;i & k; k>>=1) i ^= k;
		i ^= k;
	}
	for (int clen=2;clen<=len;clen*=2)
	{
		ll wn = qpow(( fl == 1 ) ?  g : invg , (mod-1) / clen );
		for (int j=0;j<len;j+=clen)
		{
			ll w = 1;
			for (int k=j;k<j+(clen>>1);k++)
			{
				ll x = a[k] , y = a[k+(clen>>1)] * w %mod;
				a[k] = (x + y)%mod; a[k+(clen>>1)] = (x - y+mod)%mod;
				w = (w * wn)%mod;		
			}
		}
	}
	if (fl == -1)
	{
		ll inv = qpow(len,mod-2);
		for (int i=0;i<len;i++) a[i] = a[i]*inv%mod;
	}
}

上面的膜数以及其原根有许多取法,这里给的是最常见的 998244353 998244353 998244353 3 3 3
2018.3.10补:
有时候我们会看见这种题目:
C = A × B C = A \times B C=A×B
C ( 1 ) C(1) C1,那么就是求 C C C的系数之和。这种问题,我们当然可以用 F F T 和 N T T FFT和NTT FFTNTT来做,但是我们有复杂度更低的做法,就是考虑 A i A_i Ai会与 B B B的那些相乘,那么明显是 B B B的全部项,所以答案就是 A A A的系数和乘上 B B B的系数和。

FWT

详细的在这里:FWT。
我们之前的 F F T FFT FFT解决的多项式卷积可以写成这样的形式 C [ k ] = ∑ i = 0 k A [ i ] B [ k − i ] C[k] = \sum _{i=0} ^{k}A[i] B[k-i] C[k]=i=0kA[i]B[ki]
从下标的角度来看,就是我们要卷积的下标之和为新的下标。那么,这种下标就是加法意义的下标。那么 F W T FWT FWT解决便是集合意义下的下标,就是
C [ k ] = ∑ i ⊕ j = k k A [ i ] B [ j ] C[k] = \sum _{i⊕j=k} ^{k}A[i] B[j] C[k]=ij=kkA[i]B[j]
这里 ⊕ ⊕ 符号不是指异或,而是一种二进制运算。
那么怎么做呢?我先说一个简单的,与运算 a n d and and吧。
首先我们要明白,这个东西 F W T FWT FWT其实和 F F T FFT FFT差不多,都是把原式变成点值之类的东西,再把点值乘起来,然后再还原的大体思路。那么显而易见的,这种变化要满足 A ′ × B ′ = C ′ A'\times B'=C' A×B=C,其中 A ’ , B ’ , C ’ A’,B’,C’ A,B,C 是变化后的式子。显而易见要满足这一点,不然你把变化后的两个式子乘起来都不等于原式了。
那么我们就设这种变化为 F W T FWT FWT, 其逆变换为 U F W T UFWT UFWT。那么我就说与运算怎么搞吧。
也不知道是哪个神人想出的变换,使得 A ′ [ i ] = ∑ i ∧ j = i A [ j ] ( ∧ 这 个 符 号 是 与 , 因 为 打 不 出 a n d ) A'[i] = \sum _{i ∧ j=i}A[j] (∧这个符号是与,因为打不出and) A[i]=ij=iA[j](and),莫名其妙吧,但是仔细看一下,好像是满足 A ′ × B ′ = C ′ A'\times B'=C' A×B=C这个东西的。那么我们考虑怎么求这个 A ′ A' A
类似于 F F T FFT FFT,我们考虑分治做,同样我们设一个 A 0 ′ , A 1 ′ A'_0,A'_1 A0,A1,但是与 F F T FFT FFT中不同,我们设 A 0 ′ A'_0 A0 A ′ A' A的前一半。设 A ′ A' A的长度为 2 k 2^k 2k,那么这一半的下标二进制下的最前面一位第 k k k位是为 0 0 0的,然后 A 1 ′ A'_1 A1就是后面一半,那么最前面一位第 k k k位是为 1 1 1的。这样有啥好处?就是我们就可以知道,如果算出了 A 0 ′ , A 1 ′ A'_0,A'_1 A0,A1,那么我们可以知道 A ′ A' A的后一部分必须还得是后一部分,还是 A 1 ′ A'_1 A1,而前一部分因为是与运算,所以 ( 0 ∧ 1 ) ( 0 ∧ 0 ) (0∧1)(0∧0) (01)(00)的结果都是 0 0 0,所以前一部分是 A 0 ′ + A 1 ′ A'_0+A'_1 A0+A1
那么这样子就构造完毕了,至于逆运算,也很简单。你都知道了 A ′ 0 , A ′ 1 A'0,A'1 A0,A1 A 0 , A 1 A0,A1 A0,A1的关系了,那么之前是 A 0 = A 0 ′ + A 1 ′ , A 1 = A 1 ′ A_0 = A'_0+A'_1,A_1 = A'_1 A0=A0+A1,A1=A1,那么我们可以把式子反过来,变成 A 0 ′ = A 0 − A 1 , A 1 ′ = A 1 A'_0=A_0-A_1,A'_1=A_1 A0=A0A1,A1=A1,就是简单的反过来。
好这样子的话,就把与运算解决了,那么其他的常见运算或运算与异或运算读者可以自行去做,下面贴出结论:

或运算的

A 0 = A 0 ′ , A 1 = A 0 ′ + A 1 ′ A_0 = A'_0 , A_1 = A'_0 + A'_1 A0=A0,A1=A0+A1
A 0 ′ = A 0 , A 1 ′ = A 0 + A 1 A'_0 = A_0 , A'_1 = A_0 + A_1 A0=A0,A1=A0+A1
##鬼畜的异或运算的
A 0 = A 0 ′ + A 1 ′ , A 1 = A 0 ′ − A 1 ′ A_0 = A'_0 + A'_1, A_1 = A'_0 - A'_1 A0=A0+A1,A1=A0A1
A 0 ′ = A 0 + A 1 2 , A 1 ′ = A 0 − A 1 2 A'_0 = \frac{A_0 + A_1 }{ 2}, A'_1 = \frac{A_0 - A_1}{2} A0=2A0+A1,A1=2A0A1

下面是代码:

void FWT(ll *c,ll len)
{
    ll i,j,k,clen; 
    for (i=(len>>1),j=1 ; j<len ; j++)
    {
        if (i < j) swap(c[i],c[j]);
        for (k = (len>>1) ;i & k; k>>=1) i ^= k;
        i ^= k;
    }
    for (clen = 2;clen<= len;clen<<=1)
    {
        for (j = 0;j < len;j += clen)
        {
            for ( k = j;k < j + (clen>>1) ;k++)
            {
                ll x = c[k] , y = c[k + (clen>>1) ] ;
                // xor c[k] = x + y , c[k + (clen>>1)] = x-y;
                // and c[k] = x + y , c[k + (clen>>1)] = c[k + (clen>>1)] 
				// or  c[k] = c[k]    c[k + (clen>>1)] = x+y  
                  c[k] = x + y  ; c[k + (clen>>1) ] = x - y ; 
            }
        }
    }
    
}
void UFWT(ll *c,ll len)
{
    ll i,j,k,clen;
    for (i=(len>>1),j=1 ; j<len ; j++)
    {
        if (i < j) swap(c[i],c[j]);
        for (k = (len>>1) ;i & k; k>>=1) i ^= k;
        i ^= k;
    }
    for (clen = 2;clen<= len;clen<<=1)
    {
        for (j = 0;j < len;j += clen)
        {
            for ( k = j;k < j + (clen>>1) ;k++)
            {
                ll x = c[k] , y = c[k + (clen>>1) ] ;
                // xor c[k] = x + y / 2, c[k + (clen>>1)] = x-y / 2;
                // and c[k] = x - y , c[k + (clen>>1)] = c[k + (clen>>1)] 
				// or  c[k] = c[k]    c[k + (clen>>1)] = y-x  
                 c[k] = (x + y)*rev  ; c[k + (clen>>1) ] = (x - y)*rev ;//rev 是2的逆元 
            }
        }
    }
}

细心的读者肯定发现了,这不就是刚才的 F F T FFT FFT的版改了一下运算部分吗?没错,就是这样,所以对于 F W T FWT FWT,只用记住那几个运算的变换,然后在套上 F F T FFT FFT的模板就可以了。
这里提几个细节,就是传 l e n len len的时候,一定要传 2 k 2^k 2k这种 l e n len len,而不是 2 k − 1 2^k-1 2k1这种看似正确的 l e n len len,因为你的 l e n len len的意义是有多长,所以下标为 0 0 0的那个也要算。
然后,前面那个二进制分组可以去掉,因为 F W T FWT FWT相当于是每一位独立算的,可以不用二进制分组。
还有代码中那个 r e v rev rev,它的意思是 2 2 2在你要模的数的意义下的逆元。

K进制下的FWT

void FWT(ll *c,ll len) {
    ll i , j , clen; 
    for (clen = 1;clen < len;clen *= k) {
        for (i = 0;i < len;i += clen * k) {
            for  (j = i;j < i + clen;i++) {
            	ll a[j] , a[j + len] , a[j + len * 2] , a[j + len * 3]...
            	a[j] = w(0 * 0) a[j] + w(0 * 1) a[j + len] + w(0 * 2) a[j + len * 2]...;
            	a[j + len] = w(1 * 0) a[j] + w(1 * 1) a[j + len] + w(1 * 2) a[j + len * 2]...;
            	
            }
        }
    }
}

你可能感兴趣的:(矩阵与行列式,多项式)