定理:
Z q / ( a ( x ) ⋅ b ( x ) ) ≅ Z q / ( a ( x ) ) × Z q / ( b ( x ) ) \mathbb{Z}_{q}/(a(x) \cdot b(x)) \cong \mathbb{Z}_{q}/(a(x))× \mathbb{Z}_{q}/(b(x)) Zq/(a(x)⋅b(x))≅Zq/(a(x))×Zq/(b(x)),其中 a ( x ) , b ( x ) a(x),b(x) a(x),b(x)互素
ϕ : f ( x ) → ( f ( x ) m o d a ( x ) , f ( x ) m o d b ( x ) ) \phi: f(x) \to (f(x) \ mod \ a(x), f(x) \ mod \ b(x)) ϕ:f(x)→(f(x) mod a(x),f(x) mod b(x))
当 n = 4 n=4 n=4的时候则可以得到以下分解
快速数论变换NTT的结果都可以以这种方式计算出做完NTT变换后的每项系数,也就是对应这个二叉树叶节点的值(都为常数)。其实求每个分支的值的方法也非常简单。例如我们要求第二层也就是根节点的左右儿子的值,左边:只需要将 x 2 = ω 2 x^{2}=\omega^{2} x2=ω2带入根节点的式子中即可得左儿子的值。
这里解释一下为什么根节点的右儿子可以分解成 Z q [ x ] / ( x − ω 3 ) \mathbf{Z}_{q}[x] /\left(x-\omega^{3}\right) Zq[x]/(x−ω3)和 Z q [ x ] / ( x + ω 3 ) \mathbf{Z}_{q}[x] /\left(x+\omega^{3}\right) Zq[x]/(x+ω3)的形式,是因为这里 ω \omega ω是模 q q q的本原单位根,满足 ω n = − 1 \omega^{n}=-1 ωn=−1的性质,因此 x 2 + ω 2 x^{2}+\omega^{2} x2+ω2又可以表示为 x 2 − ω 4 × ω 2 x^{2}-\omega^{4}×\omega^{2} x2−ω4×ω2也就是 x 2 − ω 6 x^{2}-\omega^{6} x2−ω6
刚刚分解已经讲过怎么去求分支的值,我们可以写一下公式分别看看根的左结点 f l f_{l} fl和根的右结点 f r f_{r} fr分别是什么
有没有发现规律,这两个结点其实每项系数都是对称的 f l 0 f_{l_{0}} fl0对应 ( f 0 + f 2 ω 2 ) (f_{0}+f_{2}\omega^{2}) (f0+f2ω2), f r 0 f_{r_{0}} fr0对应 ( f 0 − f 2 ω 2 ) (f_{0}-f_{2}\omega^{2}) (f0−f2ω2)。
那么我们就可以画出这样的图
[ f l 0 f r 0 ] ← [ f 0 + t f 0 − t ] \left[\begin{array}{l}f_{l_{0}} \\ f_{r_{0}}\end{array}\right] \leftarrow\left[\begin{array}{l}f_{0}+t \\ f_{0}-t\end{array}\right] [fl0fr0]←[f0+tf0−t]
形如蝴蝶,因此称作蝴蝶变换。其实对于上图树中的每一个节点的多项式环分解成左右两棵子树的过程都是该节点多项式环中的间隔 l 2 \frac{l}{2} 2l(这里 l l l为当前待分解多项式环的度数)距离的两组系数做一个Cooley-Tukey蝴蝶变换。
完整来看,这样一颗二叉树(下图1)就对应这样的蝴蝶变换(下图2)
从上图的蝴蝶变换我们可以看出,往往下层的某项系数是由上层的某两项系数决定的,而这两个系数是对称的,只要确定其中一个的位置也就能确定另外一个的位置。例如第一层的第0项系数 f 0 f_{0} f0和 f 2 f_{2} f2决定了下一层的零项系数(左右都是,不过是对称的)。那么我们就可以固定其中一个即可。
如果我们已知分解后的左右节点系数值,如何求得根节点的值?这里就引入Gentleman-Sande蝴蝶变换。我们先看下面一个例子(对应树中根节点 Z q / ( x 4 − w 4 ) \mathbb{Z_q}/(x^4-w^4) Zq/(x4−w4)分解到左子树 Z q / ( x 2 − w 2 ) \mathbb{Z_q}/(x^2-w^2) Zq/(x2−w2)和右子树 Z q / ( x 2 + w 2 ) \mathbb{Z_q}/(x^2+w^2) Zq/(x2+w2)过程的逆过程):
在这个例子中我们已知 f l f_l fl和 f r f_r fr的系数 f l 0 , f l 1 , f r 0 , f r 1 f_{l0},f_{l1},f_{r0},f_{r1} fl0,fl1,fr0,fr1,如何求得 f ( x ) f(x) f(x)的系数?
其实也很简单,因为我们求 f l f_l fl和 f r f_r fr的时候,是将 x 2 = w 2 x^2=w^2 x2=w2和 x 2 = − w 2 x^2=-w^2 x2=−w2代入到 f ( x ) f(x) f(x)中,因此我们可以得到 f 0 + f 1 x + f 2 ⋅ w 2 + ( f 3 ⋅ w 2 ) x = f l 0 + f l 1 x f_0+f_1x+f_2\cdot w^2+(f_3\cdot w^2)x=f_{l0}+f_{l1}x f0+f1x+f2⋅w2+(f3⋅w2)x=fl0+fl1x和, f 0 + f 1 x − f 2 ⋅ w 2 − ( f 3 ⋅ w 2 ) x = f l 0 + f l 1 x f_0+f_1x-f_2\cdot w^2-(f_3\cdot w^2)x=f_{l0}+f_{l1}x f0+f1x−f2⋅w2−(f3⋅w2)x=fl0+fl1x那么可得下列式子:
f 0 + f 2 ⋅ w 2 = f l 0 ( 1 ) f 1 x + f 3 ⋅ w 2 = f l 1 ( 2 ) f 0 − f 2 ⋅ w 2 = f r 0 ( 3 ) f 1 x − f 3 ⋅ w 2 = f r 1 ( 4 ) f_0+f_2\cdot w^2=f_{l0}(1) \\ f_1x+f_3\cdot w^2=f_{l1}(2) \\ f_0-f_2\cdot w^2=f_{r0}(3)\\ f_1x-f_3\cdot w^2=f_{r1}(4) f0+f2⋅w2=fl0(1)f1x+f3⋅w2=fl1(2)f0−f2⋅w2=fr0(3)f1x−f3⋅w2=fr1(4)
四个式子四个未知数可以求得每个系数的值为
f 0 = 1 2 ( f l 0 + f r 0 ) f 2 = 1 2 w − 2 ( f l 0 − f r 0 ) f 1 = 1 2 ( f l 1 + f r 1 ) f 3 = 1 2 w − 2 ( f l 1 − f r 1 ) f_0=\frac{1}{2}(f_{l0}+f_{r_0}) \\ f_2=\frac{1}{2}w^{-2}(f_{l_0}-f_{r_0}) \\ f_1=\frac{1}{2}(f_{l1}+f_{r_1}) \\ f_3=\frac{1}{2}w^{-2}(f_{l_1}-f_{r_1}) f0=21(fl0+fr0)f2=21w−2(fl0−fr0)f1=21(fl1+fr1)f3=21w−2(fl1−fr1)
因此GS变换如下图
如何减少乘法?
可以从一开始就不乘 1 2 \frac{1}{2} 21,则每层都是原来真实系数的两倍,最终得到系数应该是真实系数的 2 l o g n = n 2^{logn}=n 2logn=n倍。因此在逆NTT的时候需要乘 1 n \frac{1}{n} n1,但这样减少了一半的乘法。
由上面分析可以知道,我们只需要遍历每一层的一半系数就能确定下一层的系数。因此我们可以设置变量 j j j用来遍历该层一半的系数,变量 s t a r t start start确定 j j j的开始位置,以及变量 l e n len len确定 j j j的一个遍历范围。这里要注意,每层结点都是2的该层的幂次方-1,每个结点也就相当于一个多项式环也是要进行相应分解的,因此这里 s t a r t start start就对应每个结点多项式环的第一个系数。对于第一层 l e n = n 2 len=\frac{n}{2} len=2n代表的就是树中根节点的分解过程,对于第二层两个start,第一个start对应的就是树中第二层左儿子的分解,第二个start对应的树中第二层右儿子的分解,依次类推。
那么我们就可以得到如下伪代码:
Forward NTT Alogrithm:
k = 1
for len = n/2, len > 0, len = len/2 do
for start = 0 ; start < n; start = j+len do
for j = start; j < start + len; j++ do
t = w[k] * f[j+len]
f[j+len] = f[j] - t
f[j] = f[j] + t
end for
k++
end for
end for
对应C代码就是
void simple_ntt(int16_t r[N])
{
int len, start, j, k;
int16_t t, omega;
k = 1;
for (len = N / 2; len >= 1; len >>= 1)
{
for (start = 0; start < N; start = j + len)
{
omega = omegas[k++];
for (j = start; j < start + len; ++j)
{
t = (omega * r[j + len]) % Q;
r[j + len] = (r[j] - t) % Q;
r[j] = (r[j] + t) % Q;
}
}
}
}
这里的omega是该层对应的本原单位根
因为前面的计算结果都是要回到 Z q \mathbb{Z}_{q} Zq域上的,因此要不断取模,C语言中取模运算(%)用到了除法,是比较耗时且效率低的。有两种约减算法可以改善这种模运算的耗时性。分别是
计算 r = a m o d q r=a \ mod \ q r=a mod q,其中 a a a为16位有符号整数, − 2 15 ≤ r < q -2^{15} \leq r < q −215≤r<q。标准的约减需要一次整数除法,但是可以通过倒数形式表示为乘法。
r = a m o d q = a − q ⋅ [ a q ] = a − q ⋅ ⌊ a ⋅ q − 1 ⌋ r=a \bmod q=a-q \cdot\left[\frac{a}{q}\right]=a-q \cdot\left\lfloor a \cdot q^{-1}\right\rfloor r=amodq=a−q⋅[qa]=a−q⋅⌊a⋅q−1⌋
C语言实现
int16_t barrett_reduce(int16_t a) {
int16_t t;
const int16_t v = ((1U << 26) + KYBER_Q/2)/KYBER_Q;
t = (int32_t)v*a >> 26;
t *= KYBER_Q;
return a - t;
}
首先会选择一个基数 β \beta β,使得 β \beta β与 q q q互素,且 β > q \beta> q β>q,一般会取2的整数倍,当满足 0 ≤ a < β a 0 \leq a < \beta a 0≤a<βa时,将计算 r = a m o d q r = a mod q r=amodq转换为先计算 r ′ = a β − 1 m o d q r^{'} = a \beta ^{-1} \ mod \ q r′=aβ−1 mod q再计算 r = r ′ β m o d q r=r^{'} \beta \ mod \ q r=r′β mod q.计算 r ′ r^{'} r′用到的算法就是蒙哥马利约减算法。
C语言代码如下
int16_t montgomery_reduce(int32_t a)
{
int32_t t;
int16_t u;
u = a*QINV;
t = (int32_t)u*KYBER_Q;
t = a - t;
t >>= 16;
return t;
}