2021麻将大作业报告(节选)

本文探讨了如何利用动态规划解决麻将AI的决策问题,提出以巡目数作为估值指标,避免人工选取参数。通过算法思想和实现细节,介绍了如何计算巡目数,并利用状态自动机进行优化。在处理大整数和double时,采用了科学计数法和特定精度的近似计算,解决了效率问题。最后,文章提到了吃碰杠和自动机的优化思路。
摘要由CSDN通过智能技术生成

2. 基于动态规划的Rulebased

2.1 现有问题

我们可以看到,通过上听数、有效牌数能取得较好的效果,整体上牌型是向着胡牌前进的,但是它是基于很多指标的,而不同指标赋予什么样的权重,该如何组合这些指标,是否有些指标应该被忽略掉,这些都是需要人类经验的,并且未必准确。同一套指标,可能在某一局中适用,在另一局中就会给出意外的结果,这样的情况在调试的过程中是很常见的。因此,我思考的是如何减少人工选取参数对于估值的影响,甚至是没有任何参数。

受到了ZJOI2019省选的题目以及题解的启发,我抛弃了上听数和有效牌数,选取了巡目数作为估值指标。首先我们看巡目数的定义。假设牌山中剩余的牌集为 S 1 S_1 S1(即整副牌除开明牌、自家手牌、自家暗杠的牌集),现在 S 1 S1 S1中的牌会以相同的概率生成摸牌打牌的序列,对于单个生成序列 T i T_i Ti,它至少前 K T i K_{T_i} KTi张牌能和我的手牌凑成胡牌牌型,则对所有的 K T i K_{T_i} KTi取期望,该期望 E E E即为巡目数

这里给出一个具体的例子,也是在调试算法时候常用的一个小例子。假设手牌是“113399条”这六张牌,剩余的牌是“1223条、南风”这五张牌,前面我已经碰了两次了(所以只剩6张手牌)。显然我还需要两张牌才能胡,要么给我13条,要么给我两个2条。剩余的五张牌一共可以生成等概率的120个序列,通过简单的排列组合知识可得,在这120种序列中,有24个序列的前两张牌即可让我胡,有48个序列需要我从前三张牌中取两张胡,有48个序列需要我从前四张牌中取两张胡。那么此时这个牌型的巡目数就是(24 * 2 + 48 * 3 + 48 * 3) / 120 = 3.2,其含义就是期望条件下我需要从剩余序列的前3.2张牌中取出2张牌才能胡。

从上面的例子中我们也可以看到,巡目数是综合了手牌和剩余牌进行通盘考虑的,两组牌在上听数相同、有效牌相近的情况下,根据剩余牌的不同,巡目数有可能大不一样。另外,如果剩余牌是接近无限的,那么上听数越小,巡目数也就越小。可以说,上听数就是剩余牌无限情况下的巡目数-1,巡目数是上听数结合剩余牌数的泛化。

2.2 算法思想

承接上节,我们的主要目标就是求出巡目数,然后以巡目数作为出牌的依据:巡目数越大,证明在现有牌堆的情况下越难胡,因此我们会选择相对较小的巡目数来出牌。

直接计算巡目数并不是一件简单的事情。如果直接枚举可能有那些出牌序列,然后计算每一个序列的 K T i K_{T_i} KTi,显然复杂度是阶乘级的,这对于每回合1s的限制是无法承受的。因此我们需要转换思路。

假设手牌数为 m m m张,牌山中剩余牌集 S 1 S_1 S1大小为n,那么我们要求的值为 E ( x ) = ∑ i = m + 1 m + n P ( x = i ) ⋅ ( i − m ) E(x)=\sum_{i=m+1}^{m+n}P(x=i)·(i-m) E(x)=i=m+1m+nP(x=i)(im),其中 P ( i ) P(i) P(i)表示拿到 i i i张牌可以胡牌的概率,然后进行转化 E ( x ) = ∑ i = m + 1 m + n ( P ( x ≥ i ) − P ( x ≥ i + 1 ) ) ⋅ ( i − m ) E(x)=\sum_{i=m+1}^{m+n}(P(x\geq i) - P(x \geq i+1))·(i-m) E(x)=i=m+1m+n(P(xi)P(xi+1))(im),拆解然后合并同类项之后我们可以得到 E ( x ) = ∑ i = m + 1 m + n P ( X ≥ i ) E(x)=\sum_{i=m+1}^{m+n}P(X\geq i) E(x)=i=m+1m+nP(Xi)。也就是说,我们需要求的变成了如何快速计算我拿到 i i i张牌之后还不胡的概率。

这里不妨先来看一看,如何计算一副牌是否胡了。胡牌是由很多牌型的,包含了很多特殊的胡法,不同的胡法番数也不同。这里我们先简化问题,将问题限制在只能平和和七对胡这两种情况之内。首先,我们对手牌做一个编码,具体编码如下:

牌型 对应区间
万牌 1-9
条牌 12-20
筒牌 23-31
风牌 34-43
箭牌 46-52

代码中的card2num和num2card实现了这两者的转化。

int card2num(string s){
   
    assert(s.length() == 2);
    int num = s[1] - '0';
    switch(s[0]){
   
        case 'W' : return 0 + num; break;
        case 'T' : return 11 + num; break;
        case 'B' : return 22 + num; break;
        case 'F' : return 31 + num * 3; break;
        case 'J' : return 43 + num * 3; break;
        default : break;
    }
    return 0;
}
string num2card(int num){
   
    if(1 <= num && num <= 9) return "W" + to_string(num - 0);
    else if(12 <= num && num <= 20) return "T" + to_string(num - 11);
    else if(23 <= num && num <= 31) return "B" + to_string(num - 22);
    else if(34 <= num && num <= 43 && !((num - 34) % 3)) return "F" + to_string((num - 31) / 3);
    else if(num == 46 || num == 49 || num == 52) return "J" + to_string((num - 43) / 3);
    else return "aha";
    return "aha";
}

这样我们可以抛开各自的牌型,把他们当作相同类型的牌去对待。因为每种牌型之间都间隔了两个,所以形如“九万”和“幺鸡二条”是无法凑成顺子的。那么我们算法的输入就是大小为52的数组a, a i a_i ai表示编号为 i i i的牌有几张。我们在这上面做dp,令 f 0 / 1 , i , j , k f_{0/1,i,j,k} f0/1,i,j,k表示有无对子第 i − 1 i−1 i1 种牌开头的顺子有 j j j 个,第 i i i 种牌开头的顺子有 k k k 个且其余牌全部作为刻子的最大面子数。如果不考虑对子,那么 f 0 , f 1 f_0, f_1 f0,f1会分开转移并且他们的状态转移方程是完全相同的,如果分开考虑,我们有
f 0 , i + 1 , k , t = ∑ j ( f 0 , i , j

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值