【学习笔记】关于胡牌算法的一些小记

时间:2024-05-19 09:18:09

目前公司项目对于胡牌算法有一定的优化需求,对于新进公司的我来说是一个很好的锻炼机会,既可以依此熟悉公司项目代码,又可以增进自己对于技术的一些了解,这是前提。(有问题的地方还望斧正)


先说一下结果吧,目前实现的计算效率和项目中DFS效率差不太多,约为(42 - 43)万次判胡/秒【Cpu i5-3470s 内存8GB】,占用内存约为15MB,还有一些可以优化的空间。(C++实现)

【学习笔记】关于胡牌算法的一些小记


由于对于棋牌类的游戏并不是特别了解,遂在网络上查找了很多算法,解决方案大致分为两种:

1.DFS递归检测牌型

    优点:通用性高,大部分麻将规则都可以不经过修改代码直接使用到项目中,面对常见牌型效率较高。

    缺点:对于递归深度高的情景下,效率并不是特别高,碰到比较特殊的牌型和规则效率会降低很多(比如有多个赖子的麻将规则/清一色等特殊牌型)

2.查表法

    优点:对于所有牌型的处理效率为常数级,查表速度相对稳定

    缺点:由于查表需要穷举出所有可能胡牌的情况,存储胡牌表需要较高的内存,并且如果穷举所有的胡牌可能,这个计算(生成胡牌表)的过程也会消耗许多时间。

    考虑到查表法的效率实在很吸引人,如果做得好的话,胡牌速度检测速度可以提高到非常大的量级。所以就准备使用查表法进行优化。


    选好了解决方案,现在考虑如何实现。

    首先我们要解决的问题就是存储胡牌表,胡牌表是这个算法的核心,如果这张表没有做好效率会大打折扣,但是现在有一个问题。胡牌的组合大约为千万级,如果全部存储下来的话,内存消耗将会较多,并且查找速度会因为表的增大而出现放缓的趋势。所以我们需要对生成的牌型和组合进行一定的压缩。网上有一个思路非常好的解决方案,很好的利用了哈夫曼编码以及一些特定规则将牌型数量和存储量压缩的非常小的一个规模:

https://www.zhihu.com/question/47124225/answer/104955538

第一个答主的回答。

    具体的思路是先将手牌进行一定规则的排序,比如按照万(1-9),条(1-9),饼(1-9),风牌(东南西北),箭牌(中发白)这样的顺序将牌一次排列好。并且每一种的牌按照其在手牌中的数量来表示,并且将不连续的牌用【0】分割开【连续的牌是指按照上述规则排列好后的牌型中,同种花色连续的牌,比如1万2万就是连续的,1万3万就不是连续的】,这样就可以减少牌型的数量和存储大小,并且使我们并不需要关心牌型的具体情况只要关注其中的数字,减少了很多开销。例: 

【1万,2万,3万,5万,6万,7万,7条,8条,9条,东风,东风】→【1110111011102】

【1万,1万,1万,2万,3万,4万,6万,7万,8万,西风,西风,西风,白板,白板】→

【311101110302】

【3万,3万,3万,4万,4万,4万,4万,5万,5万,5万,5万,6万,6万,6万】→【3443】


    经过上述编码后可以根据各个数字出现的概率,进行一次哈夫曼编码,规则如下:

字符

编码

1

0

2

110

3

11110

4

1111110

10

10

20

1110

30

111110

40

11111110

    哈夫曼编码后,存储长度得以进一步压缩。以14张牌为例,在极端情况下,14张均不连续的牌的存储的长度也仅仅为27bit。一个int就可以存储的下。同时也可以保证数据的唯一性。

    存储问题解决了,现在需要解决的是查询的问题,就算我们对数量庞大的胡牌表进行了高效的压缩,但是压缩后的数据还有近9000条。如果按照普通的顺序查找显然是行不通的。虽然2分查找和红黑树的查找也很高效,但是我们的目的是尽可能的加速查找的效率,最后还是选择了用哈希表来对数据存储和进行查找。代价就是相对于红黑树这些数据结构来说,哈希需要额外的一些存储空间来增加其查找效率。

    为了计算出压缩后的胡牌表,本文先将牌型用int[34] 存储起来(每一个int存储手牌中该类型的数量),然后并用递归的方式穷举胡牌的组合【按照3*n +2的胡牌规则,其中n为刻子或顺子的数量】并用以上的算法进行压缩存储,并筛选掉一些非法的手牌牌型(牌的数量大于4的情况) 和去重(穷举出的牌型和压缩后的牌型),就可以得到。

    生成胡牌表中还碰到一个小问题,在生成胡牌表的时候,速度异常的慢,在穷举14张牌的胡牌表的时候居然需要好几个小时才能计算完成。这显然是不可接受的,但是我又检查了遍历的代码,又尽可能做了优化,但是还是效果甚微。查了1天之后,发现是因为我把穷举后的未压缩的数据输出至文本中,大量的数据导致磁盘的IO跟不上CPU的计算速度。注释掉之后,速度提升至1个小时计算完成。但是还是很慢。继续优化:

1.原本使用STL的hashmap来进行存储,现修改成自己写的简单hash,仅使用线性探测来解决冲突问题。

2.牌型转换方面,减少了循环检查的计算量。

3.哈夫曼数的编码转码方面,使用位操作来提升转码速度。

速度提升的幅度较大,1分钟就可以生成所有14张的胡牌牌型,如果是生成17张的玩法的话,则需要1个半小时左右。

    同时,这些优化又可以对判胡牌型有所帮助,使原本的判胡速度逐步提升(13万次/秒->23万次/秒->43万次/秒)

    有赖子判胡的话,目前的解决方案是将赖子转换成普通的牌型进行查表,效率略低,之后可以进行进一步的改进,生成赖子胡牌表,提升胡牌判断的速度。而且判胡的主要时间消耗其实在于赖子判胡。之后可以找时间对其进行进一步的优化。