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

離散フーリエ変換と数論変換 (5) 数論変換 (NTT)

1558
0

この連載について

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

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

本文

前回までに考察した複素数体C上の離散フーリエ変換 (DFT) では、入力が整数であっても小数の演算が必要でした。
そこで今回は、入力が整数の場合には整数の演算のみで済むよう、何らかの整数の集合上でも前回までの理論が成り立たないかを検討します。
この結果として、数論変換 (number-theoretic transform, NTT) が得られます。

実用性を考慮すると、結論としては素数pに対して剰余環Z/pZ上で考えるのが簡単であり、それを前提とした文献も多いですが、本稿では十分条件をなるべく広く取って考察します。

準備

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

位数・1の原始k乗根

anが互いに素であるとき、
ak1(modn)を満たす最小のk>0を、amod nにおける位数 (order) と呼ぶ。

このときのa、つまり位数kの元を、1の原始k乗根 (primitive k-th root of unity) と呼ぶ。

逆元の存在

anが互いに素
 amod nにおける逆元、すなわちax1(modn)を満たすxが存在する

数論変換

加法および乗法が定義されている何らかの整数の集合上で、これまでの DFT の議論で現れた2つの定理 (IDFT および畳み込み) が成り立つための十分条件を考察します。

まず、ω1の原始n乗根であるとし、 (1) 離散フーリエ変換 (DFT) と同様に DFT を定義します。
すなわち、ωにより生成される{ω k | 0k<n}を標本点の集合とします。
このとき、ω k1 (0<k<n),  ω n=1が成り立っています。

逆離散フーリエ変換 (IDFT) に関する定理が成り立つには、以前の証明の内容から、

(i) 0k<nω jk={n  (j=0)0  (0<j<n)
(ii) 逆元ω1が存在する
(iii) 逆元n1が存在する

が満たされていれば十分であることがわかります。

(i) については、j=0のときはつねに成り立つため、0<j<nの場合のみ考えればよいです。
(ii) については、ωωn1=1より、ω1=ωn1とすればよいため満たされます。
(iii) については定理 1 により、剰余環Z/mZで考えた場合に、nmが互いに素であることと同値です。

また、畳み込み定理については、加法および乗法が定義されていれば成り立ちます。
以上の考察から、次のようにまとめられます。

数論変換

nmは互いに素であるとする。
ωを剰余環Z/mZにおける1の原始n乗根の一つとする。
さらに、次が成り立つとする。
  0k<nω jk0(modm)   (0<j<n)

このとき、IDFT に関する定理および畳み込み定理は、値の集合と標本点の組C,ωnZ/mZ,ωに置き換えても成り立つ。
このような剰余環Z/mZ上の DFT を、数論変換 (number-theoretic transform, NTT) と呼ぶ。

ここまでの議論においては、n2の冪である必要も、mが素数である必要もありません。
これらの強い制限は、次回の高速化で活用されることになります。

n=5,6の例を考えると、次のようにいくつかのパターンがあることがわかります。
1の原始n乗根ωが存在しても NTT が適用不可であることがあります。

n=5:

Z/mZω{ωk}ω jk0n1適用備考
Z/31Z2{1,2,4,8,16}成立25mは素数
Z/25Z6{1,6,11,16,21}ω k0なし不可
Z/55Z16{1,16,36,26,31}成立なし不可
Z/22Z3{1,3,9,5,15}ω k09不可
Z/121Z3{1,3,9,27,81}成立97mは合成数
Z/341Z4{1,4,16,64,256}成立273mは合成数

n=6:

Z/mZω{ωk}ω jk0n1適用備考
Z/31Z6{1,6,5,30,25,26}成立26mは素数
Z/21Z2{1,2,4,8,16,11}ω 2k0なし不可
Z/21Z5{1,5,4,20,16,17}成立なし不可
Z/35Z4{1,4,16,29,11,9}ω 2k06不可
Z/49Z19{1,19,18,48,30,31}成立41mは合成数
Z/91Z10{1,10,9,90,81,82}成立76mは合成数

mが素数の場合は扱いやすいです。
例えばm=31のとき、位数30の元 (原始根) が存在します。したがって、30の任意の約数を位数とする元が存在します。

mが合成数の場合、ω jkについての十分条件を満たすωを見つけることが難しいです。
例えば(n,m,ω)=(5,22,3),(6,35,4)は NTT を適用できそうに見えて、実はこの条件を満たしていません。

もしω j1mod mで逆元を持てば、
  0k<nω jk0  (0<j<n)
であることは示せます。
そこで、ω jkについての十分条件を「0<j<nに対して、ω j1mは互いに素」に変更してもよいかもしれません。

次回は、mを素数に限定することで、高速化と実用性を備えた構築方法を得ることになります。

プログラミング

DFT のコードを基に NTT を実装していきましょう。
今回はパラメーターn,m,ω,n1をコンストラクターで指定できるようにしておきます。
また、1n乗根の集合{ωk}をあらかじめ計算してキャッシュします。

      using System;

public class NTT
{
    // k 番目の 1 の n 乗根 (ω^k)
    static long[] NthRoots(int n, long m, long w)
    {
        var r = new long[n];
        r[0] = 1;
        for (int k = 1; k < n; ++k)
            r[k] = r[k - 1] * w % m;
        return r;
    }

    int n;
    long m, nInv;
    long[] roots;

    public NTT(int n, long m, long w, long nInv)
    {
        this.n = n;
        this.m = m;
        this.nInv = nInv;
        roots = NthRoots(n, m, w);
    }

    // f(ω^k) の値
    long f(long[] c, int k)
    {
        var r = 0L;
        for (int j = 0; j < c.Length; ++j)
            r = (r + c[j] * roots[k * j % n]) % m;
        return r;
    }

    public long[] Transform(long[] c, bool inverse)
    {
        if (c == null) throw new ArgumentNullException(nameof(c));

        var r = new long[n];
        for (int k = 0; k < n; ++k)
            r[k] = inverse ? f(c, n - k) * nInv % m : f(c, k);
        return r;
    }

    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] % m;
        }
        return Transform(fa, true);
    }
}
    

Transform, Convolution 関数のいずれも、時間計算量はO(n2)です。次回にこれを高速化します。
では Convolution 関数をテストしてみましょう。

      using System;

static class Program
{
    static void Main()
    {
        Test(5, 31, 2, 25);
        Test(5, 121, 3, 97);
        Test(5, 341, 4, 273);

        Test(6, 31, 6, 26);
        Test(6, 49, 19, 41);
        Test(6, 91, 10, 76);
    }

    static void Test(int n, long m, long w, long nInv)
    {
        var a = new long[] { 2, 1, 1 };
        var b = new long[] { m - 1, m - 1, 1 };

        var ntt = new NTT(n, m, w, nInv);
        // { m - 2, m - 3, 0, 0, 1 }
        var c = ntt.Convolution(a, b);
    }
}
    

この例は(x2+x+2)(x2x1)=x43x2を表しています。
前節で調べた、NTT が適用可能なパラメーターに対して実行しています。

作成したサンプル コード

前回: (4) ビット反転置換
次回: (6) NTT の高速化

参考文献

投稿日:2021926
OptHub AI Competition

この記事を高評価した人

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

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

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

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

投稿者

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

コメント

他の人のコメント

コメントはありません。
読み込み中...
読み込み中
  1. この連載について
  2. 本文
  3. 準備
  4. 数論変換
  5. プログラミング
  6. 参考文献