目的
ゼナー・カード(Zener cards)は、一般に ESP カードとして知られています。
5枚をシャッフルして一枚引いてその一枚を当てる(確率 1/5)、というのを繰り返すのが一般的な使い方だと思いますが、ここでは、5枚ある ESP カードをシャッフルしたものを並べて、カードの並び順を言い当てようとした場合のことを考えることにします。
5枚のカードのうち 5 枚を言い当てるのはどの程度ありうることなのか、4 枚を言い当てるのはどの程度ありうることなのか、3 枚は、2 枚は、... とそれぞれ計算したいと思いました。
概要
ぱっと考えてわかるのは、
- 5枚全部正解、というのは 1 / 120 になるはず(順列組み合わせパターン数は $5 \times 4 \times 3 \times 2 \times 1$ なので)
- カードに重複はない、という前提で答えるならば「4枚正解」ということはありえないはず
ということくらいです。
5枚以外の正解確率の計算はややこしいので、計算機で確率計算してみることにしました。
カードの順列組み合わせと正答カード数の計算
5種類のカードの順列組み合わせを考えれば「5枚のそれぞれ異なる絵柄のカードをシャッフルして並べたもの」のパターンをすべて列挙することができます。
同様に、「推測したカードの並び」の全パターンも順列組み合わせとして生成できます。
SWI-Prolog の permutation/2 でそれぞれの順列組み合わせを生成できます。
カードは [1,2,3,4,5] と数字で与えてもよいのですが、数字だと正答数 1 の場合、正答数 2 の場合、... で出てくる数字と紛らわしくなりそうなのでアトムを使って [a,b,c,d,e] と与えることにしておきました。
Prolog 以外の言語で実装する場合には [1,2,3,4,5] を使うのがよいでしょう。
% カードのセットを [a,b,c,d,e] の順列組み合わせを生成し、
% 同様に順列組み合わせとして生成した推測カードと一致したカードの枚数をカウントする
カード5枚を当てる(_正答数) :-
permutation([a,b,c,d,e], _実際のカードの並び),
permutation([a,b,c,d,e], _推測したカードの並び),
正答数を数える(_実際のカードの並び, _推測したカードの並び, _正答数).
findall/3 で カード5枚を当てる/1
を繰り返し呼び出せば、カードの並びの全パターン 120 通りそれぞれについての正答カード数を得ることができます。
ここで、実際のカードの並びと、当てる人が答えたカードの並びとが何枚一致しているかを数える述語 正答数を数える/3
は以下のように与えることができます。
% 2つのリストについて、一致する要素をカウントする
正答数を数える([], [], _累計, _正答数) :-
_正答数 = _累計.
正答数を数える([H1|R1], [H2|R2], _累計, _正答数) :-
( H1 = H2 ->
_累計2 is _累計 + 1,
正答数を数える(R1, R2, _累計2, _正答数)
; 正答数を数える(R1, R2, _累計, _正答数)
).
正答数を数える(_実際のカードの並び, _解答として与えたカードの並び, _正答数) :-
正答数を数える(_実際のカードの並び, _解答として与えたカードの並び, 0, _正答数).
正答カード数の出現頻度
- 正答カード数 0 は ○通り、
- 正答カード数 1 は ○通り、
- 正答カード数 2 は ○通り、
- 正答カード数 3 は ○通り、
- 正答カード数 4 は ○通り、
- 正答カード数 5 は ○通り、
という形で答えを得たいので、それぞれにおける正答カード数の頻度を集計する述語を作成しました。
% 正答カード数リストを検索して、与えられた正答カード数がいくつ出現するか数える
出現頻度を数える([], _, _累計, _正答数が一致する頻度) :-
_正答数が一致する頻度 = _累計.
出現頻度を数える([H|R], _正答数, _累計, _正答数が一致する頻度) :-
( H = _正答数 ->
_累計2 is _累計 + 1,
出現頻度を数える(R, _正答数, _累計2, _正答数が一致する頻度)
; 出現頻度を数える(R, _正答数, _累計, _正答数が一致する頻度)
).
出現頻度を数える(_正答数リスト, _正答数, _正答数が一致する頻度) :-
出現頻度を数える(_正答数リスト, _正答数, 0, _正答数が一致する頻度).
結果の表示
結果をフォーマット表示するユーティリティ述語を作りました。
全パターン数 T のうち、正答カード数 C が N 回出現した、というのを
C ... N / T = xx.xxx%
のように表示します。
% 正答数とその出現頻度を表示する
結果を表示する(_正答カード数, _パターン数, _パターン数総計) :-
_確率 is 100.0 * _パターン数 / _パターン数総計,
format('~d ... ~|~` t~d~5+ / ~|~` t~d~5+ = ~|~` t~3f%~7+~n',
[_正答カード数, _パターン数, _パターン数総計, _確率]).
プログラム全体
先頭にシェバング
#!/usr/bin/env swipl
を書いて、
:- initialization(main, main).
を宣言すると、main/1 を自動的に実行する swipl スクリプトにすることができます。
:- initialization(main, main).
main(_) :-
findall(_正答カード数, カード5枚を当てる(_正答カード数), _正答カード数のリスト),
length(_正答カード数のリスト, _全パターン数),
出現頻度を数える(_正答カード数のリスト, 0, _正答カード数0となる頻度),
結果を表示する(0, _正答カード数0となる頻度, _全パターン数),
出現頻度を数える(_正答カード数のリスト, 1, _正答カード数1となる頻度),
結果を表示する(1, _正答カード数1となる頻度, _全パターン数),
出現頻度を数える(_正答カード数のリスト, 2, _正答カード数2となる頻度),
結果を表示する(2, _正答カード数2となる頻度, _全パターン数),
出現頻度を数える(_正答カード数のリスト, 3, _正答カード数3となる頻度),
結果を表示する(3, _正答カード数3となる頻度, _全パターン数),
出現頻度を数える(_正答カード数のリスト, 4, _正答カード数4となる頻度),
結果を表示する(4, _正答カード数4となる頻度, _全パターン数),
出現頻度を数える(_正答カード数のリスト, 5, _正答カード数5となる頻度),
結果を表示する(5, _正答カード数5となる頻度, _全パターン数).
結果
このようにして作成した swipl スクリプトを実行すると、以下のような表示が得られます:
0 ... 5280 / 14400 = 36.667%
1 ... 5400 / 14400 = 37.500%
2 ... 2400 / 14400 = 16.667%
3 ... 1200 / 14400 = 8.333%
4 ... 0 / 14400 = 0.000%
5 ... 120 / 14400 = 0.833%