高速かつ省スペースな
向聴数計算アルゴリズム
Cryolite
2024/06/15 麻雀アルゴリズム勉強会
ハイライト
0
10
20
30
40
50
60
向聴数計算1回当たりの計算時間 (ナノ秒)
tomo氏の実装 本発表の実装
0
5
10
15
20
25
30
35
40
45
データサイズ(MiB)
tomo氏の実装 本発表の実装
6倍の高速化 1/12以下のデータサイズ
向聴数と置換数
• ある手牌(自摸直後の場合は14枚,11枚,8枚,5枚,2枚のい
ずれか,それ以外の場合は13枚,10枚,7枚,4枚,1枚のいず
れか)の向聴数とは,その手牌を聴牌形へと変形するために必
要な自摸の最小回数
• ある手牌の置換数(※)とは,その手牌を和了形へと変形する
ために必要な自摸の最小回数
• 向聴数 = 置換数 − 1
• 本発表ではもっぱら置換数について議論する
※https://qiita.com/tomohxx/items/75b5f771285e1334c0a5
和了形と置換数
• 和了形は標準和了形(4面子1雀頭形),七対子和了形,国士無
双和了形に大別される
• 七対子和了形および国士無双和了形に対する置換数の計算は比
較的容易なので本発表では省略
• 本発表ではもっぱら標準和了形に対する置換数について議論す
る
Q. そもそも向聴数(置換数)計算は高速化しなく
ても愚直なアルゴリズムで十分速いのでは?
• 麻雀シミュレータ,麻雀 AI 等の実装において向聴数の計算は
頻繁に行われる
• 自摸直後に和了を宣言できるかどうかの判定に必要な場合がある
• 打牌時にリーチを宣言できるかどうかの判定に必要
• 他家の打牌直後に和了を宣言できるかどうかの判定に必要な場合があ
る
• 荒牌平局時に聴牌・不聴を判定するために必要
• その他,麻雀 AI の特徴量として必要な場合もある
• したがって,向聴数(置換数)の高速化は麻雀シミュレータ,
麻雀 AI 等の高速化に直結する
• ∴向聴数(置換数)の高速化は必要
標準和了形に対する置換数計算の既存の
アルゴリズム
• あら氏のアルゴリズム
• https://mahjong.ara.black/etc/shanten/index.htm
• 向聴数 = 8 − 面子の数 ∗ 2 − (面子候補の数) という計算において必
要な要素を各色(萬子,筒子,索子,字牌)毎に事前計算しテーブル
として保持
• tomo氏のアルゴリズム
• https://qiita.com/tomohxx/items/75b5f771285e1334c0a5
• https://github.com/tomohxx/shanten-number
• (cf. https://qiita.com/KamichanR/items/de08c48f92834c0d1f74)
• 部分的な置換数を各色毎に事前計算しテーブルとして保持
各色毎の事前計算によるテーブル化がキーポイント
tomo氏のアルゴリズム
手牌の萬子部分に関して 手牌の筒子部分に関して 手牌の索子部分に関して 手牌の字牌部分に関して
0面子0雀頭に対する置換数
1面子0雀頭に対する置換数
2面子0雀頭に対する置換数
3面子0雀頭に対する置換数
4面子0雀頭に対する置換数
0面子1雀頭に対する置換数
1面子1雀頭に対する置換数
2面子1雀頭に対する置換数
3面子1雀頭に対する置換数
4面子1雀頭に対する置換数
0面子0雀頭に対する置換数
1面子0雀頭に対する置換数
2面子0雀頭に対する置換数
3面子0雀頭に対する置換数
4面子0雀頭に対する置換数
0面子1雀頭に対する置換数
1面子1雀頭に対する置換数
2面子1雀頭に対する置換数
3面子1雀頭に対する置換数
4面子1雀頭に対する置換数
0面子0雀頭に対する置換数
1面子0雀頭に対する置換数
2面子0雀頭に対する置換数
3面子0雀頭に対する置換数
4面子0雀頭に対する置換数
0面子1雀頭に対する置換数
1面子1雀頭に対する置換数
2面子1雀頭に対する置換数
3面子1雀頭に対する置換数
4面子1雀頭に対する置換数
0面子0雀頭に対する置換数
1面子0雀頭に対する置換数
2面子0雀頭に対する置換数
3面子0雀頭に対する置換数
4面子0雀頭に対する置換数
0面子1雀頭に対する置換数
1面子1雀頭に対する置換数
2面子1雀頭に対する置換数
3面子1雀頭に対する置換数
4面子1雀頭に対する置換数
• 各色に関する部分的な置換数を事前にテーブル化
• 上記の空間において最小置換数を動的計画法で探索
部分置換数のテーブル
• 萬子,筒子,索子に関してはまったく同じテーブルとなるため,
数牌のテーブルと字牌のテーブルの2種類のみを持てばよい
• 0から4までの数字が9個(数牌の場合)または7個(字牌の場
合)並んだもので数字の合計が14を超えないものをキーとする
テーブルとなる
• 数牌に対するテーブルのキーの場合の数は405350通り
• 字牌に対するテーブルのキーの場合の数は43130通り
• 以下,数牌の場合のみを考える(字牌の場合も同様)
部分置換数のテーブル
•テーブルのキー: 各色の各牌の数を並べた文字列
例: 🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇🀇 = “320102401”
0から4までの数字が9個並んだ文字列で数字の合
計が14を超えないもの
•テーブルの値: 𝑚𝑚面子ℎ雀頭(0 ≤ 𝑚𝑚 ≤ 4, 0 ≤ ℎ ≤
1)に対する部分置換数(0から9までの数字を10
個並べたもの)
例: 0, 0, 0, 1, 2, 0, 0, 0, 1, 2
1エントリ当たりの値の情報量はlog21010
≈ 33.2
ビットで64ビット整数に詰め込める
部分置換数のテーブル
配列 vs. ハッシュテーブル
• 要素数 59
= 1953125 の配列をテーブルとする方式
• ある色の牌の合計枚数が14を超えないという制約を無視
• tomo氏の実装
• テーブルの参照速度は高速
• 実際には使われない(合計枚数が14を超えるもの)にも要素を割り当
てるため無駄が多い
• 最低でも59 × 8 バイト = 15625000 バイト必要
• ハッシュテーブルを用いる方式
• テーブルの参照速度は配列に劣る
• (ハッシュが衝突する場合)必要な情報量の下限は405350 ×
log2405350 ビット + 405350 × 8 バイト ≈ 10790000 バイト
配列の高速性とハッシュテーブルのコンパクトさを両立できないか?
本発表のアイデア
• テーブルのキー(0から4までの数字が9個並んだ文字列で数字
の合計が14を超えないもの)に辞書順で番号を付与 = テーブル
のキーに対する最小完全ハッシュ関数を実装
• “000000000” ⇒ 0番目
• “000000001” ⇒ 1番目
• “000000002” ⇒ 2番目
• “000000003” ⇒ 3番目
• “000000004” ⇒ 4番目
• ……
• “444110000” ⇒ 435348番目
• “444200000” ⇒ 435349番目
• これが実装できればテーブルの実装サイズを劇的に減らせる
本発表のアイデアの難しいところ
• 数字の合計が14を超えないという制約を無視する場合は簡単
• 例えば ”320102401” は何番目か?= 5進数による自然数の表現
(3201024015 = 133160110)だと思えばよい
• 数字の合計が14を超えないという制約を考慮しつつ,辞書順で
何番目か(ハッシュ)を高速に計算するのが難しい
• 例えば “320102401” は辞書順で何番目?????
例: “320102401” は数字の合計が14を超え
ない集合において辞書順で何番目か?
• Q. “3” というプレフィックスを持つキーは何番目以降になるか?
• A. (“0” というプレフィックスを持つキーの総数) + (“1” というプレフィッ
クスを持つキーの総数) + (“2” というプレフィックスを持つキーの総数)
以降
• Q. “32” というプレフィックスを持つキーは何番目以降になるか?
• A. (“3” というプレフィックスを持つキーの総数) + (“30”というプレ
フィックスを持つキーの総数) + (“31”というプレフィックスを持つキーの
総数) 以降
• ……
• Q. “320102401” というプレフィックスを持つキーは何番目以降になるか,
つまり “320102401” は何番目か?
• A. 計算はできる!
定義: 数牌語と数牌言語
字母系を Ω = 0,1,2,3,4 とする.字母系を9個並べた語であって
それらの合計が14を超えないものを数牌語と呼ぶことにする.
また,数牌語全体からなる集合(言語)を数牌言語と呼び 𝐷𝐷数牌
で表す.
列挙関数と列挙符号
• 定義(列挙関数)
Ω を字母系とする語からなる有限集合を 𝐷𝐷 とする.このとき,
ある 𝑣𝑣 ∈ Ω∗
に関して 𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷 となるような 𝑢𝑢 ∈ Ω∗
に対して 𝐷𝐷
に関する列挙関数を 𝐸𝐸𝐷𝐷 𝑢𝑢 = 𝑢𝑢𝑢𝑢|𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷 と定義する.
• 定理(列挙符号)
全順序を備える字母系 Ω の語からなる有限集合を 𝐷𝐷 とする(𝐷𝐷
には自然な辞書順が定義される).このとき 𝑤𝑤 = 𝑐𝑐0𝑐𝑐1 … 𝑐𝑐𝑙𝑙−1 ∈
𝐷𝐷 (𝑐𝑐𝑖𝑖 ∈ Ω) に対して 𝐷𝐷 の列挙符号を 𝐻𝐻𝐷𝐷 𝑤𝑤 = ∑𝑐𝑐′<𝑐𝑐0
𝐸𝐸𝐷𝐷 𝑐𝑐′
+
∑𝑐𝑐′<𝑐𝑐1
𝐸𝐸𝐷𝐷 𝑐𝑐0𝑐𝑐′
+ ⋯ + ∑𝑐𝑐′<𝑐𝑐𝑙𝑙−1
𝐸𝐸𝐷𝐷 𝑐𝑐0 … 𝑐𝑐𝑙𝑙−2𝑐𝑐′
と定義すると 𝐻𝐻𝐷𝐷
は 𝐷𝐷 に対する最小完全ハッシュ関数となる.
列挙関数と列挙符号
• 定義(列挙関数)
Ω を字母系とする語からなる有限集合を 𝐷𝐷 とする.このとき,
ある 𝑣𝑣 ∈ Ω∗
に関して 𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷 となるような 𝑢𝑢 ∈ Ω∗
に対して 𝐷𝐷
に関する列挙関数を 𝐸𝐸𝐷𝐷 𝑢𝑢 = 𝑢𝑢𝑢𝑢|𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷 と定義する.
• 定理(列挙符号)
全順序を備える字母系 Ω の語からなる有限集合を 𝐷𝐷 とする(𝐷𝐷
には自然な辞書順が定義される).このとき 𝑤𝑤 = 𝑐𝑐0𝑐𝑐1 … 𝑐𝑐𝑙𝑙−1 ∈
𝐷𝐷 (𝑐𝑐𝑖𝑖 ∈ Ω) に対して 𝐷𝐷 の列挙符号を 𝐻𝐻𝐷𝐷 𝑤𝑤 = ∑𝑐𝑐′<𝑐𝑐0
𝐸𝐸𝐷𝐷 𝑐𝑐′
+
∑𝑐𝑐′<𝑐𝑐1
𝐸𝐸𝐷𝐷 𝑐𝑐0𝑐𝑐′
+ ⋯ + ∑𝑐𝑐′<𝑐𝑐𝑙𝑙−1
𝐸𝐸𝐷𝐷 𝑐𝑐0 … 𝑐𝑐𝑙𝑙−2𝑐𝑐′
と定義すると 𝐻𝐻𝐷𝐷
は 𝐷𝐷 に対する最小完全ハッシュ関数となる.
𝑢𝑢 をプレフィックスとする 𝐷𝐷 の語の総数
𝐷𝐷 における 𝑤𝑤 の辞書順での番号
数牌言語に対する最小完全ハッシュ関数
数牌言語 𝐷𝐷数牌 の語 𝑤𝑤 = 𝑐𝑐0𝑐𝑐1 … 𝑐𝑐8 に対する列挙符号
𝐻𝐻𝐷𝐷
数牌
𝑤𝑤 = �
𝑐𝑐′<𝑐𝑐0
𝐸𝐸𝐷𝐷
数牌
𝑐𝑐′
+ ⋯ + �
𝑐𝑐′<𝑐𝑐8
𝐸𝐸𝐷𝐷
数牌
𝑐𝑐0𝑐𝑐1 … 𝑐𝑐7𝑐𝑐′
は最小完全ハッシュ関数(𝑤𝑤 の𝐷𝐷数牌における辞書順での番号).
𝐻𝐻𝐷𝐷
数牌
000000000 = 0
𝐻𝐻𝐷𝐷
数牌
000000001 = 1
…
𝐻𝐻𝐷𝐷
数牌
444110000 = 435348
𝐻𝐻𝐷𝐷
数牌
444200000 = 435349
任意の数牌語 𝑤𝑤 が与えられたときに 𝐻𝐻𝐷𝐷
数牌
𝑤𝑤 を高速に計算できれば良い.
数牌言語のプレフィックスに関する性質
Q. プレフィックス “3201” につなげて数牌語となるサフィックス
の特徴は?
A. プレフィックス “3201” は長さが4,数字の合計が6なので,長
さが5,数字の合計が8を超えないサフィックス全て
Q. プレフィックス “1041” につなげて数牌語となるサフィックス
の特徴は?
A. プレフィックス “1041” は長さが4,数字の合計が6なので,長
さが5,数字の合計が8を超えないサフィックス全て
あるプレフィックスにつなげて数牌語となるサフィックスの集合は,
そのプレフィックスの長さと数字の合計のみから定まる.
数牌言語のプレフィックス定理
• 定義(数牌語のプレフィックス状態)
ある 𝑣𝑣 ∈ Ω∗
に関して 𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷数牌 となるような 𝑢𝑢 ∈ Ω∗
に対し
て,そのプレフィックス状態 𝜎𝜎 を以下で再帰的に定義する:
• 𝜎𝜎 𝜀𝜀 = 0,0
• 𝜎𝜎 𝑢𝑢 = (𝑖𝑖, 𝑛𝑛) とするとき, ある 𝑣𝑣 ∈ Ω∗ に関して 𝑢𝑢𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷数牌 となる
𝑐𝑐 ∈ Ω に対して 𝜎𝜎 𝑢𝑢𝑢𝑢 = (𝑖𝑖 + 1, 𝑛𝑛 + 𝑐𝑐)
• 定理(数牌言語のプレフィックス定理)
ある𝑣𝑣 ∈ Ω∗
, 𝑣𝑣′
∈ Ω∗
に関して 𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷数牌, 𝑢𝑢′
𝑣𝑣′
∈ 𝐷𝐷数牌 となるよ
うな 𝑢𝑢 ∈ Ω∗
, 𝑢𝑢′
∈ Ω∗
に対して, 𝜎𝜎 𝑢𝑢 = 𝜎𝜎 𝑢𝑢𝑢 ならば
𝐸𝐸𝐷𝐷
数牌
𝑢𝑢 = 𝐸𝐸𝐷𝐷
数牌
𝑢𝑢𝑢 である.
数牌言語のプレフィックス定理
• 定義(数牌語のプレフィックス状態)
ある 𝑣𝑣 ∈ Ω∗
に関して 𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷数牌 となるような 𝑢𝑢 ∈ Ω∗
に対し
て,そのプレフィックス状態 𝜎𝜎 を以下で再帰的に定義する:
• 𝜎𝜎 𝜀𝜀 = 0,0
• 𝜎𝜎 𝑢𝑢 = (𝑖𝑖, 𝑛𝑛) とするとき, ある 𝑣𝑣 ∈ Ω∗ に関して 𝑢𝑢𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷数牌 となる
𝑐𝑐 ∈ Ω に対して 𝜎𝜎 𝑢𝑢𝑢𝑢 = (𝑖𝑖 + 1, 𝑛𝑛 + 𝑐𝑐)
• 定理(数牌言語のプレフィックス定理)
ある𝑣𝑣 ∈ Ω∗
, 𝑣𝑣′
∈ Ω∗
に関して 𝑢𝑢𝑢𝑢 ∈ 𝐷𝐷数牌, 𝑢𝑢′
𝑣𝑣′
∈ 𝐷𝐷数牌 となるよ
うな 𝑢𝑢 ∈ Ω∗
, 𝑢𝑢′
∈ Ω∗
に対して, 𝜎𝜎 𝑢𝑢 = 𝜎𝜎 𝑢𝑢𝑢 ならば
𝐸𝐸𝐷𝐷
数牌
𝑢𝑢 = 𝐸𝐸𝐷𝐷
数牌
𝑢𝑢𝑢 である.
𝜎𝜎 𝑢𝑢 = 𝑢𝑢の長さ, 𝑢𝑢の数字の合計
あるプレフィックスを持つ数牌語の総数は,そ
のプレフィックスの長さと数字の合計で決まる
数牌言語に対する最小完全ハッシュ関数
の高速計算
𝐻𝐻𝐷𝐷
数牌
𝑤𝑤 = �
𝑐𝑐′<𝑐𝑐0
𝐸𝐸𝐷𝐷 𝑐𝑐′
+ ⋯ + �
𝑐𝑐′<𝑐𝑐8
𝐸𝐸𝐷𝐷 𝑐𝑐0𝑐𝑐1 … 𝑐𝑐7𝑐𝑐′
= �
𝑐𝑐′<𝑐𝑐0
𝑇𝑇′
1, 𝑐𝑐𝑐 + ⋯ + �
𝑐𝑐′<𝑐𝑐8
𝑇𝑇′
9, 𝑐𝑐0 + 𝑐𝑐1 + ⋯ + 𝑐𝑐7 + 𝑐𝑐′
= 𝑇𝑇 1,0, 𝑐𝑐0 + ⋯ + 𝑇𝑇 9, 𝑐𝑐0 + 𝑐𝑐1 + ⋯ 𝑐𝑐7, 𝑐𝑐8
𝑇𝑇′ 𝑖𝑖, 𝑛𝑛 = プレフィックス状態が 𝑖𝑖, 𝑛𝑛 であるプレフィックスの列挙関数
𝑇𝑇 𝑖𝑖, 𝑛𝑛, 𝑐𝑐 = �
𝑐𝑐′<𝑐𝑐
𝑇𝑇′ 𝑖𝑖, 𝑛𝑛 + 𝑐𝑐𝑐
𝑇𝑇 のサイズは 9 × 15 × 5 = 675 なので事前に計算しておけばよい
任意の数牌語の最小完全ハッシュ関数は
事前計算した9個の値の和だけで計算できてしまう!
数牌言語に対する最小完全ハッシュ関数
を用いた部分置換数のテーブル実装
“000000000”
“000000001”
“000000002”
“000000003”
“000000004”
…
“444110000”
“444200000”
0要素目
1要素目
2要素目
3要素目
4要素目
…
435348要素目
435349要素目
キー 値の配列
必要なサイズ: 𝟒𝟒𝟒𝟒𝟒𝟒𝟒𝟒𝟒𝟒𝟒𝟒 × 𝟖𝟖 バイト = 𝟑𝟑𝟑𝟑𝟑𝟑𝟑𝟑𝟑𝟑𝟑𝟑𝟑𝟑 バイト
𝐻𝐻𝐷𝐷
数牌
実験
• tomo氏の実装 https://github.com/tomohxx/shanten-number
をベースラインとして速度比較
• 動的計画法の部分はtomo氏の実装とまったく同一
• 1, 2, 4, 5, 7, 8, 10, 11, 13, 14枚のいずれかの枚数の手牌をラン
ダムに生成してその向聴数を計算
• 1億回の向聴数計算を行い1回の計算当たりの平均計算時間を算
出
• GCC 11.4.0, -O3
• Intel Core i9-12900
実験
0
10
20
30
40
50
60
向聴数計算1回当たりの計算時間 (ナノ秒)
tomo氏の実装 本発表の実装
6倍の高速化
実験結果に対する考察
• 速度が大幅に向上しているが,本来なら動的計画法の部分が律
速になるはずで,その部分が同一実装なのにこれだけの速度向
上は予想外
• テーブルサイズが大幅に小さくなったことによって参照局所性
が向上して結果的に大幅な速度向上につながったか?
• プロファイラを用いたより詳細な検証が必要
まとめ
• 向聴数計算における大幅な高速化と省スペース化に成功
• 実装: https://github.com/Cryolite/nyanten
• 各色の牌姿をキーとするあらゆるテーブル実装に応用可能(ex.
有効牌のテーブル化)
• 列挙符号を用いた同様のアイデアを麻雀の点数計算の超高速化
に適用したものを開発中
https://github.com/Cryolite/tsumonya なので乞うご期待!

A Fast and Space-Efficient Algorithm for Calculating Deficient Numbers (a.k.a. Shanten Numbers).pdf