Brainf**k講座(8) BCD演算
はじめに
※本記事は、2016/7/15にHatenaBlogに投稿した記事を移行したものです
今回でいよいよ最後になりました。トリとして、BCD演算を紹介したいと思います。
講座一覧
-
Hello, BF!
導入およびシンプルなループによる数値生成 -
数値入力と簡単なループ
,
による入力と数値解釈、簡単なループの実例 -
スライド型ループ
ループ継続の判断を行うセルの位置をずらしていくループ -
条件分岐
if文、if-else文相当の処理 -
条件分岐の多段化
switch文相当の処理 -
非破壊的条件分岐
条件変数を壊さない条件分岐 -
divmodアルゴリズムと数値出力
除算・剰余算のアルゴリズムと、それを利用した不定長の数値出力 -
BCD演算 (今回)
10進数による任意長の数値の計算
※上記以外も含め、各記事はBrainf**kの記事一覧から辿ることができます
実行環境
-
ideone
オンラインのIDE環境で、bffという処理系が使えます。この講座は基本的にこのbffに対応したものです。 -
Brainf**kインタープリタ
あじさんという方が作られたブラウザ上で動くインタプリタです。細かい挙動の調整や、途中のセルの状態のダンプができたり、重宝しています。
※オプションで「バッファの型」を「4バイト符号付き整数」、「標準入力の終端」を「-1」と設定すると、bffと同じ挙動になります。
今回の話題
BCD演算
BCDというのは Binary-Coded Decimal のことで、要するに2進数なのだけど、10進数のように桁を分けて演算するというものです。ちょうど、筆算で計算するようなやり方を、Brainf**kのセル上で実現するイメージとなります。( 本当に Binary-Coded なのかどうかは置いといて )
前回足し算と数値の出力の問題を紹介したのですが、実はあのコードだと1つ困ったことがあります。それは、対象の数値が大きくなると、比例して処理時間が増大してしまうことです。どうしてかというと、足し算にせよ divmod にせよ、対象の数値をループカウンタとしたループで処理しているからです。
そこで、大きな数を処理したい場合にピッタリなのがこのBCD演算ということになります。桁の繰り上がり・桁数の調整が少し面倒ではありますが、複数桁からの集積、複数桁への展開の処理なく、ダイレクトに桁を扱え、処理が速いのが大きなメリットとなります。
BCD演算の実例1:フィボナッチ数列
まずは例として、フィボナッチ数列を計算するという問題です。
- 入力された数値 n ( 改行無し ) に対し、フィボナッチ数列の n 項目の値を出力する
フィボナッチ数列自体の説明は割愛します ( ご存じでない方はWikipediaの記事をご覧ください )。なお、初めの項は、1,1,2,… となっているものとします。
フィボナッチ数列の計算自体は、項をずらすように漸化式をループで回すことでできます。Rubyでのコードは次のようになります。
a,b=0,1
gets.to_i.times{
a,b=b,a+b
}
puts b
ただ、フィボナッチ数列は指数的に値が大きくなっていきますので、1つのセルに値を保存するような方法だと、すぐに処理時間が爆発してしまいます。
フィボナッチ数列計算の実装
そこで、BCD演算でフィボナッチ数列計算を実装したのが次のコードです。( ideoneでの実行例はこちら )
** set initial value and input n
** memory layout: nct_ab___ab____*
** a:fib(0) b:fib(1) *:terminater minus 1
*-<<<<<b<<<<<b+<<<<
c,+[
>t+++++++[-<c------->t]
t<<n[->c++++++++++<n]
n>c[-<n+>c]
c,+]
** memory layout:
* Nn___ab___ab ~ __ab____*
* N___AB____ab ~ __ab____*
* N___ABdr__ab ~ __ab____*
* N___AB___AB_ ~ _AB______
<n-[n
* move n to N ( next n )
n-[-<N+>n]
* digit loop
n->>>>>b+[b-
* move b to A ( next a ) and add to a=B ( next b )
b[-<aB+<A+>>b]
* divmod by 10 ( add quotient to the right a )
bd++++++++++
<B[->d-[d>r+>_]>r_?[r+[-<d+>r]>>>a+<_]<<<<B]
* clear d and move r to B
>d[-]>r[-<<B+>>r]
>>>>b ( slide )+]
* judge if extend digit or not
<<<<<<b[>>>>>]>>>>>-<+[-<+]n
<Nn]
* set terminater ( minus 1 ) to n
->+[->+]
* skip the highest digit ( 0 )
<<<<<<<<<<b
** output loop
** memory layout: *___ab___ab ~ _ab___abt
b+[b-
>t++++++[-<b++++++++>t]<b.
<<<<<b+]
途中でセルの役割が切り替わりますので、“memory layout”のコメントを幾つか入れています。特徴としては次の通りです。
- 一つ前の項を a、現在の項を b として、1の位同士、10の位同士、…と、同じ位の桁を隣り合わせにして配置する
- ループの繰り返し毎に位置を1つずつずらしながら計算する。( 現在のn,a,bから新しいN,A,Bを設け、これを次のn,a,bとして使う )
- 桁数が不定長なので、-1 のセルを設けて最上位の桁の方の終端とする。
計算の途中、
また、次の特徴もあります。
- 繰り上がりに対処するため、変型版のdivmodにより、10での割り算を行う
- 商q のセルを設けず、繰り上がり対象の右の a に直接商を加算する
- a,bを5セルずつずらして配置しているのは、割り算用のセルの余裕を持たせるため
- 余り r が真の B の値となる
- 桁数の増加に対応するため、常に最上位の桁を 0 にしておき、0 でなくなったら桁を拡張する。
- 足し算の性質上、1回の繰り返しで高々1桁しか増えないため、最上位の桁の 0 は1つで良い
- この最上位の桁の 0 は、最後の出力の際スキップする
なお、B の値が10を超えなくても divmod は常に実行しています。ただ、a に加算する商が 0 で、余り r も元の B の値と等しくなるので、結果的に divmod しないのと同じ状況になります。
BCD演算の実例2:popcount
続いてはpopcountです。一般に ( かどうかは分かりませんが )、ある数を2進数表記した場合の 1 となっている桁の数のことを指します。問題としては次のようにします。
- 入力 n ( 改行なし ) に対し、n の popcount の数値を出力する
どのように求めるかと言うと、2進数に展開するときと同様、divmod 2 を繰り返すことになります。その際の余りが 1 になるところが 1 になる桁に相当しますから、余りの合計を計算すればそれが popcount です。
Rubyでのコードは次のようになります。
n=gets.to_i
pc=0
while n>0
pc+=n%2
n/=2
end
puts pc
大きな数値の n に対してBrainf**kで実装する時、1セルで数値を管理すると divmod 2 の計算が遅くなりますから、ここをBCD演算に替えます。
popcountの実装
それでは、Brainf**kによる実装です。
( ideoneでの実行例はこちら )
** input loop
** memory layout: mdt_dt_d~dt_*
* m: the highest digit marker
* d: each digits
* *: terminater
m->d,+[
>t+++++++[-<d------->t]
>>d,+ (slide)]
*-<+[-<+]m
** halving loop
** memory layout:
** M_mn__n__n~__n__*p
** ~Rh_rhn~__n__*p
** ~Rh_*P_p
** M: new highest marker
** hr: result of d divmod 2
** R: left r
** pP: popcount ( previous and new )
m>n+[-
** set a new marker
<<<M-
** n loop
>>>n+[-
** borrow
R<<<<[->>>>n++++++++++<<<<R]
** divmod by 2
>>>>n[<r[-<h+>>>_]>n_?[n<h+>>>_]<<n-]
>>>n+ (slide)]
** move p to P and add R ( the last R )
>p[-<<P+>>p]<<<<<R[->>>P+<<<R]
** set a new terminater and return to the marker
>>*-<+[-<+]M
** shrink if the highest digit is zero
>[<<<]>>>h
hn+ (slide)]
** divmod loop **
>p[
** divmod by 10
>++++++++++
<[->-[>+>>]>[+[-<+>]>+>>]<<<<<]
>[-]>+
>]
** output loop **
<[-
** convert a number to a character code
>++++++[-<++++++++>]
<.
<<<]
n=893 の時を例にとり、どのようにBCD演算を行っているかを次の図に示します。
上のフィボナッチ数列のコードでは、2数を5セル毎に配置し、ループ実行毎に 1 セルずつずらしていきましたが、今回は n を3セル毎に配置し、ループ実行毎に 2 セルずつずらしていきます。
なぜこのような違いが出るかと言うと、扱う数が 2種類か 1種類かというのもありますが、divmod のコードを、divmod 2 用にカスタマイズし、必要な空きセルを節約しているからです。
あくまで BCD演算とは、10進数の桁毎にセルに数値を割り当てる方式を指すのであって、その実装は問題ごとに変わってきます。
なお、BCD演算を繰り返しながらpopcountを更新していく様子は、次の図のようになります。
divmod 2 の結果、最高位の桁が 0 になる毎に桁を縮小していき、最終的には終端に達してループが終了します。
まとめ
- 10進数の桁を各セルに割り当てることでBCD演算が行える
- BCD演算は、大きな数を扱う時に、1セルでまとめて管理するよりも速度的に有利
- ただし、桁の繰り上がり・繰り下がり、桁の拡張・縮小の処理が必要になる
- 各桁の処理用に、ある程度間を空けて桁を配置する
- ただし配置をどのようにするかは問題毎に変わる
終わりに
これまで、やや駆け足気味ながらBrainf**kの様々なコードのパターンを紹介してきましたが、いかがでしたでしょうか。Brainf**kを始める方、もっと色々なコードが書きたい方の一助となれば幸いです。また、まだまだ golf という観点からは様々な工夫の余地があろうかと思いますが、それは各読者の楽しみにとっておくことにします。最後まで読んで下さり、有難うございました。もしまたネタがあれば、続きまたは番外編ということで書くかもしれません。
Discussion