麻将胡牌算法的一种设计及其分析

马勇波  陈欣庆
(解放军理工大学工程兵工程学院研究生二队,南京 210007)
 
    摘  要  文章通过一个二维数组定义麻将的数据结构,并在此基础上设计了一种判断麻将是否胡牌的算法,该算法主要步骤的时间复杂度为O (n ),且基本上处于“原地工作”。在经过算法判断运算后,该二维数组最终会恢复到最初的数据。
    关键词  麻将;胡牌算法;复杂度
 

1 引言

    麻将起源于中国,它集益智性、趣味性、博弈性于一体,是中国传统文化的一个重要组成部分。麻将胡牌的形式千变万化,数据结构的定义也不尽相同,相应的胡牌的算法也多种多样,很值得程序设计人员学习探讨。下面介绍了一种胡牌的算法,并讨论了它的复杂度。

2  数据结构的定义

    麻将由“万”、“筒”、“索”、“字”四类牌组成,其中“万”又分为“一万”“二万”……“九万”各4张共36张,“筒”“索”类似,“字”分为“东”“南”“西”“北”“中”“发”“白”各4张共28张。这里定义了一个4 x 10的数组int allPai [4][10],它记录着手中的牌的全部信息,行号记录类别信息,第0~3行分别代表“万”“筒”“索”“字”。以第0行为例,它的第0列记录了牌中所有“万”的总数,第1~9列分别对应着“一万”~“九万”的个数,“筒”“索”类似。“字”不同的是第1~7列对应的是“东”“南”“西”“北”“中”“发”“白”的个数,第8,9列恒为0。根据麻将的规则,数组中的牌总数一定为3n+2,其中n=0,1,2,3,4。如有下面的数组:
    allPai[4][10]={{6,1,1,1,0,3},{5,0,2,0,3},{0},{3,0,3}}
    它表示手中的牌为:“一万”“二万”“三万”“五万”“五万”“五万”“二筒”“二筒”“四筒”“四筒”“四筒”“南”“南”“南”,共6张“万”,5张“筒”,0张“索”,3张“字”。

3  算法设计

    由于“七对子”“十三幺”这种特殊的牌型胡牌的依据不是牌的相互组合,而且规则也不尽相同,这里将这类情况排除在外。
    尽管能构成胡的牌的形式千变万化,但稍加分析可以看出它离不开一个模型:它可以分解为“三、三……三、二”的形式(总牌数为3n+2张),其中的“三”表示的是“顺”或“刻”(连续三张牌叫做“顺”,如“三筒”“四筒”“五筒”,“字”牌不存在“顺”;三张同样的牌叫做“刻”,如“三筒”“三筒”“三筒”);其中的“二”表示的是“将”(两张相同的牌可作为“将”,如“三筒”“三筒”)。
    在代码实现中,首先就判断手中的牌是否符合这个模型,这样就用极少的代价排除了大多数情况,具体作法是用3除allPai [i][0],其中i = 0, 1, 2, 3,只有在余数有且仅有一个为2,其余全为0的情况下才可能构成胡牌。对于余数为0的牌,它一定要能分解成一个“刻”和“顺”的组合,这是一个递归的过程,由函数bool Analyze(int [],bool)处理;对于余数为2的牌,一定要能分解成一对“将”与“刻”和“顺”的组合,由于任何数目大于等于2的牌均有作为“将”的可能,需要对每张牌进行轮询,如果它的数目大于等于2,去掉这对“将”后再分析它能否分解为“刻”和“顺”的组合,这个过程的开销相对较大,放在了程序的最后进行处理。在递归和轮询过程中,尽管每次去掉了某些牌,但最终都会再次将这些牌加上,使得数组中的数据保持不变。
    最后分析递归函数bool Analyze(int [], bool),数组参数表示一类牌:“万”、“筒”、“索”、“字”之一,布尔参数指示数组参数是否是“字”牌,这是因为“字”牌只能“刻”而不能“顺”。对于数组中的第一张牌,要构成胡牌它就必须与其它牌构成“顺”或“刻”。如果数目大于等于3,那么它们一定是以“刻”的形式组合。譬如:当前有3张“五万”,如果它们不构成“刻”,则必须有3张“六万”3张“七万”与其构成3个“顺”(注意此时“五万”是数组中的第一张牌),否则就会剩下“五万”不能组合,而此时的3个“顺”实际上也是三个“刻”。去掉这三张牌,递归调用bool Analyze(int [],bool)函数,成功则胡牌。当该牌不是字牌且它的下两张牌均存在时它还可以构成“顺”,去掉这三张牌,递归调用bool Analyze (int [],bool)函数,成功则胡牌。如果此时还不能构成胡牌,说明该牌不能与其它牌顺利组合,传入的参数不能分解为“顺”和“刻”的组合,不可以构成胡牌。

4  时间复杂度分析

    度量一个算法的效率,通常采用事前分析估算的方法。由于语言、编译程序等的不同,用绝对的时间单位来比较并不合适,一般撇开这些因素,用时间复杂度T (n)=O (f (n))来衡量,它表示随问题规模n的增大,算法执行时间的增长率和f (n)的增长率相同,算法的工作量只与问题的规模相关 [1]
    从胡牌算法可以看出,在许多情况下,函数能很快返回成功或者失败,而在某些情况下,函数可能需要不断地递归。虽然从理论上说每张牌出现的概率是相等,但要计算出它的平均时间复杂度仍然是相当困难的,这里采用另外一种思路:讨论最坏情况下的复杂度。
    不难看出,算法的主要开销集中在分解为“刻”“顺”组合的递归和寻找“将”的轮询上,而实际上轮询的复杂性也在于它调用了递归函数,因此这里主要分析递归函数的复杂度。
可以这样认为,当递归函数bool Analyze(int [],bool)传入的数组参数中有3n张牌时,问题规模为n。在j从1到10的循环中,没有递归操作,在不限定n<=4的情况下它的开销与其它处明显不在一个数量级,这里假定它消耗了10个时间单位。从代码容易看出组合成“刻”要比组合成“顺”的代价要小,所以最坏的情况下,没有“字”牌,没有数目大于等于3的牌,它们最终都以“顺”组合。记问题规模为n的该递归函数的复杂度为f (n),则在最坏的情况下:
f (n) = 1+10+4+f (n-1) +4
        = f (n-1) +19
     于是f (n) - (f (n-1) = 19
    ……
f (1) - f (0) = 19
    又  f (0) = 1
所有等式两边分别相加,
    得f (n) = 19n+1
    如果不限定问题规模n<=4,可以认为该递归函数的时间复杂度为O (n )。

5  空间复杂度分析

    空间复杂度是对算法所需存储空间的量度。记作T (n)=O (f (n))。发生函数调用时,系统首先要保存三方面的信息:(1)当前执行函数的中断返回地址;(2)当前执行函数调用时与形参结合的实参值,包括函数名和函数参数;(3)当前执行函数的局部变量,递归函数也是如此 [1]
对于此胡牌算法,一方面,递归函数调用自身的次数与问题规模n成正比,当n增大时,所需要的用来存储中断返回地址、参数、局部变量等信息的内存单元也越多。另一方面,数组作为参数传递的只是指针,每次数组操作都在一个存储空间内进行,与问题规模n没有关系,从某种意义上可以说它是在“原地工作”。

6  源代码

#include <stdio.h>
#include <math.h>
//函数声明
bool Win(int [4][10]);
bool Analyze(int [],bool);
 
int main(int argc, char* argv[])
{
     //定义手中的牌
     int allPai[4][10]={
                       {6,1,4,1},//万
                       {3,1,1,1},//筒
                       {0},//索
                       {5,2,3}//字
                       };
     if (Win (allPai))
         printf("Hu!/n");
     else
         printf("Not Hu!/n");
     return 0;
}
//判断是否胡牌的函数
bool Win (int allPai[4][10])
{
     int jiangPos;//“将”的位置
int yuShu;//余数
     bool jiangExisted=false;
     //是否满足3,3,3,3,2模型
     for(int i=0;i<4;i++)
     {
         yuShu=(int)fmod(allPai[i][0],3);
         if (yuShu==1)
         {
              return false;
         }
         if (yuShu==2) {
              if (jiangExisted)
              {
                   return false;
              }
              jiangPos=i;
              jiangExisted=true;
         }
     }
     for(i=0;i<4;i++)
     {
         if (i!=jiangPos) {
              if (!Analyze(allPai[i],i==3))
              {
                   return false;
              }
         }
     }
     //该类牌中要包含将,因为要对将进行轮询,效率较低,放在最后
     bool success=false;//指示除掉“将”后能否通过
     for(int j=1;j<10;j++)//对列进行操作,用j表示
     {
         if (allPai[jiangPos][j]>=2)
         {
              //除去这2张将牌
              allPai[jiangPos][j]-=2;
              allPai[jiangPos][0]-=2;
         if(Analyze(allPai[jiangPos],jiangPos==3))
              {
                   success=true;
              }
              //还原这2张将牌
              allPai[jiangPos][j]+=2;
              allPai[jiangPos][0]+=2;
              if (success) break;
         }
     }
     return success;
}
//分解成“刻”“顺”组合
bool Analyze(int aKindPai[],bool ziPai)
{
     if (aKindPai[0]==0)
     {
         return true;
     }
//寻找第一张牌
     for(int j=1;j<10;j++)
     {
         if (aKindPai[j]!=0)
          {
              break;
         }
     }
     bool result;
     if (aKindPai[j]>=3)//作为刻牌
     {
         //除去这3张刻牌
         aKindPai[j]-=3;
         aKindPai[0]-=3;
         result=Analyze(aKindPai,ziPai);
         //还原这3张刻牌
         aKindPai[j]+=3;
         aKindPai[0]+=3;
         return result;
     }
     //作为顺牌
     if ((!ziPai)&&(j<8)
         &&(aKindPai[j+1]>0)
         &&(aKindPai[j+2]>0))
     {
         //除去这3张顺牌
         aKindPai[j]--;
         aKindPai[j+1]--;
         aKindPai[j+2]--;
         aKindPai[0]-=3;
         result=Analyze(aKindPai,ziPai);
         //还原这3张顺牌
         aKindPai[j]++;
         aKindPai[j+1]++;
         aKindPai[j+2]++;
         aKindPai[0]+=3;
         return result;
     }
     return false;
}
 
程序的运行结果为:Hu!

参考文献

[1] 严蔚敏,吴伟民.数据结构[M].北京:清华大学出版杜,1992年6月第二版 12-15.
 
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值