(翻译)【日麻】向听数快速计算算法
本文机翻至作者@tomohxx的文章
开始
本文就麻将中向听数的计算算法进行解说,该算法的特点是时间复杂度与手牌枚数及向听数无关的快速计算方法。后半部分将对示例程序进行说明,所以想早点尝试该算法的人推荐先看示例程序。另外,由于本文过于注重于严谨性,里面全是数学公式。
背景
在日麻中,手牌的和牌进度用向听数一词来表示,这里将其定义为:向听数 =「一副手牌到听牌时最小的自摸数」
向听数计算需要分为七对子型,国士型,四面一雀头的普通型,3种进行考虑。其中七对子和国士的计算方法很简单,只需要数对子或幺九牌即可。
※在牌种类不足7种的时候,向听数需要加上(7-牌的种类)
※在幺九牌存在对子的场合,需要再 + 1
比较难的四面一雀头的普通型向听计算,一般的计算法是,将手牌分解为面子,对子,搭子,求它们的总数。
※存在雀头的场合,需要 + 1
在多数场合中,手牌有多种分解形式,在这种情况下通过计算所有分解形式的向听数,取其中最小的作为向听数。但是要使用上述式子,就需要去确认存在最终可以和牌的分解形式。例如,一个搭子转为顺子时需要第5枚的待牌,特别是在染手含4枚牌的和牌分解形式确认会变得困难。因此为了回避这个问题,本文尝试用自己的方法来求向听数。
手牌的表示
以下使用h来表示手牌,第i枚牌,使用hi(0 <= i <= 33)来表示。牌的编码可以查看下表。
牌编码 | 牌 |
---|---|
0 | 1万 |
... | ... |
8 | 9万 |
9 | 1筒 |
... | ... |
17 | 9筒 |
18 | 1索 |
... | ... |
26 | 9索 |
27 | 东 |
28 | 南 |
29 | 西 |
30 | 北 |
31 | 白 |
32 | 发 |
33 | 中 |
距离
作为求向听数的准备,在这里引入距离的概念
对手牌h和手牌g,将手牌g转为手牌h所需的换牌自摸数表示为「距离」,下面是公式上的定义
求解向听数的方针
在开头说向听数是 「一副手牌到听牌时最小的自摸数」,换句话说即 「到和牌时最小的自摸数 - 1」。因此,若想正确的求出向听数,可以求目标手牌到所有可能的和牌形状的距离。
置换数和向听数的定义
对于所有和牌形状的集合W,把其每个元素w与手牌h的距离的最小值作为置换数T(h),以下是其公式定义。
向听数S(h)和置换数T(h)可以用
定义。以上即将向听数进行了严谨的定义。这个定义对四面一雀头,七对子,国士无双的和牌形状都适用。
置换数(向听数)的计算方法
若向听数计算使用(5)式直接进行的话,W的元素数量在四面一雀头时为庞大的11498658种(参考),需要大量的时间和内存。所以这里介绍一种高效的算法来计算置换数。从现在开始限定四面一雀头的牌型进行讨论。
为了简化讨论,假设手牌h为2色数牌组成的情况,那么最接近的和牌形状显然也是同样的2色组成。在这时,这个和牌形状的集合W为各个色的n组面子或n组面子及一个雀头的手牌集合的直积。这里将n组的面子的集合表示为Vn、n组的面子和一个雀头的集合表示为Wn,则可表示为
其中X表示集合的直积。这里上标表示第几种的牌色(牌种类)。0为万子,1为筒子,2为索子,3为字牌。例如上标为0时,意味着手牌仅有万子。(5)式可以通过(7)式改写为以下表示。
注意这里(8)式大括号中的各项,比如第一项可以
带入(4)式后进行变形。最后的式子变形结果通过t和u进行表示(译注:这里应该有上下标,看公式图)。则可以将部分置換数提取为以下公式进行定义。
其中9n+8(6)的意思为、n=3即字牌时需要将9n+8改为9n+6来代替的意思。
将(8)式的其他项通过同样的方式改写变为、
即,将所有牌的组合对应的部分置换数t和u进行预先计算(保存在文件中,在计算置换数之前读取到内存)的话,通过(11)式进行计算的结果将会与通过(5)式计算的一致。另外,此时必要的内存大小为
可以说,是十分实际的内存大小。
下面来考虑下(11)式的意义。对含2种类的手牌的和牌形状,各色枚数的组合为(0,14),(3,11),...,(12,2),(2,12),(5,9),...,(14,0)等总计10种情况。对于每种情况,将手牌转换为对应面子(和一个雀头)计算必要的自摸数,求出的10个值中,将最小值作为置换数。(这里看不懂)
求4色的手牌的置换数(向听数)只需要将(11)式重复计算即可。可以使用动态规划法来计算。
此时的置换数为
其中t3(3),....t0(3),u4(3),...,u0(3)无需进行计算。
(13)式和(14)式可以改下为以下形式。
以下源码基于(17)式和(18)式进行实现。
源码
(13)式的c++代码如下,事先用(10)式计算了部分置换数
/*
lhs[5]からlhs[9]にt^{(n)}_0からt^{(n)}_4を, lhs[0]からlhs[4]にu^{(n)}_0からu^{(n)}_4をセットする。
rhs[5]からrhs[9]にt^{n+1}_0からt^{n+1}_4を, lhs[0]からlhs[4]にu^{n+1}_0からu^{n+1}_4をセットする。
lhs[5]からlhs[9]をt^{(n+1)}_0からt^{(n+1)}_4で, lhs[0]からlhs[4]をu^{(n+1)}_0からu^{(n+1)}_4で置き換える。
*/
using Vec = std::vector<int>;
void add(Vec& lhs, const Vec& rhs)
{
//t_4からt_0について計算
for(int j=9; j>=5; --j){
int sht = std::min(lhs[j]+rhs[0], lhs[0]+rhs[j]);
for(int k=5; k<j; ++k){
sht = std::min({sht, lhs[k]+rhs[j-k], lhs[j-k]+rhs[k]});
}
lhs[j] = sht;
}
//u_4からu_0について計算
for(int j=4; j>=0; --j){
int sht = lhs[j]+rhs[0];
for(int k=0; k<j; ++k){
sht = std::min(sht, lhs[k]+rhs[j-k]);
}
lhs[j] = sht;
}
}
示例程序
使用以上的算法,用c++实现了向听数计算和使用蒙特卡洛法计算配牌的向听数的示例程序。详细使用方法请参照链接地址。
shanten-number-calculator - GitHub
- 构建
$ make
- 亲家配牌时的向听数(1亿次模拟)
$ ./sample.out 14 100000000
=========================RESULT=========================
-1 315 0.000315
0 69900 0.0699
1 2333875 2.33387
2 19496567 19.4966
3 43932086 43.9321
4 28515676 28.5157
5 5496180 5.49618
6 155401 0.155401
Number of Tiles 14
Total 100000000
Time (msec.) 99710
Expected Value 3.15599
- 子家配牌时的向听数(1亿次模拟)
./sample.out 13 100000000
=========================RESULT=========================
-1 0 0
0 8196 0.008196
1 621271 0.621271
2 9357183 9.35718
3 36197590 36.1976
4 39876941 39.8769
5 13102859 13.1029
6 835960 0.83596
Number of Tiles 13
Total 100000000
Time (msec.) 90403
Expected Value 3.57966
计算结果是 亲家的配牌向听数的期望值为3.15599、子家的配牌向听数的期望值为3.57966。顺带一提严谨的值是亲家为3.15593,子家为3.57967(到小数点后5位)
结语
接下来我想写关于计算有效牌的算法。
关于计算有效牌的算法请看这里,只有源代码没有解说。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战