麻将胡牌判定方法(查表法,翻译自http://hp.vector.co.jp/authors/VA046927/mjscore/mjalgorism.html)
麻将高速判定胡牌方法说明
麻将胡牌判定通常使用的方法是回溯法,但是由于回溯法需要暴力检查麻将面值组合,所以需要的处理时间比较长。在执行一次的情况下,处理时间不会成为问题,但是在需要进行番数判定等,进行重复处理的情况下,对于算法的时间复杂度来说就比较够呛。本文中,引诉了日本一种索引法,进行高速判断。
使用索引法
索引法是先将所有的胡牌可能性进行列举,然后将其转换成一个map表,key值表示的是这个胡牌的方式,value表示的是胡牌的构成(比如 顺子情况、刻子情况等。)。所以要想判定一副手牌是否胡牌,只需将手牌转换成key值,然后在map表中进行查询。
如果要给每一种牌编一个id,因为麻将中共有34种牌,每种牌至少需要6位空间。手牌最大14张也就是需要84位。所以要保存的牌的全部组合约有1700万中,大概需要175MB的存储空间。
改进的思路是先给手牌进行排序,然后不用管具体的牌面值,只需计算连续牌的张数,得到一个【牌型】,再从表中查找牌型是否胡牌。
比如:222456万345678筒北北,可以编码为30111011111102(一个刻子,顺子,顺子,顺子,对子(将牌))如果两个属性(刻子 顺子 对子 等)间不连续(如222 和 456 不连续,222和345是连续的)使用0隔开:
"456"->"111"
"345"->"111"
"222"->"3"
"北北"->"2"
"234456"->"11211"
[123567万 123567筒 西西] -> [11101110111011102]
[111234678万 东东东西西]->[311101110302]
[11122223333444万]->[3443]
在牌型已经数值化好的情况下,要确定是否已胡,需要做比较数值处理。
因为一般的电脑都是32bit以上的计算机,所以在比较数值的时候,如果数值在32bit以内,则很容易处理。如果手牌按着上述规则进行数值化,则最坏的数值如下:
[1 3 5 7 9 万 1 3 5 7 9 筒 东 南 西 北] ->[101010101010101010101010101](27位)
因为一张牌个数最多有4张,所以可以用3bit来表示1位的话,需要3bit×27位 = 81bit,这样下去作为索引将会很难处理(数值太大)。所以我们可以利用0不会持续2个以上,将0设为前一个数字的集合,根据以下规则对比特串进行编码:
「1」→ 「0」
「2」→ 「110」
「3」→ 「11110」
「4」→ 「1111110」
「10」→「10」
「20」→「1110」
「30」→「111110」
「40」→「11111110」
如果数字串按照上述编码规则转换为位串,则不管下一个数值是否为“0”,都可以在以下规则中对位串进行编码,在下一个数字为“0”的情况下添加“10”,在非“0”的情况下添加“0”。
「1」→「」
「2」→「11」
「3」→「1111」
「4」→「111111」
按照上述规则进行编码后,刚才的手牌被如下编码:
[1 3 5 7 9万 1 3 5 7 9筒 东 南 西 北]
→「101010101010101010101010101」(编码之前)
→「101010101010101010101010100」(编码之后)
由于在编码后它是bit串,并且编码后bit数从81bit降到27bit。由于在32bit以内,所以很容易作为索引进行处理。所以我们可以检查所有胡得形式,并根据上述规则对他们进行编码将他们作为索引进行保存。
如何检查所有形式的胡
当手牌以连续牌的数量表示时,根据顺子和刻子,胡牌可以分为以下图形:
「111」「111」「111」「111」「2」(全是顺子)
「111」「111」「111」「3」「2」(一个是刻子)
「111」「111」「3」「3」「2」(两个是刻子)
「111」「3」「3」「3」「2」(三个是刻子)
「3」「3」「3」「3」「2」(全都是刻子)
「2」「2」「2」「2」「2」「2」「2」(七对子)七对子可以不例外
除此之外,考虑到副牌有吃、碰、杠的情况下:
「111」「111」「111」「2」(全部是顺子,一个在副牌)
这样的胡牌图形也可以加入。
在各个图形中都有牌重叠的情况,只需将那些全部枚举出来就行了。例如,在所有都是顺子的情况下,都有以下模式。
「11211」「111」「111」「2」
「222」「111」「111」「2」
通过查看所有牌的重叠和顺序的组合,完成索引。
顺子和刻子的构成
为了判定手牌是否胡,只需检索索引即可,但是要判定胡牌的组成,就需要知道哪个是将牌、哪个是刻子、哪个是顺子。
所以,编码前的数字串中第几个是将牌,第几个是刻子,第几个是顺子,要与索引保持一致。为了保持面子(日本麻将中顺子和刻子的统称)的配置,顺子和刻子由以下比特串构成。
低位
3bit 刻子数量
3bit 顺子数量
4bit 将牌位置
4bit 面子位置
4bit 面子位置
4bit 面子位置
4bit 面子位置
高位
面子的位置按刻子->顺子的顺序放置。
提前判定角色(顺子 刻子 等)
在创建索引时,可以从连续的牌的个数中判定一部分角色。例如,能够事先判定出以下内容。
「222」→一般高
「222」「222」→二盃口
「2」「2」「2」「2」「2」「2」「2」→七対子
「4111111113」→九莲宝灯
「111111111」→清龙
在面子构成的同时,对于事先了解的角色,也将作为比特标志进行保存。
低位
3bit 刻子数量
3bit 顺子数量
4bit 将牌位置
4bit 面子位置
4bit 面子位置
4bit 面子位置
4bit 面子位置
1bit 七对子标志
1bit 九莲宝灯标志
1bit 清龙标志
1bit 二盃口标志
1bit 一般高标志
高位
源代码
常用方法(回溯法)
列举了完整组合的Ruby语言程序
使用索引方法
go语言生成所有胡牌组合源代码
//牌型结果
type MahjongResult struct {
Num_ke int //刻子数量
Num_shun int //顺子数量
Jiang byte //将牌值
Array_ke []byte //刻子数组
Array_shun []byte //顺子数组
Qidui bool //是否七对
Tongtian bool //是否通天
}
//一组牌型
type MahjongGroup struct {
// 牌型Key值
Key uint32
// 牌型结果
Result []uint32
}
//麻将牌型表
type MahjongTable struct {
Groups []*MahjongGroup
}
//生成麻将表
/**
params:
includeQiDui : true 包含七对 ,false 不包含七对
*/
func GenMahjongTable(includeQiDui bool) *MahjongTable {
var array []*MahjongGroup
if includeQiDui {
array = append(array, analyseQiDui()...)
}
array = append(array, genData([][]int{{1, 1, 1}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {1, 1, 1}, {1, 1, 1}, {3}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {1, 1, 1}, {3}, {3}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {3}, {3}, {3}, {2}})...)
array = append(array, genData([][]int{{3}, {3}, {3}, {3}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {1, 1, 1}, {1, 1, 1}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {1, 1, 1}, {3}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {3}, {3}, {2}})...)
array = append(array, genData([][]int{{3}, {3}, {3}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {1, 1, 1}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {3}, {2}})...)
array = append(array, genData([][]int{{3}, {3}, {2}})...)
array = append(array, genData([][]int{{1, 1, 1}, {2}})...)
array = append(array, genData([][]int{{3}, {2}})...)
array = append(array, genData([][]int{{2}})...)
keyMap := make(map[uint32]*MahjongGroup)
for _, v := range array {
if _, found := keyMap[v.Key]; !found {
keyMap[v.Key] = v
}
}
table := &MahjongTable{}
for _, v := range keyMap {
table.Groups = append(table.Groups, v)
}
return table
}
//分析七对
func analyseQiDui() []*MahjongGroup {
//先输入七对子
chidui := Comb([][]int{{2}, {2}, {2}, {2}, {2}, {2}, {2}})
newChidui := make([][][]int, 0)
for _, v := range chidui {
valid := true
for _, vv := range v {
for _, vvv := range vv {
if vvv != 2 && vvv != 4 {
valid = false
break
}
}
if !valid {
break
}
}
if valid {
newChidui = append(newChidui, v)
}
}
keyMap := make(map[uint32]bool)
var array []*MahjongGroup
for _, v := range newChidui {
key := CalKey(v)
if _, found := keyMap[key]; !found {
keyMap[key] = true
data := &MahjongGroup{}
data.Key = key
data.Result = Analyse(v)
array = append(array, data)
}
}
return array
}
func genData(a [][]int) []*MahjongGroup {
r := Comb(a)
var array []*MahjongGroup
for _, v := range r {
data := &MahjongGroup{}
data.Key = CalKey(v)
data.Result = Analyse(v)
array = append(array, data)
}
return array
}
func Comb(a [][]int) (ret [][][]int) {
if a == nil {
ret = make([][][]int, 0)
return
}
size := len(a)
if size <= 1 {
ret = [][][]int{a}
return
}
ret = append(ret, PermArray(a)...)
keyMap := make(map[string]bool)
for i := 0; i < size; i++ {
for j := i + 1; j < size; j++ {
key := fmt.Sprintf("%v0%v", a[i], a[j])
if _, found := keyMap[key]; found {
continue
}
keyMap[key] = true
tMap := make(map[string]bool)
lj := len(a[j])
al := len(a[i]) + lj
for k := 0; k <= al; k++ {
t := make([]int, al+lj)
for l := lj; l < al; l++ {
t[l] = a[i][l-lj]
}
for m := 0; m < lj; m++ {
t[k+m] += a[j][m]
}
var tmp []int
valid := true
for _, v := range t {
if v > 4 {
valid = false
break
}
if v > 0 {
tmp = append(tmp, v)
}
}
if !valid {
continue
}
if len(tmp) > 9 || len(tmp) <= 0 {
continue
}
tmpStr := fmt.Sprintf("%v", tmp)
if _, found := tMap[tmpStr]; !found {
tMap[tmpStr] = true
b := make([][]int, len(a))
copy(b, a)
b = append(b[:i], b[i+1:]...)
b = append(b[:j-1], b[j:]...)
c := make([][]int, 0)
c = append(c, tmp)
if len(b) > 0 {
c = append(c, b...)
}
ret = append(ret, Comb(c)...)
}
}
}
}
return
}
//查表法
func PermArray(a [][]int) (ret [][][]int) {
ret = make([][][]int, 0)
r := perm(a)
keyM := make(map[string][][]int)
for _, v := range r {
key := fmt.Sprintf("%v", v)
if _, found := keyM[key]; found {
continue
}
keyM[key] = v
}
for _, v := range keyM {
ret = append(ret, v)
}
return
}
//排列
func perm(a [][]int) (ret [][][]int) {
ret = make([][][]int, 0)
if a == nil {
return
}
if len(a) <= 1 {
ret = append(ret, a)
return
}
for k, v := range a {
tmp := make([][]int, len(a))
copy(tmp, a)
tmp = append(tmp[:k], tmp[k+1:]...)
for _, tv := range perm(tmp) {
tv = append([][]int{v}, tv...)
ret = append(ret, tv)
}
}
return
}
/计算牌型的key值
func CalKey(a [][]int) (ret uint32) {
l := -1
ret = 0
for _, b := range a {
for _, v := range b {
l++
switch v {
case 2:
ret |= 0x3 << uint(l)
l += 2
case 3:
ret |= 0xF << uint(l)
l += 4
case 4:
ret |= 0x3F << uint(l)
l += 6
}
}
ret |= 0x1 << uint(l)
l++
}
return
}
//分析牌型(暴力拆解)
// 3 bit: 刻子数量(0~4)
// 3 bit: 顺子数量(0~4)
// 4 bit: 将牌位置(1~13)
// 4 bit: 面子1位置(0~13)
// 4 bit: 面子2位置(0~13)
// 4 bit: 面子3位置(0~13)
// 4 bit: 面子4位置(0~13)
// 1 bit: 七对子
// 1 bit: 九莲宝灯
// 1 bit: 通天
func Analyse(a [][]int) []uint32 {
size := len(a)
p_atama := 0
ret_array := make([]uint32, 0)
for i := 0; i < size; i++ {
for j := 0; j < len(a[i]); j++ {
//拆解将牌
if a[i][j] >= 2 {
for ke_shun := 0; ke_shun <= 1; ke_shun++ {
b := make([][]int, len(a))
//Golang slice 为引用类型,此处需在第二维进行copy
for k, v := range a {
b[k] = make([]int, len(v))
copy(b[k], v)
}
b[i][j] -= 2
p := 0
p_ke := make([]int, 0)
p_shun := make([]int, 0)
for k := 0; k < len(b); k ++ {
for m := 0; m < len(b[k]); m ++ {
if ke_shun == 0 {
//先取刻子
if b[k][m] >= 3 {
b[k][m] -= 3
p_ke = append(p_ke, p)
}
for len(b[k])-m >= 3 &&
b[k][m] >= 1 &&
b[k][m+1] >= 1 &&
b[k][m+2] >= 1 {
b[k][m] -= 1
b[k][m+1] -= 1
b[k][m+2] -= 1
p_shun = append(p_shun, p)
}
} else {
//先取顺子
for len(b[k])-m >= 3 &&
b[k][m] >= 1 &&
b[k][m+1] >= 1 &&
b[k][m+2] >= 1 {
b[k][m] -= 1
b[k][m+1] -= 1
b[k][m+2] -= 1
p_shun = append(p_shun, p)
}
if b[k][m] >= 3 {
b[k][m] -= 3
p_ke = append(p_ke, p)
}
}
p += 1
}
}
hu := true
for _, v := range b {
for _, vv := range v {
if vv != 0 {
hu = false
break
}
}
}
if hu {
ret := len(p_ke) + (len(p_shun) << 3) + (p_atama << 6)
l := 10
for _, ke := range p_ke {
ret |= ke << uint(l)
l += 4
}
for _, shun := range p_shun {
ret |= shun << uint(l)
l += 4
}
if len(a) == 1 {
//九莲宝灯
key := fmt.Sprintf("%v", a[0])
if key == "[4 1 1 1 1 1 1 1 3]" ||
key == "[3 2 1 1 1 1 1 1 3]" ||
key == "[3 1 2 1 1 1 1 1 3]" ||
key == "[3 1 1 2 1 1 1 1 3]" ||
key == "[3 1 1 1 2 1 1 1 3]" ||
key == "[3 1 1 1 1 2 1 1 3]" ||
key == "[3 1 1 1 1 1 2 1 3]" ||
key == "[3 1 1 1 1 1 1 2 3]" ||
key == "[3 1 1 1 1 1 1 1 4]" {
ret |= 1 << 27
}
}
//通天
if len(a) <= 3 && len(p_shun) >= 3 {
p_tongtian := 0
for _, c := range a {
if len(c) == 9 {
b_tong1 := false
b_tong2 := false
b_tong3 := false
for _, x_tong := range p_shun {
if x_tong == p_tongtian {
b_tong1 = true
}
if x_tong == p_tongtian+3 {
b_tong2 = true
}
if x_tong == p_tongtian+6 {
b_tong3 = true
}
}
if b_tong1 && b_tong2 && b_tong3 {
ret |= 1 << 28
}
}
p_tongtian += len(c)
}
}
contains := false
for _, v := range ret_array {
if uint32(ret) == v {
contains = true
break
}
}
if !contains {
ret_array = append(ret_array, uint32(ret))
}
}
}
}
p_atama += 1
}
}
//七对子
d := make([]int, 0)
total := 0
dui := true
for _, v := range a {
for _, vv := range v {
d = append(d, vv)
total += vv
if vv != 2 && vv != 4 {
dui = false
}
}
}
if total == 14 && dui {
ret_array = append(ret_array, 0x1<<26)
}
if len(ret_array) > 0 {
return ret_array
}
return nil
}