ユークリッドのアルゴリズムと SL2(Z) の生成元   ― 塩田研一覚書帳 ―


最大公約数

 2つの整数 a, b の最大公約数を gcd(a, b) と表します。 gcd は Greatest Common Divisor の頭文字です。 普通は a も b も自然数で考えることが多いですが、 a, b の符号に関わらず gcd(a, b) は 0 または正に取ります。 片方が 0 なら
gcd(a, 0) = |a|
と約束します。例えば
gcd(-12, -30) = 6,   gcd(5, 0) = gcd(-5, 0) = 5
 0 は任意の整数の倍数 ( 0倍!) ですので、任意の整数は 0 の約数であり、a の約数で最大のもの |a| が gcd(a, 0) という理屈です。
 ただしそう考えると gcd(0, 0) は任意の整数ということになってしまいますので、これだけは
gcd(0, 0) = 0
と約束します。

a x + b y の形の数

 2つの整数 a, b を固定したとき、a x + b y ( x, y は整数 ) の形に書ける整数全体の集合 G を考えましょう:
G = { a x + b y | x, y は整数 }
G は、全ての整数の集合 Z の、加法群としての部分群になりますので、ある非負整数 d を用いて
G = { d z | z は整数 }
と書き直せることが分かります。d を G の生成元と呼びます。

 最大公約数や最小公倍数の性質を使うと、実はこの d は gcd(a, b) に他ならないことが示せます。 ( gcd(0, 0) = 0 という約束事も、丁度この事実に当てはまります。) ということは、任意の整数 a, b に対して
gcd(a, b) = a x + b y を満たす整数 x, y が存在する
ということが分かります。

てんびんクイズ

 充分大きな天秤と、a グラムと b グラムの分銅が充分たくさんあるとします。 これらを用いて測ることのできる最小の重さは何グラムでしょうか。

 答えは gcd(a, b) グラムです。

 gcd(a, b) = a x + b y を満たす整数 x, y が存在することを先程述べましたが、x, y の一方は正、他方は負であり、 a グラム |x| 個を左の皿に、b グラムの分銅 |y| 個を右の皿に乗せれば、その差として gcd(a, b) グラムを測ることができます。
  • a = 3, b = 5 のとき 3×2 + 5×(-1) = 1 ⇒ 3g×2個 = 5g×1個 + 1g
  • a = 6, b = 10 のとき 6×2 + 10×(-1) = 2 ⇒ 6g×2個 = 10g×1個 + 2g
  • a = 5, b = 7 のとき 5×3 + 7×(-2) = 1 ⇒ 5g×3個 = 7g×2個 + 1g
  • a = 7, b = 11 のとき 7×(-3) + 11×2 = 1 ⇒ 7g×3個 + 1g = 11g×2個

拡張ユークリッドアルゴリズム

 上述の x, y は、整数論では「Z 上での変換」に欠かせないものですし、 暗号理論においても、例えば RSA 暗号の復号化鍵を計算するときに必要になります。 単に存在が証明されているだけでは役に立ちません。 構成的証明(アルゴリズム)を与えましょう。Python で書けばこんな感じです:
# Extended Euclidean algorithm
# returned value = [d, x, y]
#   where  d = gcd(a, b) = a + x + b * y
def euclid(a, b):
    if b == 0:
        if a >= 0:
            return [a, 1, 0]
        else:
            return [-a, -1, 0]
    else:
        r0, x0, y0 = a, 1, 0
        r1, x1, y1 = b, 0, 1
        while r1 != 0:
            q = r0 / r1
            r2 = r0 - q * r1
            x2 = x0 - q * x1
            y2 = y0 - q * y1
            r0, x0, y0 = r1, x1, y1
            r1, x1, y1 = r2, x2, y2
        if r0 < 0:
            r0, x0, y0 = -r0, -x0, -y0
        return [r0, x0, y0]
 r0, r1, r2 は所謂「ユークリッドの互除法」で計算する剰余列です。 メモリ節約のため変数を使い回ししています。 x0, y0, x1, y1, x2, y2 との間には常に
r0 = a×x0 + b×y0,   r1 = a×x1 + b×y1,   r2 = a×x2 + b×y2
が成り立っており、アルゴリズム終了時には r0 = gcd(a, b) となるので gcd(a, b) = a×x0 + b×y0 となる、という仕組みです。

モジュラー群 SL2(Z)

 整数係数で行列式が 1 の2次行列たちが成す乗法群をモジュラー群と呼びます。 保型形式の整数論を勉強するときに最初に出て来る群です。
SL2(Z) を複素上半平面 H = { τ ∈ C | Im(τ) > 0 } に作用させるとき、 その商空間のコンパクト化 (Γ\H)* はリーマン球であり、 (Γ\H)* の関数体( = SL2(Z) に関する保型関数の成す体 ) は j-関数で生成され、 その j-関数は楕円曲線の j-不変量を与えるもので ... と話が広がってゆきます。

SL2(Z) の生成元

 SL2(Z) の2つの元
のような関係式を満たしますが、この2つの元が SL2(Z) を生成することが、 ユークリッドのアルゴリズムを用いて構成的に証明できます。
を SL2(Z) の元とします。 a d-b c = 1 が成り立つためには gcd(a, c) = 1 でなければならないことを覚えておいてください。 a を c で割った商を q, 余りを r, すなわち a = q c + r とすると、

となります。A の左から S, S-1, T をいくつか掛けることによって第1列成分が a, c から -c, r に変化しました。 ユークリッドのアルゴリズムを r0 = a, r1 = c で開始したときの r2 が r ですから、 ± がずれますが、第1列成分は r0, r1 が -r1, r2 へ変化したことになります。 この操作を続ければ最終的に (2,1)-成分 ( r の部分 ) は 0、 (1,1)-成分の絶対値は gcd(a, c) = 1 になり、 S のべき乗と ± だけ違う行列に変形できます。

 例をひとつ挙げておきましょう:
従って
 この変形は次の Python プログラムで得られます:
#-*- coding: utf-8 -*-
# SL2Z.py

import random

# Euclidean algorithm
# returned value = [d, x, y]
#   with d = gcd(a, b) = a + x + b * y
def euclid(a, b):
    if b == 0:
        if a >= 0:
            return [a, 1, 0]
        else:
            return [-a, -1, 0]
    else:
        r0, x0, y0 = a, 1, 0
        r1, x1, y1 = b, 0, 1
        while r1 != 0:
            q = r0 / r1
            r2 = r0 - q * r1
            x2 = x0 - q * x1
            y2 = y0 - q * y1
            r0, x0, y0 = r1, x1, y1
            r1, x1, y1 = r2, x2, y2
        if r0 < 0:
            r0, x0, y0 = -r0, -x0, -y0
        return [r0, x0, y0]

# multiplication of (2,2)-matrices
def matmul(a, b):
    c00 = a[0][0] * b[0][0] + a[0][1] * b[1][0]
    c01 = a[0][0] * b[0][1] + a[0][1] * b[1][1]
    c10 = a[1][0] * b[0][0] + a[1][1] * b[1][0]
    c11 = a[1][0] * b[0][1] + a[1][1] * b[1][1]
    return [[c00, c01], [c10, c11]]

# display (2,2)-matrix
def matprint(a):
    print "%2d %2d" % (a[0][0], a[0][1])
    print "%2d %2d" % (a[1][0], a[1][1])
    print

# random integer in [m, n]
def r(m = 0, n = 64):
    return random.randint(m, n)

s = [[1,  1], [0, 1]]
t = [[0, -1], [1, 0]]

# initialization
while 1:
    r0 = r()
    r1 = r()
    d, x, y = euclid(r0, r1)
    if d == 1:
        break
z = r(-5, 5)    
a = [[r0, -y + r0 * z], [r1, x + r1 * z]]
#a = [[35, 27], [22, 17]] # test data
w = "A :" # transformation
v = ""    # expression
u = 0      
op = []   # operations

print w
matprint(a)

while a[1][0] != 0:
    q = a[0][0] / a[1][0]
    sq = [[1, -q], [0, 1]] # (-q)-th power of S
    a = matmul(t, matmul(sq, a))
    w = ("T S^(%d) " % (-q)) + w
    v = v + (" S^(%d) T" % q)
    u = 1 - u
    op = op + [[[1, q], [0, 1]], t]
    print w
    matprint(a)

if a[0][0] == -1:
    a = [[-a[0][0], -a[0][1]], [-a[1][0], -a[1][1]]]
    w = "T^2 " + w
    if u == 0:
        v = v + " T^2"
        op = op + [t, t]
    print w
    matprint(a)

if a[0][1] != 0:
    q = a[0][1]
    sq = [[1, -q], [0, 1]]
    a = matmul(sq, a)
    w = ("S^(%d) " % (-q)) + w
    v = v + (" S^(%d)" % q)
    op.append([[1, q], [0, 1]])
    print w
    matprint(a)

print "A =", v
print

b = [[1, 0], [0, 1]]
for c in op:
    b = matmul(b, c)    
print "Check: "
matprint(b)

戻る