继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

node.js——麻将算法(五)胡牌算法的一些优化处理方案(有赖子版)

九日王朝
关注TA
已关注
手记 180
粉丝 42
获赞 185

以前有赖子判胡算法

http://blog.csdn.net/sm9sun/article/details/65632646


以前的帖子说明了处理赖子的两种方案:枚举代替及插空补缺,并最终选择了枚举遍历方案,因为当初考虑的是通过GetAppointList方法已经会剔除大多数无关联的牌了,且后期大家通过吃碰杠等也会减少手牌,姑且认为n的值为10,因为大多数麻将四个赖子直接胡。所以三个赖子也不过是1000次而已。不过随着后期做的麻将多了,才发现自己真的是图样图森破了,例如涞源的玩法,TM的三张赖子。。。一副牌赖子比普通牌还多。。。我真的是日了狗了

https://img4.mukewang.com/5b4dd39c0001331000710072.jpg


我们看枚举法,当优化n已经满足不了我们时,我们只能想办法减少循环的次数,我们看之前的算法,若其牌好可以胡牌,那么指不定在哪个位置就返回true了,最头疼的是他还胡不了,一手不连续的烂牌导致我们跑了所有的情况,针对于这种情况,我们可以在枚举前看看是否存在一张孤立单一的牌,因为若存在一张这种牌,那么他至少就得消耗一张赖子(凑成将),这样我们就能减少一层循环。

/*混牌必须替代的牌,用于剪枝当存在一张孤立的牌时,则其至少消耗掉一张混牌也存在孤立的牌是混牌的情况,不过此时运算效率已经很快了,不需要特殊处理十三幺胡法不适用*/function HunAppoint_mast(arr, Hun,special){if (special.H_13one){return;}if (Hun < 0) {return;}for (var i = 0; i < special.mj_count; i++){if (arr[Hun] == 0){return;}if (i > 26){if (arr[i] == 1){arr[i]++;arr[Hun]--;}}else if (i == 0 || i == 9 || i == 18){if (arr[i] == 1 && arr[i + 1] == 0 && arr[i + 2] == 0){arr[i]++;arr[Hun]--;}}else if (i == 8 || i == 17 || i == 26) {if (arr[i] == 1 && arr[i - 1] == 0 && arr[i - 2] == 0){arr[i]++;arr[Hun]--;}}else if (i == 1 || i == 10 || i == 19) {if (arr[i] ==1 && arr[i - 1] == 0 && arr[i + 1] == 0 && arr[i + 2] == 0){arr[i]++;arr[Hun]--;}}else if (i == 7 || i == 16 || i == 25) {if (arr[i] ==1 && arr[i - 1] == 0 && arr[i + 1] == 0  && arr[i - 2] == 0){arr[i]++;arr[Hun]--;}}else {if (arr[i] == 1 && arr[i - 1] == 0 && arr[i + 1] == 0 && arr[i - 2] == 0 && arr[i + 2] == 0){arr[i]++;arr[Hun]--;}}}}


注:arr为手牌麻将个数数组,Hun为混牌(赖子),special是一些特殊牌型的记录,例如十三幺等



这样下来我们就可以减少几层循环了。不过仍然解决不了问题,经测试,当赖子高达8个以上时运算速度已经超1s了,游戏体验极差。


于是针对于这种情况,我们只能考虑第二种思路———插空补位


这种算法顾名思义,当我们处理一些牌的时候发现其无法剔除一组,我们不能像普通胡牌算法那样直接返回false,而是用赖子补上这个位置。最后判断需要的赖子数是否小于等于实际拥有的赖子数即可

if (Hun < 0) {var Huncount = 0;}else {var Huncount = arr[Hun];arr[Hun] = 0;}var count = 0;for (var i = 0; i < arr.length; i++) {count += arr[i];}if (special.H_7pair && count + Huncount == 14) {        var needhun = 0;        for (var i = 0; i < arr.length; i++) {            var c = arr[i];            if (c % 2 == 1) {                needhun++;            }        }        if (needhun <= Huncount) {            return true;        }  }if (special.H_13one && count + Huncount==14) {var pairCount = 0;var canhu = true;var needhun = 0;var ones = [0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33];for (var i = 0; i < ones.length; ++i) {if (arr[ones[i]] == 2) {pairCount++;}if (arr[ones[i]] > 2) {canhu = false;}if (arr[ones[i]] == 0) {needhun++;}}if (pairCount==0){pairCount++;needhun++;}if (pairCount == 1 && canhu && needhun <= Huncount) {return true;}}for (var i = 0; i < 4; i++) {var needhun = 0;for (var j = 0; j < 4; j++) {needhun += j == i ? getneedhun(arr, j,false) : getneedhun(arr, j,true);}if (needhun <= Huncount) {return true;}}return false;



注:七小对和十三幺特殊处理,若不满足这两种,则计算需要的赖子个数needhun。由于允许有空缺,所以我们无法将其按上一章基本判胡优化方法那样全部分割,不过我们可以分成四组,万筒条风,然后分别计算其某一组作为将的内组(3N+2)及其他三组(3N)需要的赖子和。

getneedhun的参数:(手牌数组,花色,是否已有将)

var getneedhun = function (old_arr, type, hasjiang) {var data = {needhun: 0,hasjiang: hasjiang};var arr = old_arr.concat();var i, j;switch (type){case 0: { i = 0; j = 8;break;}case 1: { i = 9; j = 17;break;}case 2: { i = 18; j = 26;break;}case 3: { i = 27; j = 33;break;}}data=dfs(arr, i, j, data);return data.needhun;};




dfs用作来深度遍历数组某段,计算若剔除全部对应的i牌,最小消耗的混牌数,已知剔除有两种方法,顺子剔除del_list和同牌剔除del_same(两个三个一起算)

然后选取一个最优的结果

var fmin_data = function (data1, data2) {return data1.needhun > data2.needhun ? data2 : data1;};




var dfs = function (arr, i, j, data) {if (i > j) {if (!data.hasjiang) {data.needhun += 2;}return data;}if (i % 9 == 6 && i < 27 && arr[i + 1]%3 == 1 && arr[i + 2]%3 == 1)//8 9特殊情况,此时应该补个7{return del_list(arr, i, j, data);}else if (arr[i] == 0) {return dfs(arr, i + 1, j, data);}else if (i % 9 < 7 && i < 27 && (arr[i + 1] > 0 || arr[i + 2] > 0)) {var tmp1 = del_list(arr, i, j, { needhun: data.needhun, hasjiang: data.hasjiang });var tmp2 = del_same(arr, i, j, { needhun: data.needhun, hasjiang: data.hasjiang });return fmin_data(tmp1, tmp2);}else { return del_same(arr, i, j, data); }};




注:

1.判断条件为i不是风牌且小于等于7,同时arr[i+1],arr[i+2]至少有一个,否则可以只考虑del_same这种策略

2.由于del_list和del_same会影响arr数组及data对象,所以请注意参数传递类型


我们先看del_list,即剔除从i开始的三张连续的牌,若有扣除一个,若没有needhun+1,剔除后再通过dfs下一步遍历,注意此时arr[i]不一定为0,所以dfs的i不变


var del_list = function (old_arr, i, j, data) {var arr = old_arr.concat();for (var k = 0; k < 3;k++){if (arr[i + k] > 0){arr[i+k]--;}else{data.needhun++;}}return dfs(arr, i , j, data);};


然后是del_same,这里通过hasjiang判断走到当前时是否存在将,因为若出现多个将牌,那张牌做将都可以,反正赖子要与其他的将组成3N


var del_same = function (old_arr, i, j, data) {var arr = old_arr.concat();arr[i] %= 3;switch (arr[i]) {case 0: {break;}case 1: {if (data.hasjiang) { data.needhun += 2; }else { data.needhun++; data.hasjiang = true; }break;}case 2: {if (data.hasjiang) { data.needhun += 1; }else { data.hasjiang = true; }break;}}arr[i] = 0;return dfs(arr, i + 1, j, data);};


注:


1.getneedhun最后,如果没有将,则消耗两张赖子作为将,因为在这里,arr[i]若存在1张牌或2张牌,我们将其优先做将是会减少一张赖子的代价的。只有arr[i]等于3时我们才直接跳过,若出现131等组合情况,我们也会考虑111拆分后的赖子代价,所以若最后没有将,一定是所有的牌满足3N的赖子代价小于等于1,不存在由于之前没有选将导致后期用两张赖子补将造成非最优解的情况。举个例子 13两张牌,消耗1个赖子作2 然后再用2个赖子作将,共需要3个赖子,而做成11133 或11333也需要3个赖子,并不改变最优解。

2.del_same后当前arr[i]一定为0,故dfs从i+1即可。


效率测试截图:


八赖子 不能胡

https://img.mukewang.com/5b4dd46300014a2706420190.jpg




六赖子  不能胡


https://img1.mukewang.com/5b4dd46c0001174006400173.jpg


一赖子 不能胡


https://img.mukewang.com/5b4dd474000135fc06410191.jpg


八赖子 能胡


https://img3.mukewang.com/5b4dd47a00016b2f06420181.jpg


六赖子 能胡

https://img2.mukewang.com/5b4dd4800001904906400190.jpg


一赖子 能胡


https://img1.mukewang.com/5b4dd4860001cba206450196.jpg


无赖子 不能胡


https://img2.mukewang.com/5b4dd48d0001252106390187.jpg




注:时间单位为毫秒,为了区分效果,每个样例跑了1W次,其实也不是很精确,因为实际跑的函数调用除了上述算法代码还有其他的处理,所以其实运行时间会更快,这几个测试截图只适用于对比不同牌型不同情况的运行状态而已。我们已知9个赖子一定是可以胡的,所以不考虑七对十三幺的情况下,我们对比没有赖子,一赖子,六赖子,八赖子等几种情况。

1.首先能胡的效率一定很快,因为赖子多时,很有可能在最外层枚举那种花色做将的循环中needhun <= Huncount就满足条件了,直接返回true

2.牌可以组成list的时候越多,效率越慢,这个不难理解,因为如果都是单独的,只考虑del_same就可以了。

3.最复杂的牌即每张牌都要走del_list,然后最后还不能胡

4.赖子越少需要处理的普通牌就越多,即效率越慢。所以究竟是枚举还是插空可以根据赖子个数分治处理。



打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP