从复FFT到有限域FFT的转换

时间:2012-04-21 16:36:59

标签: fft multiplication arbitrary-precision ntt

下午好!

我正在尝试基于我已经拥有的朴素递归FFT实现来开发NTT算法。

考虑以下代码(coefficients'长度,让它为m,精确为2的幂:

/// <summary>
/// Calculates the result of the recursive Number Theoretic Transform.
/// </summary>
/// <param name="coefficients"></param>
/// <returns></returns>
private static BigInteger[] Recursive_NTT_Skeleton(
    IList<BigInteger> coefficients, 
    IList<BigInteger> rootsOfUnity, 
    int step, 
    int offset)
{
    // Calculate the length of vectors at the current step of recursion.
    // -
    int n = coefficients.Count / step - offset / step;

    if (n == 1)
    {
        return new BigInteger[] { coefficients[offset] };
    }

    BigInteger[] results = new BigInteger[n];

    IList<BigInteger> resultEvens = 
        Recursive_NTT_Skeleton(coefficients, rootsOfUnity, step * 2, offset);

    IList<BigInteger> resultOdds = 
        Recursive_NTT_Skeleton(coefficients, rootsOfUnity, step * 2, offset + step);

    for (int k = 0; k < n / 2; k++)
    {
        BigInteger bfly = (rootsOfUnity[k * step] * resultOdds[k]) % NTT_MODULUS;

        results[k]          = (resultEvens[k] + bfly) % NTT_MODULUS;
        results[k + n / 2]  = (resultEvens[k] - bfly) % NTT_MODULUS;
    }

    return results;
}

它适用于复杂的FFT(用复杂的数字类型替换BigInteger(我有自己的))。即使我改变了适当地找到统一原始根的过程,它在这里也不起作用。

据说问题是这样的:rootsOfUnity参数最初只包含m的前半部分 - 这个顺序的复杂根源:

omega^0 = 1, omega^1, omega^2, ..., omega^(n/2)

这就足够了,因为在这三行代码中:

BigInteger bfly = (rootsOfUnity[k * step] * resultOdds[k]) % NTT_MODULUS;        

results[k]          = (resultEvens[k] + bfly) % NTT_MODULUS;
results[k + n / 2]  = (resultEvens[k] - bfly) % NTT_MODULUS;

我最初利用了这样一个事实:在任何递归级别(对于任何ni),统一-omega^(i) = omega^(i + n/2)的复杂根。

但是,该属性显然不适用于有限域。但有没有类似的东西可以让我仍然只计算根的前半部分?

或者我应该将周期从n/2扩展到n并预先计算所有m - 统一的根源吗?

这个代码可能还有其他问题吗?..

非常感谢你!

3 个答案:

答案 0 :(得分:1)

我最近想要实现 NTT 用于快速乘法,而不是 DFFT 。阅读了很多令人困惑的东西,到处都是不同的字母,没有简单的解决方案,而且我的有限域知识也生锈了,但今天我终于做对了(经过2天的尝试和模拟 DFT 系数)所以这是我对 NTT 的见解:

  1. <强>计算

    X(i) = sum(j=0..n-1) of ( Wn^(i*j)*x(i) );
    

    其中X[] NTT 已转换x[]大小为n,其中Wn NTT 。所有计算都在整数模数算术mod p上,任何地方都没有复数。

  2. 重要值


    Wn = r ^ L mod p NTT 的基础
    Wn = r ^ (p-1-L) mod p INTT 的基础
    Rn = n ^ (p-2) mod p正在缩放 INTT ~(1/n)的乘法常数 pp mod n == 1p>max'的首要问题
    max NTT x [i] 的最大值为 INTT的 X [i] 的最大值
    r = <1,p)
    L = <1,p)并将p-1分开 如果r,Lr^(L*i) mod p == 1必须合并i=0 i=n 如果r,L必须合并r^(L*i) mod p != 1 0 < i < n
    max'是子结果最大值,取决于n和计算类型。对于单个(I)NTT ,它是max' = n*max但是对于两个n大小的向量的卷积,它是max' = n*max*max等。有关详细信息,请参阅Implementing FFT over finite fields关于它。

  3. r,L,p的功能组合因不同的n

    而异

    这很重要,你必须在每个NTT层之前重新计算或选择表中的参数(n总是前一次递归的一半)。

  4. 这是我的 C ++ 代码,它找到了r,L,p个参数(需要模块化的算术,不包括在内,你可以用(a + b)%c替换它,(ab)% c,(a * b)%c,...但在这种情况下要注意特别是modpowmodmul的溢出。代码没有优化,但有很多方法可以大大加快速度。主表也相当有限,因此要么使用SoE or any other algo to obtain primes up to max'才能安全地工作。

    DWORD _arithmetics_primes[]=
        {
        2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,
        179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,
        419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,
        661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,
        947,953,967,971,977,983,991,997,1009,1013,1019,1021,1031,1033,1039,1049,1051,1061,1063,1069,1087,1091,1093,1097,1103,1109,1117,1123,1129,1151,
        0}; // end of table is 0, the more primes are there the bigger numbers and n can be used
    // compute NTT consts W=r^L%p for n
    int i,j,k,n=16;
    long w,W,iW,p,r,L,l,e;
    long max=81*n;  // edit1: max num for NTT for my multiplication purposses
    for (e=1,j=0;e;j++)             // find prime p that p%n=1 AND p>max ... 9*9=81
        {
        p=_arithmetics_primes[j];
        if (!p) break;
        if ((p>max)&&(p%n==1))
         for (r=2;r<p;r++)  // check all r
            {
            for (l=1;l<p;l++)// all l that divide p-1
                {
                L=(p-1);
                if (L%l!=0) continue;
                L/=l;
                W=modpow(r,L,p);
                e=0;
                for (w=1,i=0;i<=n;i++,w=modmul(w,W,p))
                    {
                    if ((i==0)      &&(w!=1)) { e=1; break; }
                    if ((i==n)      &&(w!=1)) { e=1; break; }
                    if ((i>0)&&(i<n)&&(w==1)) { e=1; break; }
                    }
                if (!e) break;
                }
            if (!e) break;
            }
        }
    if (e) { error; }           // error no combination r,l,p for n found
     W=modpow(r,    L,p);   // Wn for NTT
    iW=modpow(r,p-1-L,p);   // Wn for INTT
    

    这是我的慢速NTT和INTT实现(我还没有快速NTT,INTT)他们都成功地用Schönhage-Strassen乘法进行了测试。

    //---------------------------------------------------------------------------
    void NTT(long *dst,long *src,long n,long m,long w)
        {
        long i,j,wj,wi,a,n2=n>>1;
        for (wj=1,j=0;j<n;j++)
            {
            a=0;
            for (wi=1,i=0;i<n;i++)
                {
                a=modadd(a,modmul(wi,src[i],m),m);
                wi=modmul(wi,wj,m);
                }
            dst[j]=a;
            wj=modmul(wj,w,m);
            }
        }
    //---------------------------------------------------------------------------
    void INTT(long *dst,long *src,long n,long m,long w)
        {
        long i,j,wi=1,wj=1,rN,a,n2=n>>1;
        rN=modpow(n,m-2,m);
        for (wj=1,j=0;j<n;j++)
            {
            a=0;
            for (wi=1,i=0;i<n;i++)
                {
                a=modadd(a,modmul(wi,src[i],m),m);
                wi=modmul(wi,wj,m);
                }
            dst[j]=modmul(a,rN,m);
            wj=modmul(wj,w,m);
            }
        }
    //---------------------------------------------------------------------------
    


    dst是目标数组
    src是源数组
    n是数组大小
    m是模数(p
    w是基础(Wn

    希望这对某人有帮助。如果我忘记了什么,请写下......

    [edit1:快速NTT / INTT]

    最后,我设法快速 NTT / INTT 工作。比正常 FFT 更加棘手:

    //---------------------------------------------------------------------------
    void _NFTT(long *dst,long *src,long n,long m,long w)
        {
        if (n<=1) { if (n==1) dst[0]=src[0]; return; }
        long i,j,a0,a1,n2=n>>1,w2=modmul(w,w,m);
        // reorder even,odd
        for (i=0,j=0;i<n2;i++,j+=2) dst[i]=src[j];
        for (    j=1;i<n ;i++,j+=2) dst[i]=src[j];
        // recursion
        _NFTT(src   ,dst   ,n2,m,w2);   // even
        _NFTT(src+n2,dst+n2,n2,m,w2);   // odd
        // restore results
        for (w2=1,i=0,j=n2;i<n2;i++,j++,w2=modmul(w2,w,m))
            {
            a0=src[i];
            a1=modmul(src[j],w2,m);
            dst[i]=modadd(a0,a1,m);
            dst[j]=modsub(a0,a1,m);
            }
        }
    //---------------------------------------------------------------------------
    void _INFTT(long *dst,long *src,long n,long m,long w)
        {
        long i,rN;
        rN=modpow(n,m-2,m);
        _NFTT(dst,src,n,m,w);
        for (i=0;i<n;i++) dst[i]=modmul(dst[i],rN,m);
        }
    //---------------------------------------------------------------------------
    

    <强> [EDIT3]

    我优化了我的代码(比上面的代码快3倍),但我仍然不满意,所以我开始提出新问题。在那里,我进一步优化了我的代码(比上面的代码快大约40倍),因此它与相同位大小的浮点上的 FFT 几乎相同。链接到这里:

答案 1 :(得分:0)

你必须确保统一的根源确实存在。在 R 中只有2个统一的根:1和-1,因为只有它们x ^ n = 1才是真的。

C 中,你有无限多的统一根:w = exp(2 * pi * i / N)是一个原始的第N个单位的根,所有w ^ k为0 <= ķ

现在你的问题是:你必须确保你工作的戒指提供相同的属性:足够的统一根源。

Schönhage和Strassen(http://en.wikipedia.org/wiki/Sch%C3%B6nhage%E2%80%93Strassen_algorithm)使用模2 ^ N + 1的整数。这枚戒指有足够的统一根源。 2 ^ N == -1是统一的第2根,2 ^(N / 2)是统一的第4根,依此类推。此外,这些统一的根源的优势在于它们是2的幂,并且可以实现为二进制移位(之后使用模运算,可归结为加/减)。

我认为QuickMul(http://www.cs.nyu.edu/exact/doc/qmul.ps)模2 ^ N-1。

答案 2 :(得分:0)

要将Cooley-Tukey(复数)FFT转换为模块化算术方法,即NTT,必须替换omega的复杂定义。对于纯粹递归的方法,您还需要根据当前信号大小重新计算每个级别的omega 。这是可能的,因为分钟。当我们在调用树中向下移动时,合适的模量减小,因此用于根的模数适合于较低层。另外,由于我们使用相同的模数,因此当我们向下移动调用树时,可以使用相同的生成器。此外,对于逆变换,您应该采取额外的步骤来重新计算omega a,而是使用omega:b = a ^ -1(通过使用逆模运算)。具体地说,b = invMod(a,N)s.t. b * a == 1(mod N),其中N是所选的质数模数。

通过利用周期性来重写涉及omega的表达式仍然可以在模运算领域中使用。您还需要找到一种方法来确定问题的模数(素数)和有效的生成器。

我们注意到您的代码有效,但它不是MWE。我们使用常识扩展它,并获得多项式乘法应用的正确结果。你必须提供正确的欧米茄值,以提高某些能力。

虽然您的代码可以正常运行,但是与其他许多来源一样,您可以为每个级别添加两倍的间距。但这并不会导致干净的递归;这与根据当前信号大小重新计算欧米茄相同,因为欧米茄定义的功率与信号大小成反比。重申一下:将信号大小减半就像平方欧米茄一样,这就像为欧米茄提供双倍的力量(这是人们为间隔倍增而做的事情)。关于重新计算欧米茄的方法的好处在于,每个子问题本身都更加完整。

有一篇论文展示了模块化方法的一些数学方法;这是Baktir和Sunar从2006年开始发表的一篇论文。请参阅本文末尾的论文。

您无需将周期从n / 2扩展到n。

所以,是的,有些消息来源说明了对模块化算术方法的不同omega定义,正在大量细节之下。

另一个问题是,如果我们要执行卷积,如果我们要在结果时域信号中没有溢出,则必须确认信号大小必须足够大。此外,即使功率非常大,找到某些快速的模幂运算也可能很有用。

<强>参考

  • Baktir和Sunar - 使用快速傅立叶变换在Fermat场中实现有效的多项式乘法(2006)