C++で標数2の体

概要

 本記事では標数2の体をC++で定義する際の要点について概説する.異なる拡大による体を型で静的に区別できるような仕組みを作り,C++における自然で美しい体の実装を目指す.

動機

 夏休み中,学友がC++を学ぶために勉強会を開いた.勉強会は様々な数学的対象をC++で表現する内容だった.本記事は勉強会の一回で扱った内容の焼き直しである.

準備

 ある集合Gと演算\circ:G\times G\rightarrow Gが次の1~3の条件を満たすとき,集合Gと演算\circの組(G,\circ)を群という.また,1~4の条件を満たすとき,可換群またはアーベル群という.考えている演算が明らかなとき単にGを群と呼ぶ.

  1. 演算の結合性 : ^\forall a, b, c \in G, (a\circ b)\circ c = a\circ(b\circ c)
  2. 単位元の存在 : ^\forall a \in G, ^\exists e \in G\hspace{5pt} \text{ここで}\hspace{5pt} e\circ a = a \circ e = a
  3. 逆元の存在 : ^\forall a \in G, ^\exists x \in G \hspace{5pt} \text{ここで}\hspace{5pt} a\circ x = x\circ a = e
  4. 交換法則 : ^\forall a,b\in G, a\circ b= b\circ a

 演算\circが加法的である場合,aの逆元x-aと書き,演算\circが乗法的である場合,xa^{-1}と書く.

 ある集合Rと加法+:R\times R\rightarrow Rの組(R,+)が可換群であり,かつ以下の1~3の条件を満たすとき,R,+と乗法\ast:R\times R\rightarrow Rの組(R,+,\ast)を環という.また,1~4の条件を満たすとき,可換環という.

  1. 乗法の結合性 : ^\forall a, b, c\in R, (a\ast b)\ast c = a\ast (b\ast c)
  2. 乗法単位元の存在 : ^\forall a, \in R, ^\exists e _ \ast \in R \,\text{ここで}\, a \ast e _ \ast = e _ \ast \ast a = a
  3. 分配律 : ^\forall a, b, c \in R, (a+b)\ast c=(a\ast c)+(b\ast c)かつ a\ast(b+c)=(a\ast b)+(a\ast c)
  4. 交換法則 : ^\forall a,b\in R, a\ast b = b\ast a

 環Rの乗法単位元e _ \astについて,ne _ \ast=e _ \ast + e _ \ast + ... + e _ \ast = e _ \astとなるn>0が存在するとき,その最小の値をR標数とする.また,そのようなnが存在しないとき,R標数0とする.

イデアル

 環の部分集合Iが以下の1~3の条件を満たすときIイデアルという.1~4の条件を満たすときIを素イデアルという.

  1. 0\in I
  2. ^\forall a\in I, ^\forall r \in R, a\ast r \in I
  3. ^\forall a, b \in I, a + b\in I
  4. ^\forall r,s\in R, r\ast s \in I \text{.このとき} r\in I \text{または} s\in I

 例えば任意の整数の倍数全体の集合は,整数全体と通常の加法,乗法からなる環のイデアルとなっている.また,任意の素数の倍数全体の集合は素イデアルとなっている.可換環RイデアルIを求めることができれば,RIで割った余りからなる新たな環を定義できる.^\forall a,b\in Rについて,a-b\in Iならばa\sim bとする.この同値関係による同値類を\lbrack x\rbrack = x+Iと書くと加法 : \lbrack x \rbrack + \lbrack y \rbrack = \lbrack x + y \rbrackと乗法 : \lbrack x \rbrack \ast \lbrack y \rbrack = \lbrack x \ast y \rbrackによって剰余環R/Iが定義できる.

多項式環

 可換環や体を係数とする多項式環を定義する.Xを変数とし,可換環Rを係数とする一変数多項式環R\lbrack X \rbrackと書く.多項式 F=a_0 + a_1 X + a_2 X^{2} + ... + a_d X^{d} \in R\lbrack X \rbrackの各項のXの次数のうち最高の数を\deg F = dとし,Fの次数がdである,またはFd多項式であるという.F\deg F次の項の係数が1である多項式をモニック多項式であるという.定数でない多項式Fが,定数でない多項式の二つの積で表せないときF既約多項式という.

 ある集合\mathbb{F}可換環であり,かつ\mathbb{F}\backslash\lbrace 0 \rbraceが乗法に関して可換群をなすとき,\mathbb{F}を体という.つまり体は四則演算を自由に行える代数系である.環の素イデアルで割った剰余環は体となる.

体の代数拡大

 体の拡大とは,体\mathbb{F}\mathbb{K}\supset\mathbb{F}の組\mathbb{K}/\mathbb{F}のことをいう.\alpha \in \mathbb{K}F\in\mathbb{F}[X]の根であるとき,\alpha\mathbb{F}上代数的であるという.また,\mathbb{F}の拡大体\mathbb{K}の任意の元が代数的であるとき,\mathbb{K}/\mathbb{F}を代数拡大という.代数拡大の例に\mathbb{C}/\mathbb{R}がある.\mathbb{C}X ^ 2 + 1 \in \mathbb{R}[X]の根\sqrt{-1}\mathbb{R}に追加した体である.また,代数拡大でない拡大の例に\mathbb{R}/\mathbb{Q}がある.\pi,\mathrm{e} \in \mathbb{R}などは\mathbb{Q}上代数的でない.

 \mathbb{F}[X]の既約多項式の根\alpha\mathbb{F}に加えると\mathbb{F}を拡大できる.そのように拡大した体\mathbb{K}\supset\mathbb{F}の任意の元x\alphaa _ i \in \mathbb{F}を用いてx= a _ 0 + a _ 1\alpha + a _ 2 \alpha ^ 2 ... + a _ {\deg F - 1} \alpha ^ {\deg F - 1}と表され,\mathbb{K}\mathbb{F}のベクトル空間となる.\mathbb{F}のベクトル空間としての\mathbb{K}の次元数を拡大次数という.

標数2の体のビット表現

 整数環\mathbb{Z}の部分集合2\mathbb{Z}=\{...,-4,-2,0,2,4,...\}\mathbb{Z}の素イデアルである.剰余環\mathbb{Z}/2\mathbb{Z}標数2の体\mathbb{F} _ {2}になる.\mathbb{F} _ {2}の拡大体\mathbb{F} _ {2 ^ d}の元xは,\mathbb{F} _ {2} [X]上既約なd多項式の根\alphab _ i = 0,1 \in \mathbb{F} _ 2によって以下のように表される.

 \displaystyle
x=b _ 0 + b _ 1 \alpha + b _ 2 \alpha ^ 2 + ... + b _ {d-1} \alpha ^{d - 1}

 ここで上式のb _ idビットの整数の各ビットに対応させると\mathbb{F} _ {2 ^ d}の元を2進数として表現できる.また,\alphaを変数に置き換えれば\mathbb{F} _ 2 [X]多項式も2進数として表現できる.

実装

型の定義

 型は値に意味を与える.あるビット表現が単なる整数なのか,あるいは標数2の体の元を表現しているのか,あるいは標数2の体の元を表現しているとしてどの体の元なのかを適切に表現し計算するために,異なる体には異なる型を与える必要がある.C++ではテンプレート引数に整数引数を取れるため,体を拡大する原始多項式のビット表現をテンプレート引数で与えることができる.本記事では簡単のために拡大次数がd=8, 16, 32, 64\mathbb{F} _ {2 ^ d}のみ扱うこととするが,任意次数の拡大であっても理論は同じである.

 原始多項式のビット表現Polynomialをテンプレート引数で受け取れば,異なる原始多項式による異なる拡大体を異なる型で表現することができる.また,原始多項式のビット表現では最高次数の項がビット数によって常に一定なので省略されている.例えばT = unsigned charなら,Polynomialで直接表現されているのはX ^ 7以下の項のみで,X ^ 8の項は常に係数が1である.

template<class T, std::enable_if_t<std::is_unsigned_v<T>, T> Polynomial>
class gf {
    T value_;
};

 ここに元のビット表現を格納するメンバ変数を定義し,基本的な演算を行う演算子を追加していく.

加法

 \mathbb{F} _ {2 ^ d}の加法は\mathbb{F} _ 2と同様に排他的論理和演算で表すことができる.以下の表のように\mathbb{F} _ 2の加算をビット毎 (=元の式の項ごと) に行えばよい.加法の逆演算については-1=1\in\mathbb{F} _ 2, -0 = 0 \in\mathbb{F} _ 2であるため,加法と全く同じ演算で逆演算を行える.

+ 0 1
0 0 1
1 1 0

乗法

 \mathbb{F} _ {2 ^ d}の乗法は\mathbb{F} _ 2[X]Xを原始元に置き換えた乗法と変わらないが,元の多項式基底表現の次数がdを超える元は原始多項式で割って余りをとらなければコンピューターで表現できる桁数を超えてしまう.乗法の最も素朴な実装は,筆算をするように次数の低い項から掛けていき,ビット表現が型でサポートされる最大値を上回るとき,すなわち計算途中で\alpha ^ dを加算するとき,その代わりに,原始多項式F(\alpha) = 0の最高次数の項を移行した残りの辺のビット表現を加算するものである.例えばx=\alpha, y=\alpha + 1\in \mathbb{F} _ {2 ^ 2}の乗法x\ast yを考える.ただし,原始多項式F = X ^ 2 + X + 1とする.x\ast y = \alpha \ast (\alpha + 1) = \alpha ^ 2 + \alphaだが,このままビット表現にすると101になり,2ビットで表せる数を超える.このままでは決まったサイズで表すことができないので,\alpha ^ 2を等しい値で置き換える.F(\alpha) = \alpha ^ 2 + \alpha + 1 = 0なので,\alpha ^ 2 = \alpha + 1よりx \ast y = \alpha ^ 2 + \alpha = \alpha + 1 + \alpha = 1が求まる.

template<class T, std::enable_if_t<std::is_unsigned_v<T>, T> Polynomial>
class gf {
public:
    gf() = default;
    gf(const gf&) = default;
    explicit gf(T value)
        : value_{value}
    {}
    friend gf operator+(gf a, gf b) noexcept {
        return gf{a.value_ ^ b.value_};
    }
    friend gf operator-(gf a, gf b) noexcept { return a + b; }
    friend gf operator*(gf a, gf b) noexcept {
        gf res{};
        while(b.value_) {
            res.value_ ^= (b.value_ & 1 ? a.value_ : 0);
            a.value_ = !(a.value_ & (1 << (8*sizeof(T) - 1))) ? a.value_ << 1 : ((a.value_ << 1) ^ Polynomial);
            b.value_ >>= 1;
        }
        return res;
    }

private:
    T value_;
};

カラツバ法やSchönhage–Strassen法といった乗法の高速化手法もある.

除法

 除法は割る数の逆元を掛けることで実現する.有限群Gでは任意の元a\in Gについてa ^ {|G|-1}=a ^ {-1}となる.体は零元を除いて乗法に関して群をなすので,\mathbb{F} _ {2 ^ d}においても任意の元の2 ^ d - 2乗はその元の逆元となる.a ^ nを愚直に計算してもよいが,大きな体では逆元計算に時間がかかるようになる.べき乗は次数の2進展開をもちいて高速化できる.例えばa ^ {25}ならば,25を2進展開し1 1001とし, a ^ {25} = a ^ 1 \ast a ^ 8 \ast a ^ {16}となる.8乗はa ^ {2 ^ {2 ^ {2}}}と計算すれば掛け算は3回で済む.C++で書けば以下のようになる.

template<class T>
constexpr T pow(T a, unsigned int e) {
    T r{1};
    while (e) {
        if (e & 1) {
            r *= a;
            --e;
        }
        else {
            a *= a;
            e >>= 1;
        }
    }
    return r;
}

 最終的なソースコードは以下のようになる.

#include <type_traits>
#include <limits>

template<class T>
constexpr T pow(T a, unsigned int e) {
{
    T r{1};
    while (e) {
        if (e & 1) {
            r *= a;
            --e;
        }
        else {
            a *= a;
            e >>= 1;
        }
    }
    return r;
}

template<class T, std::enable_if_t<std::is_unsigned_v<T>, T> Polynomial>
class gf {
public:
    gf() = default;
    gf(const gf&) = default;
    explicit gf(T value)
        : value_{value}
    {}
    constexpr gf inv() const noexcept {
        return pow(*this, std::numeric_limits<T>::max() - 1);
    }
    constexpr explicit operator T() const noexcept { return value_; }
    friend gf operator+(gf a, gf b) noexcept {
        return gf{a.value_ ^ b.value_};
    }
    friend gf operator-(gf a, gf b) noexcept { return a + b; }
    friend gf operator*(gf a, gf b) noexcept {
        gf res{};
        while(b.value_) {
            res.value_ ^= (b.value_ & 1 ? a.value_ : 0);
            a.value_ = !(a.value_ & (1 << (8*sizeof(T) - 1))) ? a.value_ << 1 : ((a.value_ << 1) ^ Polynomial);
            b.value_ >>= 1;
        }
        return res;
    }
    friend gf operator/(gf a, gf b) noexcept {
        return a * b.inv();
    }
private:
    T value_;
};