可帶癩子的通用麻將胡牌算法

本文原創文章,轉載註明出處,博客地址 https://segmentfault.com/u/to... 第一時間看後續精彩文章。以爲好的話,順手分享到朋友圈吧,感謝支持。git

筆者前段時間作過一款地方麻將遊戲的後端,麻將遊戲有個特色就是種類繁多,有的玩法木有癩子,有的玩法有4個癩子,有的甚至癩子數量更多,甚至有的有花牌(春夏秋冬等),有的紅中能夠代替寶牌,具體玩法筆者在此不介紹,作相關開發的自行研究玩法就好github

查表法

筆者看過其它的算法思路,好比查表法,首先生成好麻將牌型的表存文件中,經過將牌型與文件中的牌型進行對比,此類算法,簡單玩法效率也挺高算法

缺點:segmentfault

  1. 是要提早生成好表文件,而且,因爲麻將玩法種類繁多,對於複雜的玩法,表記錄很是多,多達數百萬條記錄,雖然算法中有剪枝,可是效率仍然沒有顯著優點
  2. 表文件讀入到內存中,長期佔用大量內存
  3. 移植性弱,換一種玩法,就得從新生成表數據

在此筆者根據本身的經驗總結出一種通用的麻將胡牌算法後端

思路

知足M x ABC + N x DDD + EE 便可胡牌數組

下面表述中的 3同即DDD牌型,3連即ABC牌型,一對將即EE數據結構

image.png

一個有136張牌,萬,餅,條,東西南北中發白34種牌,有四個癩子是直接就胡牌的,最壞的狀況是有3個癩子,若是把癩子分別當作其中一張牌,3個癩子有34x34x34=39304接近4萬種排列組合,這種算法明顯很差app

從另一個大的思路出發,將手牌分離成寶,萬,條,筒,風5個一維數組(同類型牌才能造成整撲或將),先無論出癩子,我先計算出剩下的牌造成整撲一將(整撲即ABC或DDD,將即EE),至少須要多少癩子,若是須要的癩子數量小於或等於手上有的癩子數量,便可胡牌模塊化

這裏剛開始就分紅寶,萬,條,筒,風5個一維數組的好處是:分類處理,簡化後面的判斷牌型邏輯,而且對於有花牌或其餘特殊類型牌時,可根據玩法,適當調整或增長類型,容易擴展,通用處理方案,且單獨提出函數,模塊化,容易根據玩法修改函數

四種狀況
  1. 將在[萬]中,餅[風]必然是整撲
  2. 將在[餅]中,萬[風]必然是整撲
  3. 將在[條]中,萬[風]必然是整撲
  4. 將在[風]中,萬[條]必然是整撲

那麼問題來了,如何判斷造成整撲,須要的最少癩子數量?

通過分析,必須從小到大排序後,先去3同,再去3連,再去2同,再去2連,這些最容易造成整撲的去掉後,而後剩下的每張牌都須要2癩子,這才能獲得最少的癩子數量

2連:某張牌若是能和後面的牌差爲1或2

2同:如某張牌和下張牌相等

麻將的數據結構

根據麻將牌的特別,經過百位數1,2,3,4,5區別牌類型,個位數表明具體哪張牌,方便後面算法中進行判斷

整理代碼以下:

其中包含了特殊玩法的 制飛 ,十三爛,7對等特殊玩法的判斷,讀者可下載運行,

package algorithm

import (
    "fmt"
    "sort"
    "log"
)

/**
 * WuMing
 *2017/7/6 下午2:40
 *針對瑞金麻將的函數
 */

var (
    ruiJinmahjongArr = []int{
        101, 102, 103, 104, 105, 106, 107, 108, 109, //#萬
        101, 102, 103, 104, 105, 106, 107, 108, 109,
        101, 102, 103, 104, 105, 106, 107, 108, 109,
        101, 102, 103, 104, 105, 106, 107, 108, 109,
        201, 202, 203, 204, 205, 206, 207, 208, 209, //#餅
        201, 202, 203, 204, 205, 206, 207, 208, 209,
        201, 202, 203, 204, 205, 206, 207, 208, 209,
        201, 202, 203, 204, 205, 206, 207, 208, 209,
        301, 302, 303, 304, 305, 306, 307, 308, 309, //#條
        301, 302, 303, 304, 305, 306, 307, 308, 309,
        301, 302, 303, 304, 305, 306, 307, 308, 309,
        301, 302, 303, 304, 305, 306, 307, 308, 309,
        401, 402, 403, 404, 405, 406, 407, //# 東 西 南 北 中 發 白
        401, 402, 403, 404, 405, 406, 407,
        401, 402, 403, 404, 405, 406, 407,
        401, 402, 403, 404, 405, 406, 407,
        501, 502, 503, 504, //花(春夏秋冬)
    }
)

/**
吳名
2017/7/10 下午5:26
3n+2牌型的胡牌
*/
func isHU(arr []int, bao int) bool {
    mjArr := append([]int{}, arr...)

    //寶,萬,筒,條,風
    sptArr := seperateRuiJinArr(mjArr, bao)

    baoNum := len(sptArr[0]) //手牌中寶的數量
    if baoNum == 4 || (bao > 500 && baoNum == 3) {
        //飛
        log.Println("胡:全部的寶牌都在一家,飛")
        return false
    }

    //檢測牌數量
    if len(mjArr)%3 != 2 {
        log.Println("牌數量不符合3n+2")
        return false
    }

    needBaoArr := [5]int{}
    for i := 1; i <= 4; i++ {
        a := append([]int{}, sptArr[i]...)
        needNum := getNeedBaoNumToZhengPu(a)
        needBaoArr[i] = needNum
    }

    // 吳名 2017/7/7 下午8:30 將在"萬"中
    needBaoNum := needBaoArr[2] + needBaoArr[3] + needBaoArr[4]
    if needBaoNum <= baoNum {
        leftBaoNum := baoNum - needBaoNum //剩下可用於去拼 "萬"成整撲一將的癩子數量
        num := getBaoNumToZhengPuJiang(sptArr[1])
        if leftBaoNum >= num {
            return true
        }
    }

    // 吳名 2017/7/7 下午8:30 將在"筒"中
    needBaoNum = needBaoArr[1] + needBaoArr[3] + needBaoArr[4]
    if needBaoNum <= baoNum {
        leftBaoNum := baoNum - needBaoNum //剩下可用於去拼 "筒"成整撲一將的癩子數量
        num := getBaoNumToZhengPuJiang(sptArr[2])
        if leftBaoNum >= num {
            return true
        }
    }

    // 吳名 2017/7/7 下午8:31 將在"條"中
    needBaoNum = needBaoArr[1] + needBaoArr[2] + needBaoArr[4]
    if needBaoNum <= baoNum {
        leftBaoNum := baoNum - needBaoNum //剩下可用於去拼 "條"成整撲一將的癩子數量
        num := getBaoNumToZhengPuJiang(sptArr[3])
        if leftBaoNum >= num {
            return true
        }
    }
    // 吳名 2017/7/7 下午8:31 將在"風"中
    needBaoNum = needBaoArr[1] + needBaoArr[2] + needBaoArr[3]
    if needBaoNum <= baoNum {
        leftBaoNum := baoNum - needBaoNum //剩下可用於去拼 "風"成整撲一將的癩子數量
        num := getBaoNumToZhengPuJiang(sptArr[4])
        if leftBaoNum >= num {
            return true
        }
    }
    return false
}

/**
吳名
2017/7/12 下午3:05
判斷制飛是否成功:來任何一張牌都能胡(2,減去手上一張寶以後,剩下的牌造成整撲 1,減去手上一張寶以後,剩下的牌造成6對子)
*/
func zhiFei(arr []int, bao int) bool {
    mjArr := append([]int{}, arr...)

    //寶,萬,筒,條,風
    sptArr := seperateRuiJinArr(mjArr, bao)

    //1:  13張牌,7對的制飛
    if len(mjArr) == 13 {
        danNum := 0
        for i, arr := range sptArr {
            l := len(arr)
            switch i {
            case 0:
                //寶不須要去除對子
                if l == 4 || (bao > 500 && l == 3) {
                    log.Println("對對胡:全部的寶牌都在一家,飛")
                    return false
                }
            default:
                danArr, _ := separate2Same(arr)
                danNum += len(danArr)
            }
        }
        //4,寶數量>=剩下的單張數量(去掉一張寶後,12張牌造成了6對)
        if len(sptArr[0])-1 >= danNum {
            log.Println("對對胡:制飛成功")
            return true
        }
    }

    //2, 減掉一張寶以後,剩下牌造成整撲

    //檢測牌數量
    if len(mjArr)%3 != 1 {
        log.Println("牌數量不符合3n+1,不能飛")
        return false
    }

    needBaoNumToZhengPu := 0
    for i := 1; i <= 4; i++ {
        needBaoNumToZhengPu += getNeedBaoNumToZhengPu(sptArr[i])
    }
    if len(sptArr[0])-1 == needBaoNumToZhengPu {
        log.Println("3n+2胡:制飛成功")
        return true
    }
    return false
}

/**
吳名
2017/7/11 下午1:50
爛胡(在調用這個以前,已經排除了)
*/
func lanHu(arr []int, bao int) bool {

    mjArr := append([]int{}, arr...)

    //1,判斷牌長度,必須爲14
    if len(mjArr) != 14 {
        return false
    }
    //2,按類型分組
    //寶,萬,筒,條,風
    sptArr := seperateRuiJinArr(mjArr, bao)
    //3,萬筒條任意兩張牌的差必須>=3
    //4,風牌任何兩張牌不能相等
    for i, arr := range sptArr {
        l := len(arr)
        switch i {
        case 0:
            //寶不須要處理,只要萬筒條風符合要求,無論幾個寶,均可以配合
            if l == 4 || (bao > 500 && l == 3) {
                log.Println("爛胡:全部的寶牌都在一家,飛")
                return false
            }
        case 4:
            //風(任何兩張不能相等)
            for j, _ := range arr {
                if j <= l-2 && arr[j] == arr[j+1] {
                    log.Println("爛胡:風相等")
                    return false
                }
            }
        default:
            //萬筒條(差必須>=3),seperateRuiJinArr()返回的已是排序後的
            for k, _ := range arr {
                if k <= l-2 && arr[k+1]-arr[k] <= 2 {
                    log.Println("爛胡:萬筒條未跳兩張")
                    return false
                }
            }
        }
    }
    log.Println("符合爛胡")
    return true
}

func duiDuiHu(arr []int, bao int) bool {

    mjArr := append([]int{}, arr...)

    //1,判斷牌長度,必須爲14
    if len(mjArr) != 14 {
        return false
    }
    //2,按類型分組
    //寶,萬,筒,條,風
    sptArr := seperateRuiJinArr(mjArr, bao)
    //3,去掉全部對子,獲得單張數量
    danNum := 0
    for i, arr := range sptArr {
        l := len(arr)
        switch i {
        case 0:
            //寶不須要去除對子
            if l == 4 || (bao > 500 && l == 3) {
                log.Println("對對胡:全部的寶牌都在一家,飛")
                return false
            }
        default:
            danArr, _ := separate2Same(arr)
            danNum += len(danArr)
        }
    }
    //4,寶數量>=剩下的單張數量
    if len(sptArr[0]) >= danNum {
        return true
    }
    return false
}

/**
吳名
2017/7/8 下午5:01
萬,筒,條,風,成爲整撲一將須要的最少癩子數量
*/
func getBaoNumToZhengPuJiang(arr []int) int {
    if len(arr) <= 0 {
        //若是數組爲空,至少須要2個癩子組成一對將
        return 2
    }
    //尋找對子

    //吳名 2017/7/8 下午8:09 先去掉順子的影響
    t := arr[0] / 100 //萬筒條風類型
    a := []int{}
    switch t {
    case 4:
        a = separateFeng3Lian_ruiJin(arr)
    default:
        a, _ = separate3Lian(arr)
    }

    l := len(a)
    switch l {
    case 0:
        return 2
    case 1:
        return 1
    default:
        //多是一張牌,兩張牌,或3張牌
        for i, _ := range a {
            switch i {
            //只有2張牌,進入這裏,爲第一張
            case l - 2:
                if l == 2 && a[i] == a[i+1] {
                    //找到對子了
                    b := append(a[:i], a[i+2:]...)
                    return getNeedBaoNumToZhengPu(b)
                }
                //最後1張牌
            case l - 1:
                //到最後一張牌,尚未找到對子

                //此時的3同參與不了順子
                subArr := separate3Same(a)
                //2連不能拿去拼對子
                //吳名 2017/7/10 上午11:53 分離2連,再造成對子(101,104,105)
                canLianNum := 0
                switch t {
                case 4:
                    subArr, canLianNum = separateFeng2Lian_ruiJin(subArr)
                default:
                    subArr, canLianNum, _ = separate2Lian(subArr, -1, false)
                }

                switch len(subArr) {
                case 0:
                    return canLianNum + 2
                case 1:
                    return canLianNum + 1
                default:
                    //101,105,105,105,不能拆3同

                    return 1 + canLianNum + getNeedBaoNumToZhengPu(subArr[:len(subArr)-1]) //剩下的,最後1張牌拿去拼將去了(只能最後一張,101,101,101,104)
                }
            default:

                //101,104,104,104,107,不拆開3同(3同對造成順子無影響)

                //舉例總結:3同能參與造成順子時,拆掉3同須要癩子數<=不拆,3同不能參與造成順子時,3同利用不到,不能拆3同
                switch t {
                case 4:
                    if a[i] == a[i+2] {
                        //3同對子,可是對造成順子有影響(401,401,401,404)
                        if i >= 1 && (a[i] <= 404 || a[i-1] >= 405) {
                            //401,404,404,404 或 405,406,406,406
                            //3同能和前面造成順子或有影響(包括4同)
                            b := append(a[:i], a[i+2:]...)
                            return getNeedBaoNumToZhengPu(b)
                        }
                        if l-i >= 4 && (a[i+3] <= 404 || a[i] >= 405) {
                            //403,403,403,404 或 406,406,406,407

                            //3同能和後面造成順子或有影響(包括4同)
                            b := append(a[:i], a[i+2:]...)
                            return getNeedBaoNumToZhengPu(b)
                        }
                    } else {
                        //純對子(非3同中的對子)
                        if a[i] == a[i+1] {
                            if i == 0 || a[i-1] != a[i] {
                                b := append(a[:i], a[i+2:]...)
                                return getNeedBaoNumToZhengPu(b)
                            }
                        }
                    }
                default:
                    if a[i] == a[i+2] {
                        //3同對子,可是對造成順子有影響(101,103,103,103,105)
                        if i >= 1 && a[i]-a[i-1] <= 2 {
                            //3同能和前面造成順子或有影響(包括4同)
                            b := append(a[:i], a[i+2:]...)
                            return getNeedBaoNumToZhengPu(b)
                        }
                        if l-i >= 4 && a[i+3]-a[i+2] <= 2 {
                            //3同能和後面造成順子或有影響(包括4同)
                            b := append(a[:i], a[i+2:]...)
                            return getNeedBaoNumToZhengPu(b)
                        }
                    } else {
                        //純對子(非3同中的對子)
                        if a[i] == a[i+1] {
                            if i == 0 || a[i-1] != a[i] {
                                b := append(a[:i], a[i+2:]...)
                                fmt.Println("b:", b)
                                return getNeedBaoNumToZhengPu(b)
                            }
                        }
                    }
                }
            }
        }
    }
    return 0
}

/**
吳名
2017/7/6 下午7:05
萬,筒,條,風,成爲順子或者三連須要的癩子數量
*/
func getNeedBaoNumToZhengPu(subArr []int) int {
    length := len(subArr) //萬的張數
    switch length {
    case 0:
        return 0
    case 1:
        return 2
    case 2:
        t := subArr[0] / 100 //萬筒條風類型
        switch t {
        case 4:
            //風
            if subArr[1] <= 404 {
                //兩個都是東南西北(無論作順子或刻),東南西北任何三個也能夠互吃
                return 1
            }
            if subArr[0] >= 405 {
                //兩個都是中發白,中發白任何三個能夠互吃
                return 1
            }
            //一個東南西北,一個是中法白
            return 4
        default:
            //萬,筒,條
            d := subArr[1] - subArr[0] //subArr是已經通過排序的
            if d <= 2 {
                //1萬1萬,1萬2萬,1萬3萬
                return 1
            } else {
                //1萬4萬
                return 4
            }
        }
    default:
        //3張以上萬筒條或風
        //++++++++必須從小到大排序後,先去3同,再去3連,再去2同,再去2連,這些最容易造成整撲的去掉後,而後剩下牌兩個一組分割算須要癩子數,這才能獲得最少的癩子數量++++++++++
        //1,分離3同
        subArr = separate3Same(subArr)
        if len(subArr) <= 2 {
            //去除3同後剩餘牌數<=2,直接結束
            return getNeedBaoNumToZhengPu(subArr)
        }
        t := subArr[0] / 100 //萬筒條風類型
        switch t {
        case 4:
            //風
            //2,分離3連
            subArr = separateFeng3Lian_ruiJin(subArr)
            l := len(subArr)
            if l <= 2 {
                return getNeedBaoNumToZhengPu(subArr)
            } else {
                needCount := 0
                //3,分離2同
                subArr, duiZiNum := separate2Same(subArr)
                needCount += duiZiNum //有多少個對子就須要多少個癩子把它變整撲
                //3,分離2連
                subArr, canLianNum := separateFeng2Lian_ruiJin(subArr)
                needCount += canLianNum + getNeedBaoNumToZhengPu(subArr)
                return needCount
            }
        default:
            //萬或筒或條
            //2,分離3連
            subArr, _ = separate3Lian(subArr)
            l := len(subArr)
            if l <= 2 {
                return getNeedBaoNumToZhengPu(subArr)
            } else {
                needCount := 0
                //3,分離2同和2連(至關於只須要1癩子就能成的牌都去掉)
                subArr, canLianOrSameNum, _ := separate2LianAnd2Same(subArr, -1, false)
                needCount += canLianOrSameNum + getNeedBaoNumToZhengPu(subArr) //有多少個對子或2連就須要多少個癩子把它變整撲
                return needCount
            }
        }
    }
}

/**
吳名
2017/7/8 上午11:54
分離2連(風,適用):返回去除後的數組,以及2連數量(裏面3順子,對子必須提早已去除)
*/
func separateFeng2Lian_ruiJin(arr []int) ([]int, int) {
    is := false
    lianNum := 0

    l := len(arr)
    for i, _ := range arr {
        if arr[i] != 0 && i <= l-2 {
            if arr[i+1] <= 404 || arr[i] >= 405 {
                //1,東南西北三張互吃 2,中發白互吃(對子前一步已經去除,因此不可能相等)
                arr[i] = 0
                arr[i+1] = 0
                lianNum++
                is = true
                break
            }
        }
    }

    if is {
        //若是祛除過順子,那麼須要清洗0以後繼續祛除
        r := []int{}
        for _, v := range arr {
            if v != 0 {
                r = append(r, v)
            }
        }
        a, num := separateFeng2Lian_ruiJin(r)
        return a, lianNum + num
    } else {
        return arr, 0
    }
}

/**
吳名
2017/7/7 下午5:45
分離順子(針對風):東南西北任何三個也能夠互吃,中發白任何三個能夠互吃
*/
func separateFeng3Lian_ruiJin(arr []int) []int {
    is := false
    for i, _ := range arr {
        //前3張無對子的狀況下(401,402,403,404)
        if i <= len(arr)-3 {
            if arr[i+2] <= 404 && arr[i] != arr[i+1] && arr[i+1] != arr[i+2] {
                //連續3張都是中南西北,且三張各不相等,能夠互吃
                arr[i] = 0
                arr[i+1] = 0
                arr[i+2] = 0
                //log.Println("去除順子:%v", arr)
                is = true
                break
            }
            if arr[i] == 405 && arr[i+1] == 406 && arr[i+2] == 407 {
                //連續3張是中發白
                arr[i] = 0
                arr[i+1] = 0
                arr[i+2] = 0
                //log.Println("去除順子:%v", arr)
                is = true
                break
            }
        }
        //前3張有對子的狀況下(401,401,402,403,404)
        if i <= len(arr)-4 {
            if arr[i+3] <= 404 && arr[i] != arr[i+1] && arr[i+1] != arr[i+3] {
                arr[i] = 0
                arr[i+1] = 0
                arr[i+3] = 0
                //log.Println("去除順子:%v", arr)
                is = true
                break
            }
        }
    }
    if is {
        //若是祛除過順子,那麼須要清洗0以後繼續祛除
        r := []int{}
        for _, v := range arr {
            if v != 0 {
                r = append(r, v)
            }
        }
        return separateFeng3Lian_ruiJin(r)
    } else {
        return arr
    }
}

/**
吳名
2017/7/6 下午4:27
分割 寶,萬,筒,條,風
*/
func seperateRuiJinArr(mjArr []int, bao int) [5][]int {
    result := [5][]int{}
    for _, mj := range mjArr {
        index := mj / 100
        //寶是花牌
        if bao > 500 {
            switch index {
            case 5:
                //寶
                result[0] = append(result[0], mj)
            default:
                result[index] = append(result[index], mj)
            }
        } else {
            switch index {
            case 5:
                //此時花牌處理成寶的本位牌
                i := bao / 100
                result[i] = append(result[i], bao)
            default:
                if mj == bao {
                    //寶
                    result[0] = append(result[0], mj)
                } else {
                    result[index] = append(result[index], mj)
                }
            }
        }
    }
    //升序排列
    for _, arr := range result {
        sort.Sort(sort.IntSlice(arr))
    }
    return result
}

單元測試及性能測試:

func Test_lanHu(t *testing.T) {
    //arr := []int{101, 104, 104, 104, 104}
    arr := []int{101, 104, 104, 107, 201, 204, 209, 302, 305, 309, 403, 406, 409, 501}
    t.Logf("爛胡:%v", lanHu(arr, 104))
}

func Test_duiDuiHu(t *testing.T) {
    arr := []int{101, 101, 104, 104, 201, 201, 209, 209, 305, 306, 407, 407, 407, 407}
    t.Logf("對對胡:%v", duiDuiHu(arr, 407))
}

func Test_zhiFei(t *testing.T) {
    arr := []int{101, 101, 104, 104, 201, 201, 209, 209, 305, 306, 407, 407, 407}
    t.Logf("制飛:%v", zhiFei(arr, 407))
}

func Test_isHU(t *testing.T) {
    arr := []int{101, 101, 104, 104, 201, 201, 209, 209, 305, 306, 307, 407, 407, 407}
    t.Logf("胡:%v", isHU(arr, 407))
}
func Benchmark_isHU(b *testing.B) {

    for i := 0; i < b.N; i++ {
        arr := []int{101, 101, 104, 104, 201, 201, 209, 209, 305, 306, 307, 407, 407, 407}
        isHU(arr, 407)
        //b.Logf("胡:%v", isHU(arr, 407))
    }

    //b.RunParallel(func(pb *testing.PB) {
    //
    //    for pb.Next() {
    //        arr := []int{101, 101, 104, 104, 201, 201, 209, 209, 305, 306, 307, 407, 407, 407}
    //        isHU(arr, 407)
    //        //b.Logf("胡:%v", isHU(arr, 407))
    //    }
    //})
}

性能測試結果

1s=10^3ms(毫秒)=10^6μs(微秒)=10^9ns(納秒)
可胡牌:
arr := []int{101, 101, 104, 104, 201, 201, 209, 209, 305, 306, 307, 407, 407, 407}

可胡牌狀況下,一次判斷只須要4822ns,即一秒鐘能夠執行約20萬次判斷,若是牌數量更少時的n%3=2,則效率更高

pkg: dataStructures-algorithm-demo/麻將相關算法
300000          4822 ns/op
--- BENCH: Benchmark_isHU-8
    ruijinMjHu_test.go:95: 胡:true

不可胡牌時,效率比可胡牌的牌型效率略高一些

pkg: dataStructures-algorithm-demo/麻將相關算法
500000          3711 ns/op
--- BENCH: Benchmark_isHU-8
    ruijinMjHu_test.go:95: 胡:false

綜上,此算法不只效率很高,且通用性很強(任何類型玩法的麻將均可用)

完整代碼已上傳GitHub,歡迎star並提出優化建議,如發現bug,歡迎給予指正,但願和大神們共同進步

相關文章
相關標籤/搜索