一人麻雀和了確率計算アルゴリズム トップダウン vs ボトムアップ
トップダウンとボトムアップの定義を逆にしました。(2022/09/23追記)
実装例を追記しました。(2022/09/23追記)
このブログでは度々一人麻雀の和了確率を計算するアルゴリズムを取り上げてきました。ところでこのブログ以外でも一人麻雀の和了確率や期待値の計算方法を解説しているサイトがあります。
- http://critter.sakura.ne.jp/ (critter 氏)
- https://mahjong.ara.black/ (あら氏)
- https://pystyle.info/mahjong-expected-value-in-mahjong/ (nekobean 氏)
これらのサイト、そしてこのブログは一人麻雀の和了確率や期待値を計算しているはずですが、すべて同じ値を計算できているのでしょうか?言い換えればこれらのサイトで用いられているアルゴリズムはすべて等価なのでしょうか?今回はこれらのアルゴリズムの関係を明らかにしたいと思います。なお先に結論を述べておくとすべて等価です。
先に言及したアルゴリズムに共通するのは手牌変化の木を探索するということです。手牌変化の木とは以下の図のように手牌の変化を木で表現したものです。なお、議論を簡単にするため以降では引いた牌に対してどの牌を捨てるかが事前に決まっているものとします。
それぞれのアルゴリズムで求める値は次のように決定的に異なります。手牌変化の木の根を上としたときにそれらの値を上から下へ求めるか下から上へ求めるかによって、これらのアルゴリズムはトップダウンとボトムアップに分かれます。このブログでこれまで取り上げてきたアルゴリズムはトップダウンに、それ以外はボトムアップになります。
- トップダウン
- 巡目にノードの状態である確率を求める。
簡単な問題を使って2つの違いを見ていきます。
トップダウンの解法
ボトムアップの解法
(3)式と(4)式を見ると違いは2点あり、①時間発展の方向が互いに逆である②行列が互いに共役転置の関係にあることがわかります。計算過程は省略しますがこれらを解くと
の関係が得られます。よってトップダウンとボトムアップのどちらでも同じ和了確率を得られます。
では手牌の変化の行先が複数の場合について考えてみます。例えばの変化の他にの変化がある場合、ボトムアップでは以下の連立確率漸化式を解きます。なおトップダウンについては https://tomohxx.github.io/mahjong-algorithm-book/ を参照してください。
(6)式のように行先の分だけ項が増えます。ここまでに解説したボトムアップのアルゴリズムがおそらくcritter氏のアルゴリズムに相当すると思います。
ここで(2)式に戻ってこれを形式的に解いてみます。
念のため途中式を記載しました。さて(7)式は「巡目の和了確率は、巡目まで有効牌を引かず巡目に有効牌を引く確率に巡目の和了確率をかけたものを]に渡って足したものである」と解釈できます。この(7)式を使うアルゴリズムがあら氏とnekobean氏のアルゴリズムに相当すると思います。
以上の議論から冒頭で言及した3つのサイトとこのブログで用いられたアルゴリズムはすべて等価であることが簡単には(厳密ではない)説明できました。
最後にどのアルゴリズムを使うべきか考えてみます。素朴に実装すれば計算量はどれも同じですが、ボトムアップでは途中のノードの和了確率をキャッシュする(メモ化再帰)ことで訪問するノード数を減らせるので計算速度を追求するならボトムアップを使うべきです。また(2)式と(7)式は等価ですが、(7)式はループカウンタの分変数が増えるので(2)式の方が実装が簡単だと思います。つまりcritter氏のアルゴリズムが一番おすすめです。
ここまで完全に机上の議論しかしてきませんでしたが、ボトムアップを実装したものがこちらです。
手替わりなし・シャンテン戻しなしの条件付きですが、5シャンテンまでなら1秒以下、6シャンテンでは10秒から20秒程度で和了確率を計算することができます。6シャンテンの計算はキャッシュを使うからこそできるのであって、キャッシュの使えないトップダウンでは時間がかかりすぎて事実上計算不可能です。というわけでボトムアップの優位性は明白です。また一般の5シャンテンや6シャンテンの高シャンテン数の手牌の和了確率を計算できるプログラムは他にないと思われます。なお高シャンテン数であっても対称性が高い特殊な手牌では以前から計算可能でした。