見出し画像

Excelと多倍長整数の四則演算の4

Excelでどこまでできるかチャレンジ!の4回目です。
回をおって、私のノートパソコンのフリーズ回数も増えてきました・・・。
今回は指数演算(べき乗)です。

第5章 指数

べき乗の方法

べき乗のアルゴリズムは、いろいろあるようなのですが、正確な名前と方法がよくわかりません。(私の勉強不足です)
検索してなんとなくわかった方式を試してみます。

  • 愚直ループ

  • バイナリ法

  • より高速なやつ?

愚直ループ

文字通り、$${x^n}$$なら、$${n}$$回ループする方法です。
コードと計算結果がこちら。

Function Name : CnPowHo

Discription : 文字列数のベギ乗。愚直版。

Argments : cx, n

Function definition :
=LET(
    _n, VALUE(_CnAbs(n)),
    _sx, _GetCnSign(cx),
    _sz, IF(AND(_sx = -1, ISODD(_n)), "n", "p"),
    _ax, _CnToAn(cx),
    _az, IF(_n > 1, _AnPowHo(_ax, _n), IF(_n = 1, _ax, 1)),
    _sz & _AnToCn(_az)
)
Function Name : _AnPowHo

Discription : 配列数のベギ乗。愚直版。

Argments : ax, n

Function definition :
=REDUCE(
	ax, SEQUENCE(n - 1), 
	LAMBDA(
		_a, _s, 
		_AnMul(_a, ax)
	)
)
2^1000愚直ループの結果

ちゃんと動きました。
_AnPowHo関数の中身は、単純なREDUCE関数でのループですので、説明もいらないかと思います。
CnPowHo関数で$${n>1}$$として2以上の場合のみ計算するようにしています。
あと、_AnPowHo関数の中に_AnMul関数というやつがいますが、これは第3章の_AnMulWc関数と_AnMulFft関数を、桁数によって使い分けている関数です。後ほど、ファイルを置いておくので、中身が気になる方は直接見てください。

バイナリ法

バイナリ法の正確な定義はわかりませんが、皆さんが言っているのはたぶんこんな感じ。

  • 例えば$${x^n}$$を計算する場合、

    • $${y:=1}$$、$${z:=x}$$とおく。

    • $${n}$$が奇数のとき、$${y := y × z}$$をする。

    • $${z := z × z}$$をする

    • ビットシフト$${n:=n>>1}$$する。

  • これを、$${n = 0}$$になるまで繰り返す。

  • $${y}$$を返す。

例えば$${2^5}$$を考えると、

  • $${y=1,z=2,n=5}$$が初期値。

    • $${n=5}$$は奇数なので、$${y:=y×z=1×2=2}$$

    • $${z:=z×z=2×2=4}$$

    • $${n:=5>>1=2}$$

  • 1周して、$${y=2,z=4,n=2}$$となる。

    • $${n=2}$$は偶数なので、$${y}$$はそのまま。

    • $${z:=z×z=4×4=16}$$

    • $${n:=2>>1=1}$$

  • 2周して、$${y=2,z=16,n=1}$$

    • $${n=1}$$は奇数なので、$${y:=y×z=2×16=32}$$

    • $${z:=z×z=16×16=256}$$

    • $${n:=1>>1=0}$$

  • $${n=0}$$になったので終了。$${y=32}$$を返す。

こんな感じです。
最後の$${z:=z×z=16×16=256}$$は無駄ですね。条件をつけて処理しないようにしましょう。
さて、ループ内に$${y}$$と$${z}$$の2変数があるので、REDUME関数で実装するのはちとめんどうくさい・・・。
とりあえず、VSTACK関数で2変数を強引に並べてループしてみたのがこちら。

Function Name : _AnPowBi

Discription : 配列数のベギ乗。バイナリ―法

Argments : ax, n

Function definition :

=LET(
    _x, ax,
    _n, n,
    _v, REDUCE(
        VSTACK(1, _x),
        SEQUENCE(1 + LOG(_n, 2), , 0),
        LAMBDA(_a, _s,
            LET(
                _y, LET(_r, CHOOSEROWS(_a, 1), FILTER(_r, NOT(ISERROR(_r)))),
                _z, LET(_r, CHOOSEROWS(_a, 2), FILTER(_r, NOT(ISERROR(_r)))),
                _ns, BITRSHIFT(_n, _s),
                _y2, IF(BITAND(_ns, 1), _AnMul(_y, _z), _y),
                _z2, IF(_ns = 1, 0, _AnMul(_z, _z)),
                VSTACK(_y2, _z2)
            )
        )
    ),
    CHOOSEROWS(_v, 1)
)

では、説明です。

  • _x、_nはそのままですね。

  • _v:REDUCE関数の返り値が入ります。これはVSTACK関数で並べた2行のデータになっています。{ _y2, _z2 }

  • REDUCE関数内

    • 初期値:1行目に1、2行目に_xを入れています。1行目が$${y}$$、2行目が$${z}$$にあたります。

    • ループ配列:指数をバイナリ表示にしたときの長さです。

      • $${n=5}$$の場合、$${1+LOG(5,2)=1+2=3}$$なので、$${SEQUENCE(3,,0)={0,1,2}}$$となります。

    • _y:_aの1行目を取ってきて、エラーセルをFILTER関数で除去しています。1行目と2行目の長さが違うと、エラーが入ってしまうので苦肉の策です。

    • _z:_aの2行目を代入

    • _ns:_nをビットシフトしています。最初の_sは0なので、シフトしません。

    • _y2:_nsが奇数なら_yに_zを掛けます。奇数判定をBITAND関数でしているのは、なんとなくノリです。

    • _z2:_nsが0になったら計算終了。そうでなければ_zに_zを掛けます。

    • 最後に_y2と_z2をVSTACK関数で並べてループします。

  • ループが終わったら、_vの1行目に$${y}$$の値が入っているので取り出して終わりです。

実行結果がこちら、

2^1000バイナリ法の結果

早いですね。
単純に20倍ぐらいの速さになっています。
しかし、愚直版CnPowHo関数が999回乗算しているのに対し、バイナリ版CnPowBi関数の乗算回数はかなり少ないはず。
実際の乗算回数はこうなると考えられる。

  • 1000をバイナリ表記すると、「1111101000」。10桁で、1が6個。

  • $${z×z}$$が$${10-1=9}$$で9回、$${y×z}$$が6回、合計15回。

$${999÷15=66.6}$$なので、60倍ぐらい早くなってほしい!
このコードだと、変数周りでごちゃごちゃしているのが重そうなので、シンプル版も作ってみました。

Function Name : _AnPowB2

Discription : 配列数のベギ乗。バイナリ法改良版

Argments : ax, n

Function definition :
=REDUCE(
    1,
    LET(
        _v1, IF(ISODD(n), 1, 2),
        _v2, REDUCE(
            _v1,
            SEQUENCE(LOG(n, 2)),
            LAMBDA(_a, _s,
                LET(
					_ns, BITRSHIFT(n, _s), 
					IF(ISODD(_ns), VSTACK(1, 0, _a), VSTACK(0, _a)))
            )
        ),
        FILTER(_v2, _v2 < 2)
    ),
    LAMBDA(
		_a, _s, 
		IF(_s = 0, _AnMul(_a, _a), _AnMul(_a, ax))
	)
)

配列側で、$${x×x}$$をするか、$${y×z}$$をするのか決めて、単純ループです。ループ回数は多くなりますが、乗算の回数は同じです。
配列を作るLET関数内を説明します。

  • _v1:一番最後の処理を決めます。

    • 奇数のときは「1」で良いのですが、偶数のときは「なにもしない」のでブランクにしようかとも思ったのですが、そしたらブランクの配列ができてしまうので、適当に「2」にして最後にFILTER関数で消しています。

  • _v2:「$${n}$$をバイナリ表記にしたときの長さ ー 1」回ループ。

    • $${n}$$をビットシフトして、奇数ならVSTACK関数で頭に{1,0}をつけます。

    • 逆に偶数なら{0}を頭につけます。

  • FILTER関数で「2」を消します。

これでなぜうまくいくのかは、次のように考えています。

  • $${n}$$に対して、奇数なら1を引く、偶数なら「2」で割る、という処理を「1」になるまで繰り返す。

  • 例えば、$${n=6}$$の場合、

    • 6→3→2→1

  • 偶数を0、奇数を1に置き換える。

    • {6,3,2,1} → {0,1,0,1}

  • 左右をひっくり返す。これが欲しい配列。

    • {0,1,0,1} → {1,0,1,0}

上記コードでは、ビットシフトを使うことで、「奇数なら1引いて2で割る」になっています。奇数と偶数の処理を1回でやっているかんじです。
このバージョン2の実行結果がこちら

2^10000バイナリ法比較

1.5倍ほど早くなりました。

より高速なやつ?

検索すると「window法」や「sliding window法」「バイナリ法より乗算回数が少ない方法」などが、ヒットします。
しかし、どれもまだ理解できないので、本日はここまで。
いつか理解して、よりはやくなったら追記したいと思います。

勉強結果

今回の勉強結果はこちら

デバッグよろしくお願いします。

いいなと思ったら応援しよう!