概要
麻雀においてシャンテン数は手牌の牌を聴牌までに何枚を入れ替えればよいかを表す数値であり、数値が小さいほど聴牌に近い手牌となり、良い手牌であるとされる。今回はJavaのプログラムを用いてゲームで使用する牌のセットを変え、親の$14$枚配牌時のシャンテン数を$100$万回分計算することで、配牌時の平均シャンテン数の分布を計算した。コードを見るとJavaである必要はなく、Cのプログラムで計算しても良かったように思われる。
本記事では麻雀のルールを理解していることを前提としているため、基本的な麻雀のルールは各種サイトを参照のこと。
なお、本記事において麻雀牌の画像は下記サイトよりお借りした。
https://majandofu.com/mahjong-images
使った牌のセット
①標準ルール
数牌は萬子・筒子・索子の各$1~9$までの各$9$種、字牌$7$種類の$34$種を各$4$枚ずつ、合計$136$枚を使う。
②$3$人麻雀
数牌は筒子・索子の各$1~9$までの各$9$種、萬子は$1$と$9$の$2$種、字牌$7$種類の$27$種を各$4$枚、合計$108$枚を使う。
③数牌のみ
萬子・筒子・索子の各$1~9$までの各$9$種を$4$枚ずつ、合計$108$枚を使う。
数牌のみを使うルールで、関西を中心に行われているという。
④字牌+$1$種類の数牌
字牌$7$種と$1$種類の数牌$9$種を$4$枚ずつ、合計$64$枚を使う。
シャンテン数の計算
今回は前章で述べた牌のセットから$14$枚の配牌を取得し、そのシャンテン数を計算することを$100$万回ずつ行い、その分布を求めた。
計算手法
参考サイトにて紹介されている数式を用いて計算を行った。
①面子手・七対子・国士無双の各種和了形のシャンテン数を求める
②面子手・七対子・国士無双のシャンテン数をそれぞれ求め、最小のものを手牌のシャンテン数とする。
七対子のシャンテン数
$ (シャンテン数) = 13-(2枚以上ある牌の種類数)×2-(1枚しかない牌の種類数) $
ただし、$(2枚以上ある牌の種類数)+(1枚しかない牌の種類数) \le 7 $に制限する。
国士無双のシャンテン数
$ (シャンテン数)=13-(1・9・字牌の種類数)-(1・9・字牌の対子があれば\ 1) $
七対子や国士無双のシャンテン数は単純に対子・孤立牌や$19$字牌の数を数えるだけであり、面子手に比べて難しくない。ただし、数牌のみ、字牌と$1$種類の数牌を使うルールでは国士無双になることがない。
面子手のシャンテン数
面子手のシャンテン数は下記の公式で求めることができる。ただし、例外が多くあるため、「補正」を考えなくてはならない。補正を考えてもなお正しいシャンテン数を計算できない場合がある。
$ (シャンテン数)=8-(面子候補の数)-(面子数)×2+(補正) $
ただし、「補正」は下記のいずれかを満たす場合1とする。
・$(面子候補の数)+(面子数)>5$の場合
・$(面子候補の数)+(面子数)=5$かつ雀頭がない場合
ここで、面子は「刻子または順子」を指し、面子候補とは「両面・カンチャン・ペンチャンの各ターツ、対子」を指すとする。また、「面子および面子候補」をブロックという。
手牌のブロック分けのなかで、上記の式を計算し、その最小値を面子手のシャンテン数とする。
面子手の補正が必要な理由
麻雀の面子手で和了形は$4$面子$1$雀頭(対子)、すなわち$5$ブロックかつ雀頭が必須という形になる。そのため、$6$ブロック以上の場合と、$5$ブロックかつ雀頭がない場合に上記の式で求めたシャンテン数と実際のシャンテン数に誤差が生じる。
例えば、
さらに$5$ブロックであっても
のような手牌では面子候補が$2$個、面子が$3$個であるから、公式に当てはめると$0$シャンテン、つまり聴牌であるが、実際は$1$シャンテンである。 $6$ブロック以上や$5$ブロック雀頭なしの場合、和了までに入れ替えなければならないブロックが発生するため、シャンテン数を下げることに寄与しないブロックが発生するためにシャンテン数が$1$増えてしまうのである。 この例外は雀頭がある場合は雀頭を含めてブロックを$5$個までに制限すること、雀頭がない場合はブロックを$4$個までに制限することで回避している。上記公式の問題点
和了に5枚目の牌が必要なテンパイ形を排除できない
例えば、
という$14$枚の組み合わせをテンパイであると判定してしまう。麻雀では自分で$4$枚使っている牌の$5$枚目でしかシャンテン数が進まない場合、形の上でのシャンテン数に$1$を足さなければならない。 上記の配牌は実際には$1$シャンテンである。ただし、この不具合は$4$枚の牌が$2$組配牌に含まれているときに起きる可能性がある。配牌で$4$枚使いが$2$組できる確率は低いので、結果に与える影響は軽微であるとして無視することにした。実際、標準ルールや$3$人麻雀の数値が先行事例での全探索による計算結果に近い値となったため、無視してよかったようである。
計算方法
手牌は長さ$34$の配列で表す。配列の各要素にはインデックスに対応する牌を使っている枚数を格納しておく。
| インデックス | 対応する牌 |
|---|---|
| 0 | ![]() |
| 1 | ![]() |
| 2 | ![]() |
| 3 | ![]() |
| 4 | ![]() |
| 5 | ![]() |
| 6 | ![]() |
| 7 | ![]() |
| 8 | ![]() |
| 9 | ![]() |
| 10 | ![]() |
| 11 | ![]() |
| 12 | ![]() |
| 13 | ![]() |
| 14 | ![]() |
| 15 | ![]() |
| 16 | ![]() |
| 17 | ![]() |
| 18 | ![]() |
| 19 | ![]() |
| 20 | ![]() |
| 21 | ![]() |
| 22 | ![]() |
| 23 | ![]() |
| 24 | ![]() |
| 25 | ![]() |
| 26 | ![]() |
| 27 | ![]() |
| 28 | ![]() |
| 29 | ![]() |
| 30 | ![]() |
| 31 | ![]() |
| 32 | ![]() |
| 33 | ![]() |
public static void main(String[] args){
int[] tehai = new int[34];
}
計算コード
面子手に関する部分のみとし、七対子や国士無双への対応は省略する。
private static int shantenKeisan(int[] tehai) {
//第6引数は8より大きい数値なら何でもOK
int mentsuShanten = blockBunkai(tehai, 0, 0, 0, 0, 10);
return mentsuShanten;
}
/*
blockBunkai(int[] tehai, int head, int taatsu, int mentsu, int start, int shanten)
int[] tehai:手牌を表す長さ34の配列
int head:トイツ数
int mentsu:面子数
int start:ブロックを取り出し始める位置 *再帰回数削減のため
int shanten:これまでに見つかった最小シャンテン数
*/
private static int blockBunkai(int[] tehai, int head, int taatsu, int mentsu, int start, int shanten) {
if(tehai.length != 34) {return -100;}
int tempShanten = shanten(head, taatsu, mentsu);
for(int i = start; i < tehai.length; i ++) {
/*
刻子や対子では同じ牌が再度、刻子や対子になることを考えないので、
再帰の際に第5引数をi+1で呼び出す。特に4枚使いを2対子にしないためにこの対策が必要。
順子やターツを見つけた場合では第5引数をiのままで呼び出すため、
222234のように刻子(対子)かつ順子(ターツ)の一部になっている場合も漏らさずに網羅できている。
*/
//刻子を抜き取る
if(tehai[i] >= 3) {
tehai[i] -= 3;
tempShanten = Math.min(shanten,blockBunkai(tehai, head, taatsu, mentsu + 1, i + 1, tempShanten));
tehai[i] += 3;
}
//対子を抜き取る
if(tehai[i] >= 2) {
tehai[i] -= 2;
tempShanten = Math.min(shanten,blockBunkai(tehai, head + 1, taatsu, mentsu, i + 1, tempShanten));
tehai[i] += 2;
}
//字牌は刻子と対子だけ考え、順子とターツは考えない
if(i > 26) {continue;}
/*順子やターツは同じものが再度、出る可能性があるため、第5引数はiで呼び出す*/
//順子(順子は7以下の牌から始まるので数が7以下になるように調整する)
if((i%9 + 1)<=7 && tehai[i] >= 1 && tehai[i+1] >= 1 && tehai[i+2] >= 1) {
tehai[i] -= 1;tehai[i+1] -= 1;tehai[i+2] -= 1;
tempShanten = Math.min(shanten,blockBunkai(tehai, head, taatsu, mentsu + 1, i, tempShanten));
tehai[i] += 1;tehai[i+1] += 1;tehai[i+2] += 1;
}
//ターツをとる
//両面・ペンチャンターツ
if((i%9 + 1)<=8 && tehai[i] >= 1 && tehai[i+1] >= 1) {
tehai[i] -= 1;tehai[i+1] -= 1;
tempShanten = Math.min(shanten,blockBunkai(tehai, head, taatsu + 1, mentsu, i, tempShanten));
tehai[i] += 1;tehai[i+1] += 1;
}
//カンチャンターツ
if((i%9 + 1)<=7 && tehai[i] >= 1 && tehai[i+2] >= 1) {
tehai[i] -= 1;tehai[i+2] -= 1;
tempShanten = Math.min(shanten,blockBunkai(tehai, head, taatsu + 1, mentsu, i, tempShanten));
tehai[i] += 1;tehai[i+2] += 1;
}
}
return Math.min(shanten, tempShanten);
}
private static int shanten(int head, int taatsu, int mentsu) {
int block = head + taatsu + mentsu;
//6ブロック以上の場合
if (block >= 6) {
block = 5;
}
//5ブロック以上雀頭なし
if (head == 0 && block >= 5) {
block --;
}
return 8 - block - mentsu;
}
結果
詳しい数値は下記の表のようになった。
| シャンテン数 | 標準 | 3人打ち | 数牌のみ | 字牌+1種類 |
|---|---|---|---|---|
| 和了 | 3 | 9 | 55 | 223 |
| 聴牌 | 676 | 1619 | 8253 | 19556 |
| 1向聴 | 23374 | 40610 | 151698 | 231468 |
| 2向聴 | 196681 | 251450 | 538057 | 476170 |
| 3向聴 | 438232 | 438546 | 290218 | 256485 |
| 4向聴 | 284390 | 239784 | 11719 | 33550 |
| 5向聴 | 55063 | 27884 | 0 | 548 |
| 6向聴 | 1581 | 98 | 0 | 0 |
| 7向聴 | 0 | 0 | 0 | 0 |
| 平均向聴 | 3.15379 | 2.958283 | 2.145287 | 2.07198 |
考察
ネット上で全探索を行って計算した先人は標準ルールにおける親の配牌シャンテン数は平均$3.15594$と結果を出している。今回のシュミレーションでは$3.15379$という結果が出ているため、妥当であるといえる。
その他のルールでの平均シャンテン数の先行研究は見つからなかったが、今回のシュミレーション結果では配牌時のシャンテン数平均は$3$人麻雀で$2.95$、数牌のみの麻雀で$2.14$、混一色麻雀で$2.07$であった。
今後の発展
計算アルゴリズムの改良
シャンテン数を正しく計算できない手牌への対応
前述のとおり、今回の計算アルゴリズムではシャンテン数を正しく計算できない手牌が存在する。出現頻度が低いため、今回は無視した。
全探索
標準ルールにおいて$14$枚の配牌は$3265$億$2050$万$4500$通りある。自分の環境では$100$万回のシュミレーションを約$4$秒で行った。ただ、$100$万回であっても全体の約$32$万$6520$分の$1$に過ぎないため、単純計算であるが全探索にはおよそ$130$万秒以上(半月位)時間がかかることになる。この先全探索を行うに当たっては計算速度向上が必須である。
参考
シャンテン数計算アルゴリズムについての先行研究
シャンテン数を全探索して計算した事例
下記のサイトに様々な確率を計算した結果が掲載されていたが、すでに閉鎖されている。
2026年4月現在、上記サイトはアーカイブサイトで閲覧できる。
参考書籍


































