乱数とは
乱数とは、でたらめに現れる数のことです。コンピュータにおける乱数
何かを決めるとき、自分では無い他の誰かに決めてほしいことが多々あります。
トランプをかき混ぜる時は誰かの意思が働かないほうが望ましいし、じゃんけんでは相手の手が分かっているとつまらなくなります。
ギャンブルにおいては、予め答えが分かっていたら勝負にならない上に、場合によっては罪に問われることもあります。
また、煙や水の動きなど、多すぎる不確定な数値をすべて人間の手によって決定するのは現実的ではありません。
そんな時に便利なのが「乱数」です。
自分の意志とは関係なく、でたらめに現れる数が答えを決めてくれます。
コンピュータにおける乱数は、実は乱数ではありません。環境が変わっても同じ乱数列を
正しくは「疑似乱数」と呼び、一見して乱数に見える数字の羅列を計算で生成して、本物の乱数の代用にしています。
このため、アルゴリズムと初期値さえ同じであれば、同じ結果を得ることができます。
本物の乱数は物理法則に従うので、同じ結果を得ることはできません。
よく、「空中に放り投げたコインが着地した面の向き」や「サイコロを振った時の出目」として使われます。
コインやサイコロを投げた時に全く同じ環境を再現することが物理的にできないことは理解できるでしょう。
最近のプログラミング言語には、ほぼすべてに高機能な乱数生成器が内蔵されており、内蔵でなくても付録として付いていたりします。疑似乱数のアルゴリズム
また、Linux の様に高機能な OS には乱数生成器が内蔵されており、気軽に利用できるようになっています。
アプリケーションを作るときはプログラムからこれらを使用すれば良いので、通常は乱数生成器そのものを作成する必要はありません。
十進 BASIC では「N = INT(RND * 10)」の記述で N に 0~9 の乱数を求めることができます。
便利な機能ですが、欠点もあります。
プログラミング言語や OS に内蔵されている乱数発生器は、それぞれ独自のアルゴリズムによって実装されているため、環境が異なると同じ乱数列を発生させることができません。
また、アルゴリズムが同一でも実数を返す形式の乱数発生器だと、小数の丸め誤差により値が同一になりません。
具体的には、乱数をキーにした暗号器で暗号化した文書を他の言語で復号化できなかったり、
シミュレーションの結果が、異なる OS で同じ再現にならないことになります。
ここでは乱数発生器を作成し、異なる環境でも同じ乱数列を生成させることにより、同じ実行結果を得られるようにします。
ここでは、Xorshift という手法の中から 128bit 版の xorshift128() を実装します。
これは周期が 296-1 になる (らしい*1) のです。
*1すみません、ちゃんと証明していません。
疑似乱数の周期とは、生成した数字と同じパターンが次に現れるまでの回数のことを指します。
例えば、
1, 3, 2 , 1, 3, 2 , 1, 3, 2 , 1, 3, 2 , ···
であれば 3、
2, 13, 3, 11, 7 , 2, 13, 3, 11, 7 , 2, 13, 3, 11, 7 , ···
なら 5 です。
計算で疑似的に乱数を生成するので、どうしても周期が現れてしまいます。
296-1 は想像しにくいので 10 進数に直すと、
95 ≦ log2X < 96となり、10 進数に直しても想像しにくいほど長い周期をもつということが分かりました。(1028 は 1 穣)
95 ≦ log10X / log102 < 96
95 ≦ log10X / 0.3010 < 96
28.595 ≦ log10X < 28.896
296-1 = 1028.595~28.895-1
(1 秒間に 100 億回の乱数を生成すると仮定して約 2,500 億年必要。えーと、ビッグバン説が正しいとして、宇宙の誕生って 138 億年前だっけ)
xorshift128 には制約があって、内部的に符号なし 32bit の整数演算が出来なければなりません。
十進 BASIC は符号なし 32bit の整数演算を直接することはできませんが 15 桁までの実数が扱えるので、
これを使って符号なし 32bit の整数演算を実現します。(232+1 は 1010 に収まる)
余談: VBA の場合、整数は符号あり 32bit の演算しかできませんが、複数の Byte 型変数を連結することにより、符号なし 32bit の実装が可能です。(四則演算は面倒だけど、論理演算とシフトだけなら結構カンタン)
アルゴリズムは xorshift128() の、そのまんまパクリです。
(符号なし 32bit 演算が前提なので、直接に負数や実数を扱うことは出来ません)
*2例えば 0~9 の値を求めるには「MOD(n, 10)」と記述する。
DIM xorshift_state(4) DATA 123456789, 0, 0, 0 ! すべてが 0 にならないように初期化する。 MAT READ xorshift_state DECLARE EXTERNAL SUB Xorshift128 FOR i = 1 TO 100 ! 1029 個も確認してられないので、とりあえず 100 個。 CALL Xorshift128(xorshift_state) LET n = xorshift_state(1) PRINT n ! 乱数を表示する。(実際に使うときは MOD(n, x) であまりを求める*2) NEXT i END
! 乱数発生器 EXTERNAL SUB Xorshift128(state()) LET t = MOD(state(4), 2^32) ! fool proof*3 LET s = MOD(state(1), 2^32) ! fool proof*3 LET state(4) = state(3) LET state(3) = state(2) LET state(2) = s LET t = MOD(bitXOR(t, t * 2^11), 2^32) ! t ^= t << 11 LET t = bitXOR(t, INT(t / 2^8)) ! t ^= t >> 8 LET state(1) = bitXOR(bitXOR(t, s), INT(s / 2^19)) ! t ^ s ^ (s >> 19) END SUB
*3この 2 箇所の MOD(n, 2^32) は、人間が初期値として n < 232 を暗算するのが苦手のため記述している。本来は不要。
実行結果
このプログラムと Python 版の実行結果が同一になることを確認してください。
123457022 123456789 123457022 3736181605 123505008 3736526827 123457022 1432556739 3063349270 3736524968 123456189 1255343001 1145711973 743558876 4119259081 664534315 850386360 2418725407 1381029756 2866416312 3872669766 547933353 4103542007 2388807447 3404789997 2891688094 3760660814 2090979498 836466755 1085618622 2571193783 3944217269 31760116 3899994047 2076896243 794588269 174742604 1260618345 1738173837 2880012815 4084802314 2753480646 274473165 4144128307 3445190058 2670437168 1853923920 2322554186 2354922896 1319978248 626326703 3575738434 3006717619 2543274200 363097857 3420040855 3458150937 3823334026 3574191504 3550639280 3826916135 479547916 2276172123 1198277354 1939581443 3307770702 525554633 973945307 2489861931 358286347 2417217650 3280199754 352630286 3635581945 3593230557 175207394 989134948 1989439896 3268027687 1134932223 3519114959 67952094 2541495141 4180695869 593356226 1105014362 936135183 1281234552 2162268612 686217340 2100261725 3246278486 1306380548 1407639035 1431697729 1713520714 3306434659 2692228159 1511874176 776562073
おまけ
乱数を使いやすくするために、公差が 1 の等差数列をかき混ぜる副プログラム DustShuffle を掲載します。(Dust: Richard Durstenfeld)
ダステンフェルドのシャッフルは、本来は要素を限定しないのですが、この副プログラムでは等差数列としています。
実行結果の数列を添え字として使用することで、本来の目的を果たせるように作ってあります。
DustShuffle の中から呼び出している副プログラム Xorshift128 は上記と同じものです。
本当は引数 siz を省略したいところだけど、十進 BASIC は配列の長さを可変にできないためこうしています。
DIM xorshift_state(4) DATA 123456789, 0, 0, 0 ! すべてが 0 にならないように初期化する。 MAT READ xorshift_state DECLARE EXTERNAL SUB Xorshift128 DECLARE EXTERNAL SUB DustShuffle DIM idx(8) CALL DustShuffle(idx, SIZE(idx), xorshift_state) FOR i = 1 TO 8 PRINT idx(i); NEXT i PRINT END
! 乱数発生器 EXTERNAL SUB Xorshift128(state()) LET t = MOD(state(4), 2^32) ! fool proof LET s = MOD(state(1), 2^32) ! fool proof LET state(4) = state(3) LET state(3) = state(2) LET state(2) = s LET t = MOD(bitXOR(t, t * 2^11), 2^32) ! t ^= t << 11 LET t = bitXOR(t, INT(t / 2^8)) ! t ^= t >> 8 LET state(1) = bitXOR(bitXOR(t, s), INT(s / 2^19)) ! t ^ s ^ (s >> 19) END SUB
! ダステンフェルドのシャッフル EXTERNAL SUB DustShuffle(arr(), siz, state()) FOR i = 1 TO siz LET arr(i) = i NEXT i FOR i = siz TO 1 STEP -1 CALL Xorshift128(state) LET p = MOD(state(1), i) + 1 LET n = arr(p) LET arr(p) = arr(i) LET arr(i) = n NEXT i END SUB
実行結果
6 4 8 5 1 3 2 7