3
大学数学基礎解説
文献あり

離散フーリエ変換と数論変換 (6) NTT の高速化

2073
0

この連載について

離散フーリエ変換 (DFT) および数論変換 (NTT) の原理、そしてそれらのプログラミングにおける実装方法について記述します。
各用語の定義の違いを明確にするため、それぞれについての回を分割します。

  1. 離散フーリエ変換 (DFT)
  2. 数列の畳み込み
  3. 高速フーリエ変換 (FFT)
  4. ビット反転置換
  5. 数論変換 (NTT)
  6. NTT の高速化 (本稿)
  7. 中国剰余定理による値の復元

本文

前回は離散フーリエ変換 (DFT) を剰余環Z/mZ上で考える数論変換 (NTT) を説明し、時間計算量O(n2)の実装を紹介しました。
今回はこれに高速フーリエ変換 (FFT) と同様の手法を適用して、O(nlogn)に高速化します。

用語

まず、NTT についての資料などでたびたび言及される高速剰余変換 (fast modulo transform, FMT) という用語の出典について調べてみたところ、初期の信頼できる文献として次のものが見つかりました。

どうやら本稿の「NTT に FFT の手法を適用して高速化したもの」という文脈とは異なるようです。
また、英語で調べてもこの用語は見当たりませんでした。

次の論文では、「NTT に FFT の手法を適用して高速化したもの」が高速数論変換 (fast number-theoretic transform, FNTT) という用語で定義されています。一般的に使われる用語になっているかどうかはわかりません。

準備

今回の議論に必要な定義および定理を準備します。有名な定理の証明は省略します。

原始根・生成元

pを素数とする。
mod pにおける位数がp1である元を、mod pにおける原始根 (primitive root) または生成元 (generator) と呼ぶ。

原始根または生成元のうちの一つをgで表すことが多いです。
この定義は、gkにより1からp1までの数を生成するようなgと言い換えられます。
すなわち、
  { gk(modp) | 0<k<p}={ x | 0<x<p}
を満たすgを指し、こちらを定義とすることもあります。

原始根の存在定理

pを素数とするとき、mod pにおける原始根は存在する。

なお、原始根はただ一つに定まるとは限りません。

位数dの元の個数

pを素数とし、dp1の約数とする。
このとき、mod pにおける位数がdの元の個数はφ(d)である。
ここで、φ Euler の totient 関数 である。

したがって、mod pにおける原始根の個数はφ(p1)です。
なお、この定理からφに関する性質dnφ(d)=nも得られます。

Dirichlet の算術級数定理

adは互いに素であるとする。
等差数列an:=a+dn  (n=0,1,2,)は、素数を無限に含む。

p3以上の素数とし、gmod pにおける原始根の一つとするとき、
  gp121(modp)

x:=gp12とし、x±1であると仮定すると、(x+1)(x1)0となるはずである。
しかし、(x+1)(x1)=x21gp110に矛盾するため、x±1のいずれかとなる。
gp12g p11は相異なる数だから、gp121となる。

NTT の高速化

前回に考察した NTT を、FFT と同様の手法により高速化するための十分条件について考察します。
(3) 高速フーリエ変換 (FFT) における考察から、FFT に関する定理が成り立つには、

(i) n2の冪
(ii) n>1のとき、ω21の原始n/2乗根
(iii) n>1のとき、ω n/2=1

が満たされるとよいことがわかります。
なお、(i) が成り立つときは、(ii) も成り立っています。

いったん、ここまでの考察を次の定理にまとめます。

n2の冪とし、nmは互いに素であるとする。
ωを剰余環Z/mZにおける1の原始n乗根の一つとする。
次の式が成り立つとする。
  0k<nω jk0(modm)   (0<j<n)
また、n>1のとき、ω n/21を満たすとする。

このとき、FFT に関する定理は、値の集合と標本点の組C,ωnZ/mZ,ωに置き換えても成り立つ。

n>1のとき、mは奇数であると言い換えられます。

n=4,8の例を以下に挙げます。
1の原始n乗根ωが存在しても NTT の高速化が適用不可であることがあります。

n=4:

Z/mZω{ωk}ω jk0適用備考
Z/13Z5{1,5,1,5}成立mは素数
Z/15Z2{1,2,4,8}ω 2k0不可
Z/25Z7{1,7,1,7}成立mは合成数

n=8:

Z/mZω{ωk}ω jk0適用備考
Z/17Z2{1,2,4,8,1,2,4,8}成立mは素数
Z/51Z2{1,2,4,8,16,32,13,26}ω 2k0不可
Z/289Z110{1,110,251,155,1,110,251,155}成立mは合成数

しかし、このような(n,m,ω)の組を見つける方法はアドホックになってしまいます。
特定の位数nを持つ元ωを見つけるには、原始根から求める方法が簡単です。
そこで、実用における利便性のために強めの制約として、mを素数pとすることを考えます。
これにより、次のように定理の十分条件を整理できます。

原始根の存在定理により、Z/pZにおける原始根が存在するため、その一つをgとします。
gの位数はp1であることから、1の原始n乗根が存在するにはnp1が成り立つようにpを選んでおく必要があります。
dn=p1 (d>0)とするとき、ω:=gdと決めることができます。
このp=dn+1 (d>0)と表せる素数pの存在は、Dirichlet の算術級数定理により保証されます。

このとき、定理 4 より、n>1に対してω n/21が成り立ちます。
さらに、0<j<nに対して、ω j1pは互いに素であることから、
  0k<nω jk0(modp)
が成り立ちます。

以上の考察から、次のようにまとめられます。

n2の冪、pを素数とする。
あるd>0が存在し、p=dn+1を満たすとする。

このとき、Z/pZにおける原始根の一つをgとし、ω:=gdとすると、FFT に関する定理は、値の集合と標本点の組C,ωnZ/pZ,ωに置き換えても成り立つ。

前の定理におけるmを素数と決めることで原始根gの存在を保証し、ωを副次的に決めることができるため実用しやすいものとなっています。

C,ωnに対しては、いくらでも大きいnを利用することができましたが、Z/pZ,ωpにも依存しています。したがって、多項式の次数が高くなるほど、大きいpを選ぶ必要があります。
また、C上で考える場合は値の制限を気にする必要がありませんでしたが、Z/pZにおいては剰余類の集合[0,p)に制限されるため、入出力となる値が大きいほど、大きいpを選ぶ必要があります。

また、あるnで定理が成り立つ場合、n/2としても成り立つため、任意のn1次以下の多項式に適用できます。
したがって、実用においてはn,pをあらかじめ大きな値で構成しておけば変更することなく再利用できます。
とくに、p1が素因数として2を多く含むとよいです。

例として、
  p=998244353=717223+1
n=218としてもn=223としても利用できますが、計算する値がp以上となる場合 (10億など) はmod pにおける値となります。

通常は、p,gを決めて NTT の高速化を利用することになるでしょう。
p=dn+1 (d>0)を満たすp,dの組、そして原始根gはプログラムを作るなどして求めることができます。
原始根gについては、Wolfram|Alpha で PrimitiveRoot[998244353] のように調べる方法もあります。
特徴的なp,d,gの組を求めた結果をいくつか挙げておきます。

pdn+1g備考
655371216+13
1677721615225+13
998244353119223+13一部の分野でよく利用される
110729625733225+110109<p<231
201326592115227+1312109<p<231

プログラミング

(4) ビット反転置換 (5) 数論変換 (NTT) のコードを組み合わせます。
パラメーターn,p,gをコンストラクターで指定できるようにしておきます。

      using System;

public class FNTT
{
    public static int ToPowerOf2(int n)
    {
        var p = 1;
        while (p < n) p <<= 1;
        return p;
    }

    static int[] BitReversal(int n)
    {
        var b = new int[n];
        for (int p = 1, d = n >> 1; p < n; p <<= 1, d >>= 1)
            for (int k = 0; k < p; ++k)
                b[k | p] = b[k] | d;
        return b;
    }

    long MPow(long b, long i)
    {
        long r = 1;
        for (; i != 0; b = b * b % p, i >>= 1) if ((i & 1) != 0) r = r * b % p;
        return r;
    }

    // k 番目の 1 の n 乗根 (0 <= k < n/2)
    long[] NthRoots(int n, long w)
    {
        var r = new long[n >> 1];
        r[0] = 1;
        for (int k = 1; k < r.Length; ++k)
            r[k] = r[k - 1] * w % p;
        return r;
    }

    int n;
    public int Length => n;
    long p, nInv;
    int[] br;
    long[] roots;

    // length は 2 の冪に変更されます。
    public FNTT(int length, long p = 998244353, long g = 3)
    {
        n = ToPowerOf2(length);
        this.p = p;
        nInv = MPow(n, p - 2);
        br = BitReversal(n);
        roots = NthRoots(n, MPow(g, (p - 1) / n));
    }

    // c の長さは 2 の冪とします。
    // h: 更新対象の長さの半分
    void TransformRecursive(long[] c, int l, int h)
    {
        if (h == 0) return;
        var d = (n >> 1) / h;

        TransformRecursive(c, l, h >> 1);
        TransformRecursive(c, l + h, h >> 1);

        for (int k = 0; k < h; ++k)
        {
            var v0 = c[l + k];
            var v1 = c[l + k + h] * roots[d * k] % p;
            c[l + k] = (v0 + v1) % p;
            c[l + k + h] = (v0 - v1 + p) % p;
        }
    }

    // 戻り値の長さは 2 の冪となります。
    public long[] Transform(long[] c, bool inverse)
    {
        if (c == null) throw new ArgumentNullException(nameof(c));

        var t = new long[n];
        for (int k = 0; k < c.Length; ++k)
            t[br[k]] = c[k];

        TransformRecursive(t, 0, n >> 1);

        if (inverse && n > 1)
        {
            Array.Reverse(t, 1, n - 1);
            for (int k = 0; k < n; ++k) t[k] = t[k] * nInv % p;
        }
        return t;
    }

    // 戻り値の長さは 2 の冪となります。
    public long[] Convolution(long[] a, long[] b)
    {
        if (a == null) throw new ArgumentNullException(nameof(a));
        if (b == null) throw new ArgumentNullException(nameof(b));

        var fa = Transform(a, false);
        var fb = Transform(b, false);

        for (int k = 0; k < n; ++k)
        {
            fa[k] = fa[k] * fb[k] % p;
        }
        return Transform(fa, true);
    }
}
    

これで NTT をO(nlogn)に高速化できました。

使用例は次のようになります。
p,gを変更して利用したい場合はコンストラクターで指定できます。

      using System;

static class Program
{
    static void Main()
    {
        var a = new long[] { 1, 2, 3, 4 };
        var b = new long[] { 5, 6, 7, 8, 9 };
        var n = a.Length + b.Length - 1;

        var fntt = new FNTT(n);
        var c = fntt.Convolution(a, b);
        // { 5, 16, 34, 60, 70, 70, 59, 36 }

        var fntt2 = new FNTT(n, 1107296257, 10);
        var c2 = fntt2.Convolution(a, b);
        // { 5, 16, 34, 60, 70, 70, 59, 36 }
    }
}
    

作成したサンプル コード

次回は、求めたい値がp以上になる場合の復元方法について説明します。

前回: (5) 数論変換 (NTT)
次回: (7) 中国剰余定理による値の復元

改訂履歴

  • 2021.10.01 Dirichlet の算術級数定理を追加。

参考文献

投稿日:2021929
OptHub AI Competition

この記事を高評価した人

高評価したユーザはいません

この記事に送られたバッジ

バッジはありません。
バッチを贈って投稿者を応援しよう

バッチを贈ると投稿者に現金やAmazonのギフトカードが還元されます。

投稿者

Researcher and Developer for Algorithms, using C# (.NET). 数検1級金賞 (2002).

コメント

他の人のコメント

コメントはありません。
読み込み中...
読み込み中
  1. この連載について
  2. 本文
  3. 用語
  4. 準備
  5. NTT の高速化
  6. プログラミング
  7. 改訂履歴
  8. 参考文献