麻将胡牌判定方法-索引法

本文介绍了一种基于索引法的麻将胡牌判定算法,该算法通过预先生成所有可能的胡牌组合并将其存储为索引,实现快速判断手牌是否胡牌。通过编码牌型,将手牌转换为数值化的key值,从而在map表中进行查询,大大减少了判定时间。文章详细解释了编码规则、牌型分析和源代码实现。
摘要由CSDN通过智能技术生成

麻将高速判定胡牌方法说明

麻将胡牌判定通常使用的方法是回溯法,但是由于回溯法需要暴力检查麻将面值组合,所以需要的处理时间比较长。在执行一次的情况下,处理时间不会成为问题,但是在需要进行番数判定等,进行重复处理的情况下,对于算法的时间复杂度来说就比较够呛。本文中,引诉了日本一种索引法,进行高速判断。

使用索引法

索引法是先将所有的胡牌可能性进行列举,然后将其转换成一个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
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值