#6 代数的数の演算 まとめ
#3 代数的数の演算と終結式 以降、代数的数の演算についての話題を引っ張ってきたが、ここらでまとめたいと思う。
#3 の最後で取り上げた課題としては、終結式の計算と、演算結果の分離区間についてのものがあったが、前者は #4 擬除算と多項式剰余列 で一応実装できた。 後者の方針の一つとして、前回(#5 区間演算と計算可能実数)は区間列の扱いを掘り下げた。 今回は、もう一つの方針として、実根の距離の最小値の評価を紹介する。
判別式と根の分離
実根の距離の評価に入る前に、判別式について論じておく。
判別式
二次方程式の判別式はみなさんご存知だろう。 二次方程式 の判別式は で、 なら重根を持つ、というやつだ(実係数の場合は、 の値によって実根の個数がどうのこうのということも言える)。 この方程式の(重複を込めて)2つの根を と書けば、 となる。
三次方程式 にも判別式と呼ばれる量があり、 であればこの三次方程式は重根を持つ:
一般に、 次方程式 の根を重複を込めて と書いたとき、判別式 (discriminant) を と定義する。 基本的な性質として、多項式 が重根を持つ時、かつその時に限り となる。
演習問題: の場合の判別式を、係数を使って表せ(ヒント:根と係数の関係を使う)。 特に、 の判別式が となることを確かめよ。
さて、多項式 が重根を持つということは、 とその微分 の GCD が非自明となることと同値であった。 さらに、終結式の性質より、それは と同値だ。
この事実、および、判別式が「根に関する積」で書けることから、判別式と終結式に密接な関係があると察した読者も多いだろう。 実際、終結式を使って判別式を表すことができる。定理. 次多項式 について、 の判別式 は次のように書ける:
証明. とおく。 この時、それぞれの について が成り立つ。 なぜなら、 は と の積として書けるので、微分のライプニッツ則より となり、それに を代入すれば良い。
終結式と根の関係より、 が成り立つ。 これを使うと を得る。
が終結式を最高次の係数 で割っているのは、 が で割り切れるからである。 というのは、シルベスター行列の行列式で書いたときに一番上の行に が単独で現れるからだ。 つまり、次が言える:
系. -係数多項式 の判別式 は の元となる。
「 の元となる」よりは「 の元として定義できる」と言った方が正確かもしれない。
とすれば次が得られる:
系. 整数係数多項式の判別式は整数である。 特に、 (つまり、 が無平方)の場合、 が成り立つ。
根の距離の最小値
の根の距離の最小値(英語では root separation と呼ぶ)を とおく。 最小値を実現する と を , とおいても一般性を失わない。
の実根を含む区間の幅を 未満に取れば、実根を確実に分離することができる。 そのため、 を下から評価したい。
根の限界(根の絶対値の上界;#2 で取り扱った)を で表すと、2つの根の距離を と評価できる。 それを踏まえて の絶対値を取ると より、 という評価を得る。 右辺は の係数のみから計算できる。
さらに、 が整数係数で無平方の場合、 なので、判別式の計算を省いて という評価ができる。
こういう「代数的数同士の距離」というトピックは、超越数論にも関わってくるようである。 今回はこれ以上は掘り下げない。
代数的実数の演算の実装
四則演算
まずは和から考えよう。
定義多項式 を持つ と、定義多項式 を持つ を考える。 #3 の結果より、和 を根に持つ多項式として を取ることができる。 必要であれば を で割ることによって無平方とできる。 あとは、その分離区間を求めたい。 分離区間を求める方法として、3通り考えられる。
方法1・実根の数え上げを使う: は計算可能実数、 も計算可能実数なので、 も計算可能実数である。 に収束する縮小区間列 を取る。 のスツルム列を計算すれば、それぞれの区間 に何個実根が存在するかがわかる。 を十分大きく取れば、区間 に存在する実根を1個にできる(つまり、 が分離区間になる)。
方法2・根の距離を使う方法1: 方法1と同様に、 に収束する縮小区間列 を取る。 さっきの節の結果を使うと、 の下からの評価 が計算できる。 区間 の幅が 未満になるように を十分大きく取れば、 が分離区間になる。
方法3・根の距離を使う方法2: の分離区間 を幅 未満で計算、 の分離区間 を幅 未満で計算する。 すると、 の分離区間は と取れる。 (この方法は和や差の場合は良いが、積の場合は幅を事前に見積もるのが難しく、使いづらい。)
ここでは、方法1を採用することにする。 この方法の欠点は、スツルム列の計算が必要になることである。
なお、単項マイナス negate
と逆数 recip
に関しては、分離区間を計算し直す必要はない。
AlgReal
型に対する各種演算の実装は AlgReal.hs
に追加で書き込んでいくことにする。
多項式と分離区間 から AlgReal
型の代数的実数を構築する関数 mkAlgReal
はすでに用意しているが、追加で補助関数を用意しておく:
mkAlgRealWithInterval :: UniPoly Rational -> Interval -> AlgReal
mkAlgRealWithInterval f (Iv a b) = mkAlgReal f a b
-- | 与えられた無平方多項式と、その実根を表す計算可能実数から、代数的実数を構築する
mkAlgRealWithCReal :: UniPoly Rational -> CReal -> AlgReal
mkAlgRealWithCReal f (CReal xs) = mkAlgRealWithInterval f (head $ dropWhile (\(Iv a b) -> countRealRootsBetween a b f >= 2) xs)
では、 AlgReal
型に対する Num
クラスのインスタンスを作っていこう。 先にコードを提示し、そのあとに解説する。
instance Num AlgReal where
negate (FromRat x) = FromRat (negate x)
negate (AlgReal f s a b) = AlgReal (compP f (-ind)) (-s) (-b) (-a)
まずは単項マイナスである。 単項マイナスに関しては、分離区間は単純に区間として符号を逆転させるだけで良い:
定義多項式は を に変える。コードで書くと compP f (-ind)
であるが、この実装に関しては改善の余地がある。
前回、 AlgReal
データ構築子の定義に「多項式の符号の変化」を表すフィールド( の符号と同じ)を追加したが、それも逆になる(符号を反転させる)。
-- Num クラス 続き(和)
FromRat x + FromRat y = FromRat (x + y)
FromRat k + AlgReal f s a b = mkAlgReal (compP f (ind - constP k)) (a + k) (b + k)
x@(AlgReal _ _ _ _) + y@(FromRat _) = y + x
x + y = mkAlgRealWithCReal (squareFree $ resultant_poly f_x_y g) (algRealToCReal x + algRealToCReal y)
where f = mapCoeff constP $ definingPolynomial x
f_x_y = compP f (constP ind - ind) -- f(x-y)
g = mapCoeff constP $ definingPolynomial y
【12月17日 修正】FromRat
と AlgReal
の和がゼロになる場合を考慮し忘れていたので修正。
和の実装を見ていこう。 有理数同士の場合は Rational
型の和を使う。
有理数 と代数的実数 を足す場合は、定義多項式は から になる( ならば なので)。 なので、符号の変化はそのままである。 分離区間は、 だけ平行移動する:
代数的実数同士の和が肝心で、定義多項式は (を無平方にしたもの)となる。 この際、2変数多項式環 が登場するわけだが、2つの不定元は、内側の が constP ind :: UniPoly (UniPoly Rational)
、外側の が ind :: UniPoly (UniPoly Rational)
と書ける。
終結式の計算には、前々回の演習問題とした resultant_poly
関数を使う。 分離区間の計算には、一旦計算可能実数を経由する。
-- Num クラス 続き(差)
FromRat x - FromRat y = FromRat (x - y)
FromRat k - AlgReal f s a b = mkAlgReal (compP f (constP k - ind)) (k - b) (k - a)
AlgReal f s a b - FromRat k = mkAlgReal (compP f (ind + constP k)) (a - k) (b - k)
x - y = mkAlgRealWithCReal (squareFree $ resultant_poly f_x_y g) (algRealToCReal x - algRealToCReal y)
where f = mapCoeff constP $ definingPolynomial x
f_x_y = compP f (constP ind + ind) -- f(y+x)
g = mapCoeff constP $ definingPolynomial y
【12月17日 修正】FromRat
と AlgReal
の差がゼロになる場合を考慮し忘れていたので修正。
差も、和とほとんど同じである。
-- Num クラス 続き(積)
FromRat x * FromRat y = FromRat (x * y)
FromRat k * AlgReal f s a b
| k == 0 = 0
| k > 0 = AlgReal (compP f (scaleP (recip k) ind)) s (a * k) (b * k)
| k < 0 = AlgReal (compP f (scaleP (recip k) ind)) (-s) (b * k) (a * k)
x@(AlgReal _ _ _ _) * y@(FromRat _) = y * x
x * y = mkAlgRealWithCReal (squareFree $ resultant_poly y_f_x_y g) (algRealToCReal x * algRealToCReal y)
where f = definingPolynomial x
y_f_x_y = UniPoly $ V.reverse $ V.imap (\i a -> constP a * ind^i) $ coeff f -- y^n f(x/y)
g = mapCoeff constP $ definingPolynomial y
積も似たような感じで実装するが、使う終結式は と、やや複雑になる。 一見すると分数(有理式)が出てくるように見えるが、 と書けば なので、有理式にはならない。 計算には、注意深く多項式の係数を操作すれば良い。
-- Num クラスの残り
abs x | x >= 0 = x
| otherwise = negate x
signum x | x > 0 = 1
| x == 0 = 0
| x < 0 = -1
fromInteger n = FromRat (fromInteger n)
Num
クラスの残りの演算は適当に実装する。
instance Fractional AlgReal where
recip (FromRat x) = FromRat (recip x)
recip (AlgReal f s a b) = AlgReal (UniPoly $ V.reverse $ coeff f) s' (recip b) (recip a)
where s' | even (degree' f) || 0 < a = -s
| otherwise = s
fromRational = FromRat
Fractional
クラスも実装する。 fromRational
は FromRat
データ構築子をそのまま使えるので、あとは逆数関数 recip
の実装である(除算に関してはデフォルト実装を使えば良い)。
逆数に関する定義多項式は、係数を反転させる。 区間も、区間として逆数を取れば良い(AlgReal
データ構築子における区間は、ゼロを含まないようなものになっていることに注意)。
符号の変化はどうだろうか。 の定義多項式が次数 の で の定義多項式を とすると、 より となる。つまり、
- が偶数または が正の場合は、 の符号は と逆
- が奇数かつ が負の場合は、 の符号は と同じ
である(s'
の計算)。
平方根、べき根
平方根や冪根( 乗根)を計算する関数があると便利である。 これらは「多項式の実根を求める操作」の一種ではあるが、使用頻度が高いと考えられるので、用意しておく価値はあるだろう。
多項式の根は一般に複数あるが、 の符号については
- が非負の場合は、 も非負
- が奇数で が負の場合は は負
- が偶数で が負の場合は、エラー
とするのが普通だろう。
有理数の平方根
有理数 に対して、多項式 の実根のうち、正のもの(区間 にあるもの)を返す。
sqrtQ :: Rational -> AlgReal
sqrtQ a | a > 0 = case realRootsBetween (ind^2 - constP a) (Finite 0) PositiveInfinity of
[sqrt_a] -> sqrt_a
_ -> error "sqrt: none or multiple roots"
| a == 0 = 0
| otherwise = error "sqrt: negative"
有理数のべき根
の実根のうち、 と符号が同じもの(区間 または にあるもの)を返す。
nthRootQ :: Int -> Rational -> AlgReal
nthRootQ !n !a
| n == 0 = error "0th root"
| n < 0 = nthRootQ (-n) (recip a)
| a > 0 = case realRootsBetween (ind^n - constP a) (Finite 0) PositiveInfinity of
[b] -> b
l -> error ("nthRoot: none or multiple roots " ++ show l)
| a == 0 = 0
| odd n = case realRootsBetween (ind^n - constP a) NegativeInfinity (Finite 0) of
[b] -> b
l -> error ("nthRoot: none or multiple roots " ++ show l)
| otherwise = error "nthRoot: negative"
代数的実数の平方根
定義多項式 に を代入したもの は、 を根に持つ多項式となる。 の実根の中から、2乗したものが の分離区間に入るものを返せば良い。
sqrtA :: AlgReal -> AlgReal
sqrtA (FromRat x) = sqrtQ x
sqrtA x = case filter (\y -> FromRat a < y^2 && y^2 <= FromRat b) $ realRootsBetween (compP f (ind^2)) (Finite 0) PositiveInfinity of
[sqrtx] -> sqrtx
r -> error $ "sqrt: none or multiple roots" ++ show r
where f = definingPolynomial x
(a,b) = isolatingInterval x
代数的実数のべき根
定義多項式 に を代入したもの は、 を根に持つ多項式となる。 の実根の中から、 乗したものが の分離区間に入るものを返せば良い。
nthRootA :: Int -> AlgReal -> AlgReal
nthRootA !n (FromRat x) = nthRootQ n x
nthRootA !n x
| n == 0 = error "0th root"
| n < 0 = nthRootA (-n) (recip x)
-- now n must be positive
| x == 0 = 0
| x > 0 = case filter (\x -> FromRat a < x^n && x^n <= FromRat b) $ realRootsBetween (compP f (ind^n)) (Finite 0) PositiveInfinity of
[rx] -> rx
_ -> error "nthRoot: none or multiple roots"
-- x must be negative
| odd n = case filter (\x -> FromRat a < x^n && x^n <= FromRat b) $ realRootsBetween (compP f (ind^n)) NegativeInfinity (Finite 0) of
[rx] -> rx
_ -> error "nthRoot: none or multiple roots"
| otherwise = error "nthRoot: negative"
where f = definingPolynomial x
(a,b) = isolatingInterval x
べき乗、有理数係数多項式への代入
かけ算を実装済みなので、それを使った冪乗( 乗)の実装ももちろんできる。 やってみよう。
例えば、 の根 の10乗を計算するとしたら、皆さんはどうするだろうか? 指数10を二進法で表して により4回のかけ算で済ませる!……と答えた方は不正解である。 かけ算を9回行う、と答えた方ももちろん不正解である。
たった4回のかけ算、と思われるかもしれないが、現在の素朴な(因数分解をしない)実装では、結果として得られる代数的数の定義多項式の次数は となってしまう。 このように次数が大きいと、計算には非常に時間がかかる。 (演算に必要な時間や空間が一定ならばこのような繰り返し乗算による方法はうまくいくが、代数的数には通用しない)
賢い読者は、関係式 を利用することを思いついたかもしれない。 この関係式を使って の冪乗を計算すると、 となる。 手計算でやってみるとなかなか大変だが、やっていることは有理数係数の多項式除算である。
あとは に対して代数的実数のかけ算等を使えば良い。 この例では、代数的実数どうしの演算は1回しか使わない。
「関係式 を使って を計算する」というのは、言い方を変えれば「 についての多項式 を で割った余りを計算する」ということである。 多項式除算はすでに実装済みで、何回も使っている。
Haskell で実装すると次のようになるだろう:
powIntA :: AlgReal -> Int -> AlgReal
powIntA _ 0 = 1
powIntA x n | n < 0 = recip $ powIntA x (-n)
powIntA (FromRat x) n = FromRat (x^n)
powIntA x n = case (ind^n) `modP` f of
g -> valueAt x (mapCoeff FromRat g)
where f = definingPolynomial x
case 式以下が、多項式除算を使う部分である。
同じ理屈で、「次数の高い有理数係数多項式に代数的実数を代入する操作」を多項式除算を使って実装できる。
演習問題:有理数係数多項式に代数的実数を代入したものを計算する関数 valueAtA :: AlgReal -> UniPoly Rational -> AlgReal
を実装せよ。
有理数乗
冪根と(整数による)冪乗を実装したので、有理数乗 も実装できる。
powRatA :: AlgReal -> Rational -> AlgReal
powRatA x y = nthRootA (fromInteger $ denominator y) (powIntA x (fromInteger $ numerator y))
ちなみに、有理数乗 の定義多項式は、終結式を使って と計算できる。
遊んでみよう
実装が済んだら、早速試してみよう。 $ stack repl
で REPL を起動する。
$ stack repl
algebraic-real-0.1.0.0: initial-build-steps (lib)
Configuring GHCi with the following packages: algebraic-real
GHCi, version 8.0.2: http://www.haskell.org/ghc/ :? for help
[1 of 5] Compiling Numeric.AlgebraicReal.UniPoly ( .../src/Numeric/AlgebraicReal/UniPoly.hs, interpreted )
...
[5 of 5] Compiling Numeric.AlgebraicReal.AlgReal ( .../src/Numeric/AlgebraicReal/AlgReal.hs, interpreted )
Loaded GHCi configuration from /private/var/folders/0c/.../T/ghci41849/ghci-script
*Numeric.AlgebraicReal.UniPoly Numeric.AlgebraicReal.AlgReal Numeric.AlgebraicReal.CReal Numeric.AlgebraicReal.Interval Numeric.AlgebraicReal.Resultant Numeric.AlgebraicReal.UniPoly> :set prompt "> "
> sqrtQ 2
AlgReal (UniPoly [(-2) % 1,0 % 1,1 % 1]) 1 (3 % 4) (3 % 2)
> 2 :: AlgReal
FromRat (2 % 1)
> (sqrtQ 2)^2
AlgReal (UniPoly [(-4) % 1,0 % 1,1 % 1]) 1 (9 % 16) (9 % 4)
> (sqrtQ 2)^2 == 2
True
を2乗すると2となることが確認できた(ただし、 2
と (sqrtQ 2)^2
では表示させた時の見た目は異なる)。
> sqrtQ 2 + sqrtQ 8
AlgReal (UniPoly [36 % 1,0 % 1,(-20) % 1,0 % 1,1 % 1]) 1 (3 % 1) (6 % 1)
> sqrtQ 2
AlgReal (UniPoly [(-2) % 1,0 % 1,1 % 1]) 1 (3 % 4) (3 % 2)
> 2 * sqrtQ 2
AlgReal (UniPoly [(-2) % 1,0 % 1,1 % 4]) 1 (3 % 2) (3 % 1)
> sqrtQ 8
AlgReal (UniPoly [(-8) % 1,0 % 1,1 % 1]) 1 (9 % 4) (9 % 2)
> 2 * sqrtQ 2 == sqrtQ 8
True
等式 が確認できた。
係数膨張
もっと複雑な代数的数も扱ってみよう。
> let x = ind
> let [a0,a1,a2,a3] = realRoots $ (x-4)*(x-3)*(x-2)*(x+3)+1
> a0
AlgReal (UniPoly [(-71) % 1,54 % 1,(-1) % 1,(-6) % 1,1 % 1]) (-1) ((-9) % 2) ((-9) % 4)
> a1
AlgReal (UniPoly [(-71) % 1,54 % 1,(-1) % 1,(-6) % 1,1 % 1]) 1 (9 % 8) (9 % 4)
> a2
AlgReal (UniPoly [(-71) % 1,54 % 1,(-1) % 1,(-6) % 1,1 % 1]) (-1) (9 % 4) (27 % 8)
> a3
AlgReal (UniPoly [(-71) % 1,54 % 1,(-1) % 1,(-6) % 1,1 % 1]) 1 (27 % 8) (9 % 2)
多項式 から適当に代数的数をでっち上げる。
根どうしの和を計算してみよう。
…
筆者の環境ではしばらく時間がかかり、画面が数字で埋め尽くされた。
これは、すでに何回か触れた、係数膨張である。 この膨大な数字が虚仮威しに過ぎないことは、モニック多項式に変換する(定数倍する)ことでわかる:
> toMonic (definingPolynomial it)
UniPoly [11778048 % 5,(-97967232) % 5,191816064 % 5,(-23103648) % 1,(-57623376) % 5,130774488 % 5,(-17815540) % 1,32400936 % 5,(-6375679) % 5,399798 % 5,111231 % 5,(-31524) % 5,3631 % 5,(-42) % 1,1 % 1]
これまでに載せた通りの実装では、終結式の計算と、無平方成分の計算で係数膨張が起こる。 そこで、それぞれの計算後に、モニック多項式への変換 toMonic
を挟む。
演習問題 適宜モニック多項式に変換することにより、係数膨張を軽減せよ。 具体的には、 UniPoly.hs
の squareFree
関数と、さっき実装した各演算の resultant_poly
の呼び出しの部分を編集する。
因数分解へ
これで代数的実数についての演算を一通り実装できた。 だが、大きな課題として、定義多項式の因数分解をしていないために多項式の次数が不必要に増大してしまうという欠点がある。
例えば、4の平方根 を計算させると有理数 FromRat 2
ではなく2次の代数的数 AlgReal (UniPoly [(-4) % 1,0 % 1,1 % 1]) 1 (5 % 4) (5 % 2)
が得られる(もちろん、有理数 2 と比較すれば「等しい」と判定される)。 また、 の次数は2( なので)となるべきであるが、今の実装で計算すると次数が4となってしまう。
これらの問題を解決するには、終結式の計算で得られた多項式を因数分解し、演算結果の代数的実数に対する最小多項式を得ることが必要である。 (ちなみに、有理数かどうかの判定に関しては、因数分解しなくても判別する方法はある。)
というわけで早速次回から多項式の因数分解に取り掛かりたいところだが、その前に、多項式の係数を整数係数に変えておきたい。 今の実装では、代数的実数の定義に使う多項式は有理数係数としているが、実際のところ、そこは整数係数で十分である。 それに、整数係数の方が何かと効率的であるし、有理数係数多項式の因数分解においても、結局は整数係数多項式の因数分解に帰着させることになる。
そこで、次回は多項式の係数を整数係数化し、その次から多項式の因数分解に取り掛かることにする。