乱数とは
乱数とは、でたらめに現れる数のことです。コンピュータにおける乱数
何かを決めるとき、自分では無い他の誰かに決めてほしいことが多々あります。
トランプをかき混ぜる時は誰かの意思が働かないほうが望ましいし、じゃんけんでは相手の手が分かっているとつまらなくなります。
ギャンブルにおいては、予め答えが分かっていたら勝負にならない上に、場合によっては罪に問われることもあります。
また、煙や水の動きなど、多すぎる不確定な数値をすべて人間の手によって決定するのは現実的ではありません。
そんな時に便利なのが「乱数」です。
自分の意志とは関係なく、でたらめに現れる数が答えを決めてくれます。
コンピュータにおける乱数は、実は乱数ではありません。環境が変わっても同じ乱数列を
正しくは「疑似乱数」と呼び、一見して乱数に見える数字の羅列を計算で生成して、本物の乱数の代用にしています。
このため、アルゴリズムと初期値さえ同じであれば、同じ結果を得ることができます。
本物の乱数は物理法則に従うので、同じ結果を得ることはできません。
よく、「空中に放り投げたコインが着地した面の向き」や「サイコロを振った時の出目」として使われます。
コインやサイコロを投げた時に全く同じ環境を再現することが物理的にできないことは理解できるでしょう。
最近のプログラミング言語には、ほぼすべてに高機能な乱数生成器が内蔵されており、内蔵でなくても付録として付いていたりします。疑似乱数のアルゴリズム
また、Linux の様に高機能な OS には乱数生成器が内蔵されており、気軽に利用できるようになっています。
アプリケーションを作るときはプログラムからこれらを使用すれば良いので、通常は乱数生成器そのものを作成する必要はありません。
Java では「int n = (new Random()).nextInt(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 の整数演算が出来なければなりません。
Java は符号なし 32bit の整数演算を直接することはできませんが long の下位 32bit を使用すれば実現できます。
アルゴリズムは xorshift128() の、そのまんまパクリです。
(符号なし 32bit 演算が前提なので、直接に負数や実数を扱うことは出来ません)
*2この 2 箇所の n & 0xFFFFFFFFL は、人間が初期値として n < 232 を暗算するのが苦手のため記述している。本来は不要。
public class Xorshift { // 乱数発生器 static long[] state = new long[]{123456789,0,0,0}; // すべてが 0 にならないように初期化する。 public static long Xorshift128() { long t = state[3] & 0xFFFFFFFFL; // fool proof*2 long s = state[0] & 0xFFFFFFFFL; // fool proof*2 state[3] = state[2]; state[2] = state[1]; state[1] = s; t = (t ^ t << 11) & 0xFFFFFFFFL; // t ^= t << 11 // unsigned 32bit t ^= t >> 8; state[0] = t ^ s ^ (s >> 19); return state[0]; }
public static void main(String[] args) { for(int i = 0 ; i < 100 ; i++) { // 1029 個も確認してられないので、とりあえず 100 個。 long n = Xorshift128(); System.out.printf("%d\n", n); // 乱数を表示する。(実際に使うときは % であまりを求める*3) } } }
*3例えば 0~9 の値を求めるには「n % 10」と記述する。
実行結果
このプログラムと Python 版、十進 BASIC 版、Swift 版の実行結果が同一になることを確認してください。
おまけ
乱数を使いやすくするために、公差が 1 の等差数列をかき混ぜる関数 DustShuffle を掲載します。(Dust: Richard Durstenfeld)
ダステンフェルドのシャッフルは、本来は要素を限定しないのですが、この関数では等差数列としています。
実行結果の数列を添え字として使用することで、本来の目的を果たせるように作ってあります。
DustShuffle の中から呼び出している関数 Xorshift128 は上記と同じものです。
public class Xorshift { // 乱数発生器 static long[] state = new long[]{123456789,0,0,0}; // すべてが 0 にならないように初期化する。 public static long Xorshift128() { long t = state[3] & 0xFFFFFFFFL; // fool proof long s = state[0] & 0xFFFFFFFFL; // fool proof state[3] = state[2]; state[2] = state[1]; state[1] = s; t = (t ^ t << 11) & 0xFFFFFFFFL; // t ^= t << 11 // unsigned 32bit t ^= t >> 8; state[0] = t ^ s ^ (s >> 19); return state[0]; }
// ダステンフェルドのシャッフル public static long[] DustShuffle(int length) { long[] idx = new long[length]; for(int i = 0 ; i < length ; i ++) idx[i] = i; for(int i = length - 1 ; i >= 0 ; i --) { long p = Xorshift128() % (i + 1); long n = idx[(int) p]; idx[(int)p] = idx[i]; idx[i] = n; } return idx; }
public static void main(String[] args) { long[] idx = DustShuffle(8); System.out.println(Arrays.toString(idx)); } }
実行結果