離散フーリエ変換 (DFT) および数論変換 (NTT) の原理、そしてそれらのプログラミングにおける実装方法について記述します。
各用語の定義の違いを明確にするため、それぞれについての回を分割します。
前回は離散フーリエ変換 (DFT) を剰余環
今回はこれに高速フーリエ変換 (FFT) と同様の手法を適用して、
まず、NTT についての資料などでたびたび言及される高速剰余変換 (fast modulo transform, FMT) という用語の出典について調べてみたところ、初期の信頼できる文献として次のものが見つかりました。
どうやら本稿の「NTT に FFT の手法を適用して高速化したもの」という文脈とは異なるようです。
また、英語で調べてもこの用語は見当たりませんでした。
次の論文では、「NTT に FFT の手法を適用して高速化したもの」が高速数論変換 (fast number-theoretic transform, FNTT) という用語で定義されています。一般的に使われる用語になっているかどうかはわかりません。
今回の議論に必要な定義および定理を準備します。有名な定理の証明は省略します。
原始根または生成元のうちの一つを
この定義は、
すなわち、
を満たす
なお、原始根はただ一つに定まるとは限りません。
このとき、
ここで、
したがって、
なお、この定理から
等差数列
しかし、
前回に考察した NTT を、FFT と同様の手法により高速化するための十分条件について考察します。
(3) 高速フーリエ変換 (FFT)
における考察から、FFT に関する定理が成り立つには、
(i)
(ii)
(iii)
が満たされるとよいことがわかります。
なお、(i) が成り立つときは、(ii) も成り立っています。
いったん、ここまでの考察を次の定理にまとめます。
次の式が成り立つとする。
また、
このとき、FFT に関する定理は、値の集合と標本点の組
適用 | 備考 | ||||
---|---|---|---|---|---|
成立 | 可 | ||||
不可 | |||||
成立 | 可 |
適用 | 備考 | ||||
---|---|---|---|---|---|
成立 | 可 | ||||
不可 | |||||
成立 | 可 |
しかし、このような
特定の位数
そこで、実用における利便性のために強めの制約として、
これにより、次のように定理の十分条件を整理できます。
原始根の存在定理により、
この
このとき、定理 4 より、
さらに、
が成り立ちます。
以上の考察から、次のようにまとめられます。
ある
このとき、
前の定理における
また、
また、ある
したがって、実用においては
とくに、
例として、
は
通常は、
原始根
特徴的な
備考 | |||
---|---|---|---|
一部の分野でよく利用される | |||
(4) ビット反転置換
と
(5) 数論変換 (NTT)
のコードを組み合わせます。
パラメーター
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 を
使用例は次のようになります。
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 }
}
}
次回は、求めたい値が
前回:
(5) 数論変換 (NTT)
次回:
(7) 中国剰余定理による値の復元