麻将胡牌算法(遍历+剪枝)

这篇具有很好参考价值的文章主要介绍了麻将胡牌算法(遍历+剪枝)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

简介

这个算法采用以遍历为基础的方法对胡牌进行搜索,并加入了一定的剪枝策略。没有对算法复杂度和运行时间进行测试,但整体运行速度基本可观,基本剪去了大部分无用的搜索。这个算法是部署在我的个人网站上的,有兴趣的童鞋可以到我的个人网站去体验一下。因为框架原因,所以我使用TypeScript语言编写的,用JavaScript语言的童鞋可以稍加修改后服用。下面介绍算法的实现逻辑和代码。

麻将想要胡牌的话首先当前手中牌数肯定是除三余一的,也就是可能是1,4,7,10,13张牌。然后再加上一张胡牌的话总张数就是除三余二的,也就是可能是2,5,8,11,14张牌。除了十三幺七小对这两种特殊牌型外,胡牌的基本规则就是总牌数能够形成AA+mABC+nAAA这种牌型。也就是必须得有一个AA型牌型(叫做将),剩余的牌就可以每三个分为一组(此后将ABC与AAA型统称为三元组),然后检测是否能拆解成m个AAA和n个ABC牌型就可以判断是否能胡牌。

麻将胡牌算法及代码

1. 方法引入

该算法共引入了两个方法,分别为 deepClone 和 countItemInArray 。其中 deepClone 可以实现数组的深复制,由于我使用的vant框架带有这个函数便直接使用了,使用其他的深复制方法代替也可以。 countItemInArray 是自己封装的一个方法,用以统计一个元素在数组中出现的次数,后续计算胡牌需要用到,代码如下

import {deepClone} from "vant/es/utils/deep-clone";
import {countItemInArray} from "@/utils";

//from @/utils
export const countItemInArray = (array: Array<any>, item: any): number => {
    let count = 0
    for (let index = 0; index < array.length; index++) {
        if (item == array[index]) count++
    }
    return count
}

2. 类型定义

2.1 牌定义

将筒从一筒到九筒用数组分别定义为[11, 12, 13, 14, 15, 16, 17, 18, 19]
将条从一条到九条用数组分别定义为[21, 22, 23, 24, 25, 26, 27, 28, 29]
将萬从一萬到九萬用数组分别定义为[31, 32, 33, 34, 35, 36, 37, 38, 39]
将东、南、西、北、中、發、白,用数组分别定义为[41, 42, 43, 44, 45, 46, 47]
并将所有的牌按顺序构成数组,方便后面索引牌的特征使用

export const allTiles = [11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29,
    31, 32, 33, 34, 35, 36, 37, 38, 39, 41, 42, 43, 44, 45, 46, 47]

2.2 牌特征定义

定义牌的特征结构,结构包括是否能和周边的牌联系( isContactNear )、能够联系的牌( contactTiles )、牌的图片路径( imagePath ),方便后续计算使用

export type tileFeature = {
    isContactNear: boolean,
    contactTiles: number[],
    imagePath: string,
}

并将所有的牌按顺序构成牌特征数组,借助 allTiles 的索引来找到对应牌的特征

export const tilesFeature: tileFeature[] = [
    {isContactNear: true, contactTiles: [11, 11 + 1], imagePath: 'o1.png'},
    {isContactNear: true, contactTiles: [12 - 1, 12, 12 + 1], imagePath: 'o2.png'},
    {isContactNear: true, contactTiles: [13 - 1, 13, 13 + 1], imagePath: 'o3.png'},
    {isContactNear: true, contactTiles: [14 - 1, 14, 14 + 1], imagePath: 'o4.png'},
    {isContactNear: true, contactTiles: [15 - 1, 15, 15 + 1], imagePath: 'o5.png'},
    {isContactNear: true, contactTiles: [16 - 1, 16, 16 + 1], imagePath: 'o6.png'},
    {isContactNear: true, contactTiles: [17 - 1, 17, 17 + 1], imagePath: 'o7.png'},
    {isContactNear: true, contactTiles: [18 - 1, 18, 18 + 1], imagePath: 'o8.png'},
    {isContactNear: true, contactTiles: [19 - 1, 19], imagePath: 'o9.png'},
    {isContactNear: true, contactTiles: [21, 21 + 1], imagePath: 'l1.png'},
    {isContactNear: true, contactTiles: [22 - 1, 22, 22 + 1], imagePath: 'l2.png'},
    {isContactNear: true, contactTiles: [23 - 1, 23, 23 + 1], imagePath: 'l3.png'},
    {isContactNear: true, contactTiles: [24 - 1, 24, 24 + 1], imagePath: 'l4.png'},
    {isContactNear: true, contactTiles: [25 - 1, 25, 25 + 1], imagePath: 'l5.png'},
    {isContactNear: true, contactTiles: [26 - 1, 26, 26 + 1], imagePath: 'l6.png'},
    {isContactNear: true, contactTiles: [27 - 1, 27, 27 + 1], imagePath: 'l7.png'},
    {isContactNear: true, contactTiles: [28 - 1, 28, 28 + 1], imagePath: 'l8.png'},
    {isContactNear: true, contactTiles: [29 - 1, 29], imagePath: 'l9.png'},
    {isContactNear: true, contactTiles: [31, 31 + 1], imagePath: 'w1.png'},
    {isContactNear: true, contactTiles: [32 - 1, 32, 32 + 1], imagePath: 'w2.png'},
    {isContactNear: true, contactTiles: [33 - 1, 33, 33 + 1], imagePath: 'w3.png'},
    {isContactNear: true, contactTiles: [34 - 1, 34, 34 + 1], imagePath: 'w4.png'},
    {isContactNear: true, contactTiles: [35 - 1, 35, 35 + 1], imagePath: 'w5.png'},
    {isContactNear: true, contactTiles: [36 - 1, 36, 36 + 1], imagePath: 'w6.png'},
    {isContactNear: true, contactTiles: [37 - 1, 37, 37 + 1], imagePath: 'w7.png'},
    {isContactNear: true, contactTiles: [38 - 1, 38, 38 + 1], imagePath: 'w8.png'},
    {isContactNear: true, contactTiles: [39 - 1, 39], imagePath: 'w9.png'},
    {isContactNear: false, contactTiles: [41], imagePath: 'fe.png'},
    {isContactNear: false, contactTiles: [42], imagePath: 'fs.png'},
    {isContactNear: false, contactTiles: [43], imagePath: 'fw.png'},
    {isContactNear: false, contactTiles: [44], imagePath: 'fn.png'},
    {isContactNear: false, contactTiles: [45], imagePath: 'fz.png'},
    {isContactNear: false, contactTiles: [46], imagePath: 'ff.png'},
    {isContactNear: false, contactTiles: [47], imagePath: 'fb.png'},
]

3. 计算胡牌

将当前手中的牌排序后输入胡牌计算算法( mahjongCalculate 函数输入的牌已经是经过排序的),首先对手牌检测是否为十三幺和七小对这种特殊牌型,若不是特殊牌型则将其输入普通牌型胡牌计算算法得到胡牌。

export const mahjongCalculate = (tiles: number[]): number[] => {
    let checkThirteenYaoResult = checkThirteenYao(deepClone(tiles))
    if (checkThirteenYaoResult.length > 0) {
        return checkThirteenYaoResult
    }
    let checkSevenPairResult = checkSevenPair(deepClone(tiles))
    if (checkSevenPairResult.length) {
        return checkSevenPairResult
    }
    return checkCommon(deepClone(tiles))
}

3.1 检测十三幺牌型

十三幺牌型为固定牌型,胡牌也是固定胡牌,检测非常方便,只要检测输入手牌是否和十三幺牌型一致即可

const checkThirteenYao = (tiles: number[]): number[] => {
    const thirteenYao: number[] = [11, 19, 21, 29, 31, 39, 41, 42, 43, 44, 45, 46, 47]
    if (tiles.toString() == thirteenYao.toString()) {
        return thirteenYao
    } else {
        return []
    }
}

3.2 检测七小对牌型

七小对牌型也相对较为固定,牌型为6AA+B,胡牌为B。这里采用双指针法来遍历手牌,前后指针始终错位1张牌。如果检测到配对则指针全部前进两张牌,若未配对则指针全部前进一个并记录下胡牌。最终对配对数和胡牌数加以校验即可得到结果

const checkSevenPair = (tiles: number[]): number[] => {
    let result: number[] = []
    if (tiles.length < 13) return result
    tiles.push(0)
    let pairCount = 0
    let startPoint = 0
    let endPoint = 1
    let huTiles: number[] = []
    while (endPoint < 14) {
        if (tiles[startPoint] == tiles[endPoint]) {
            pairCount++
            startPoint += 2
            endPoint += 2
        } else {
            huTiles.push(tiles[startPoint])
            startPoint += 1
            endPoint += 1
        }
    }
    if (pairCount == 6 && huTiles.length == 1) {
        result = huTiles
    }
    return result
}

3.3 检测普通牌型胡牌

普通牌型胡牌计算算法需要输入当前手牌,根据当前手牌检测所有可能的胡牌,并逐个判断可能的胡牌是否为真的胡牌,若是真的胡牌则加入胡牌结果并最终返回结果。

const checkCommon = (tiles: number[]): number[] => {
    let possibleHuTiles: number[] = []
    for (let i = 0; i < tiles.length; i++) {
        let currentTile = tiles[i]
        let currentPossibleHuTiles = tilesFeature[allTiles.indexOf(currentTile)].contactTiles
        for (let j = 0; j < currentPossibleHuTiles.length; j++) {
            let currentPossibleHuTile = currentPossibleHuTiles[j]
            if (!possibleHuTiles.includes(currentPossibleHuTile)) {
                possibleHuTiles.push(currentPossibleHuTile)
            }
        }
    }

    let huTiles: number[] = []
    for (let i = 0; i < possibleHuTiles.length; i++) {
        let checkTiles = deepClone(tiles)
        checkTiles.push(possibleHuTiles[i])
        checkTiles.sort((a, b) => (a - b))
        if (checkIsHu(checkTiles)) {
            huTiles.push(possibleHuTiles[i])
        }
    }
    return huTiles
}
3.3.1 检测所有可能的胡牌

所有可能的胡牌一定是每张手牌的可以联系到的牌的总和(以此为条件进行初步的剪枝),每张牌可以联系到的牌可以从 tilesFeature 中当前牌的 contactTiles 获得。构建一个数组用来存放可能的胡牌结果,遍历当前手牌,对于当前牌可以联系到的牌,若不在可能的胡牌数组中则将其添加,以此来避免重复添加

let possibleHuTiles: number[] = []
for (let i = 0; i < tiles.length; i++) {
    let currentTile = tiles[i]
    let currentPossibleHuTiles = tilesFeature[allTiles.indexOf(currentTile)].contactTiles
    for (let j = 0; j < currentPossibleHuTiles.length; j++) {
        let currentPossibleHuTile = currentPossibleHuTiles[j]
        if (!possibleHuTiles.includes(currentPossibleHuTile)) {
            possibleHuTiles.push(currentPossibleHuTile)
        }
    }
}
3.3.2 检测可能的胡牌是否为真的胡牌

遍历所有可能的胡牌,将可能的胡牌添加到手牌中,添加后需要对手牌数组重新排序,随后送入胡牌检测器检测能否构成胡牌。此处在遍历过程中需要先对手牌使用深复制( deepClone )复制一份,随后再添加可能的胡牌并送入胡牌计算器,以免修改原数组导致结果出错。

let huTiles: number[] = []
for (let i = 0; i < possibleHuTiles.length; i++) {
    let checkTiles = deepClone(tiles)
    checkTiles.push(possibleHuTiles[i])
    checkTiles.sort((a, b) => (a - b))
    if (checkIsHu(checkTiles)) {
        huTiles.push(possibleHuTiles[i])
    }
}
检测牌型能否为胡牌(核心)

经过添加可能的胡牌后,当前手牌张数应满足除三余二,即可能是2,5,8,11,14张牌。此时若需满足胡牌条件,则必定需要有且仅有一对将牌。所以我们需要找到手牌中可以作为将牌的牌( findPossiblePairs ),并遍历可能的将牌,将手牌中的将牌剔除,随后送去检测是否能进行三元组拆解,若能完成拆解则该牌型可以胡牌,否则不能胡牌。此处在遍历过程中需要先对手牌使用深复制( deepClone )复制一份,随后再剔除将牌,以免修改原数组导致结果出错。

const checkIsHu = (tiles: number[]): boolean => {
    let possiblePairs: number[] = findPossiblePairs(tiles)
    for (let i = 0; i < possiblePairs.length; i++) {
        let checkTiles = deepClone(tiles)
        checkTiles.splice(checkTiles.indexOf(possiblePairs[i]), 2)
        if (checkTriples(checkTiles)) return true
    }
    return false
}
(1)查找可能的将牌

此处采用类似于检测七小对的算法,也即双指针法。若遇到配对则在查重之后加入可能的将牌数组。

const findPossiblePairs = (tiles: number[]): number[] => {
    let possiblePairs: number[] = []
    let startPoint = 0
    let endPoint = 1
    while (endPoint < tiles.length) {
        if (tiles[startPoint] == tiles[endPoint]) {
            if (!possiblePairs.includes(tiles[startPoint])) possiblePairs.push(tiles[startPoint])
            startPoint += 2
            endPoint += 2
        } else {
            startPoint += 1
            endPoint += 1
        }
    }
    return possiblePairs
}
(2)检测是否能每三张牌进行拆解(重点)

此处采用递归来迭代拆解三元组,消除一个三元组后进入更深一层的消除,递归终止条件为手牌为空。递归内容如下

  1. 检测递归终止条件
  2. 创建存放可能的三元组的数组,同时创建一个字符串类型的数组用以判断重复
  3. 从输入的手牌创建一个无重复牌的数组,以此来避免重复的牌反复送入检测三元组,实现剪枝的目的
  4. 循环将无重复牌的手牌数组的每个牌及原输入的手牌数组送入 findPossibleTriplesOfTile 进行检测,对每个检测到的结果判断不与存放可能的三元组数组重复后,将其添加到数组中,检测重复可以根据字符串型的数组和三元组转换成字符串进行判断。通过检测重复再次实现剪枝的目的。
  5. 遍历可能的三元组数组,剔除手牌中的该三元组,剔除后再次送入 checkTriples 函数进行递归。此处在遍历过程中需要先对手牌使用深复制( deepClone )复制一份,随后再剔除三元组,以免修改原数组导致结果出错。
const checkTriples = (tilesWithoutPair: number[]): boolean => {
    if (tilesWithoutPair.length == 0) return true
    let allPossibleTriples: Array<number[]> = []
    let allPossibleTriplesString: string[] = []
    let unRepeatTiles = Array.from(new Set(tilesWithoutPair))
    for (let i = 0; i < unRepeatTiles.length; i++) {
        let possibleTriplesOfTile: Array<number[]> = findPossibleTriplesOfTile(tilesWithoutPair, unRepeatTiles[i])
        for (let j = 0; j < possibleTriplesOfTile.length; j++) {
            let possibleTriple = possibleTriplesOfTile[j]
            if (!allPossibleTriplesString.includes(possibleTriple.toString())) {
                allPossibleTriples.push(possibleTriple)
                allPossibleTriplesString.push(possibleTriple.toString())
            }
        }
    }
    for (let i = 0; i < allPossibleTriples.length; i++) {
        let possibleTriple = allPossibleTriples[i]
        let checkTilesWithoutPair = deepClone(tilesWithoutPair)
        for (let k = 0; k < 3; k++) {
            checkTilesWithoutPair.splice(checkTilesWithoutPair.indexOf(possibleTriple[k]), 1)
        }
        if (checkTriples(checkTilesWithoutPair)) return true
    }
    return false
}
(3)查找当前牌在手牌中可能的拆解牌型(重点)

该算法需要输入待消除三元组的手牌和一张手牌中的牌来检测传入牌在手牌中可能构成的三元组,构建数组来存放传入牌可能的三元组。
若传入的牌能够和附近的牌联系(即筒、条、萬),则开始匹配ABC型的三元组。其中若牌面数字为1或9,则只有一种情况,即123或789。若牌面数字为2或8,则各有两种情况,即123和234或678和789。其他情况下则均有三种情况,即[n-2, n-1, n], [n-1, n, n+1], [n, n+1, n+2]
随后匹配AAA型的三元组,使用 countItemInArray 方法来统计传入牌在手牌中出现的次数,如果至少有三张,则加入可能的三元组数组。

const findPossibleTriplesOfTile = (tilesWithoutPair: number[], tile: number): Array<number[]> => {
    let possibleTriples: Array<number[]> = []
    if (tilesFeature[allTiles.indexOf(tile)].isContactNear) {
        if (tile % 10 == 1) {
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        } else if (tile % 10 == 2) {
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        } else if (tile % 10 == 8) {
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
        } else if (tile % 10 == 9) {
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
        } else {
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        }
    }
    if (countItemInArray(tilesWithoutPair, tile) >= 3) possibleTriples.push([tile, tile, tile])
    return possibleTriples
}

完整代码

其中 deepClone 方法可以用其他深复制方法替换, countItemInArray 方法在方法引入出有详细定义。

import {deepClone} from "vant/es/utils/deep-clone";
import {countItemInArray} from "@/utils";

export const allTiles = [11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29,
    31, 32, 33, 34, 35, 36, 37, 38, 39, 41, 42, 43, 44, 45, 46, 47]
export type tileFeature = {
    isContactNear: boolean,
    contactTiles: number[],
    imagePath: string,
}
export const tilesFeature: tileFeature[] = [
    {isContactNear: true, contactTiles: [11, 11 + 1], imagePath: 'o1.png'},
    {isContactNear: true, contactTiles: [12 - 1, 12, 12 + 1], imagePath: 'o2.png'},
    {isContactNear: true, contactTiles: [13 - 1, 13, 13 + 1], imagePath: 'o3.png'},
    {isContactNear: true, contactTiles: [14 - 1, 14, 14 + 1], imagePath: 'o4.png'},
    {isContactNear: true, contactTiles: [15 - 1, 15, 15 + 1], imagePath: 'o5.png'},
    {isContactNear: true, contactTiles: [16 - 1, 16, 16 + 1], imagePath: 'o6.png'},
    {isContactNear: true, contactTiles: [17 - 1, 17, 17 + 1], imagePath: 'o7.png'},
    {isContactNear: true, contactTiles: [18 - 1, 18, 18 + 1], imagePath: 'o8.png'},
    {isContactNear: true, contactTiles: [19 - 1, 19], imagePath: 'o9.png'},
    {isContactNear: true, contactTiles: [21, 21 + 1], imagePath: 'l1.png'},
    {isContactNear: true, contactTiles: [22 - 1, 22, 22 + 1], imagePath: 'l2.png'},
    {isContactNear: true, contactTiles: [23 - 1, 23, 23 + 1], imagePath: 'l3.png'},
    {isContactNear: true, contactTiles: [24 - 1, 24, 24 + 1], imagePath: 'l4.png'},
    {isContactNear: true, contactTiles: [25 - 1, 25, 25 + 1], imagePath: 'l5.png'},
    {isContactNear: true, contactTiles: [26 - 1, 26, 26 + 1], imagePath: 'l6.png'},
    {isContactNear: true, contactTiles: [27 - 1, 27, 27 + 1], imagePath: 'l7.png'},
    {isContactNear: true, contactTiles: [28 - 1, 28, 28 + 1], imagePath: 'l8.png'},
    {isContactNear: true, contactTiles: [29 - 1, 29], imagePath: 'l9.png'},
    {isContactNear: true, contactTiles: [31, 31 + 1], imagePath: 'w1.png'},
    {isContactNear: true, contactTiles: [32 - 1, 32, 32 + 1], imagePath: 'w2.png'},
    {isContactNear: true, contactTiles: [33 - 1, 33, 33 + 1], imagePath: 'w3.png'},
    {isContactNear: true, contactTiles: [34 - 1, 34, 34 + 1], imagePath: 'w4.png'},
    {isContactNear: true, contactTiles: [35 - 1, 35, 35 + 1], imagePath: 'w5.png'},
    {isContactNear: true, contactTiles: [36 - 1, 36, 36 + 1], imagePath: 'w6.png'},
    {isContactNear: true, contactTiles: [37 - 1, 37, 37 + 1], imagePath: 'w7.png'},
    {isContactNear: true, contactTiles: [38 - 1, 38, 38 + 1], imagePath: 'w8.png'},
    {isContactNear: true, contactTiles: [39 - 1, 39], imagePath: 'w9.png'},
    {isContactNear: false, contactTiles: [41], imagePath: 'fe.png'},
    {isContactNear: false, contactTiles: [42], imagePath: 'fs.png'},
    {isContactNear: false, contactTiles: [43], imagePath: 'fw.png'},
    {isContactNear: false, contactTiles: [44], imagePath: 'fn.png'},
    {isContactNear: false, contactTiles: [45], imagePath: 'fz.png'},
    {isContactNear: false, contactTiles: [46], imagePath: 'ff.png'},
    {isContactNear: false, contactTiles: [47], imagePath: 'fb.png'},
]
export const getMahjongImageUrl = (tile: number) => {
    if (allTiles.includes(tile)) {
        return new URL(`/src/assets/service/mahjong/${tilesFeature[allTiles.indexOf(tile)].imagePath}`, import.meta.url).href
    } else {
        return new URL('/src/assets/service/mahjong/fk.png', import.meta.url).href
    }
}
export const mahjongCalculate = (tiles: number[]): number[] => {
    let checkThirteenYaoResult = checkThirteenYao(deepClone(tiles))
    if (checkThirteenYaoResult.length > 0) {
        return checkThirteenYaoResult
    }
    let checkSevenPairResult = checkSevenPair(deepClone(tiles))
    if (checkSevenPairResult.length) {
        return checkSevenPairResult
    }
    return checkCommon(deepClone(tiles))
}

const checkThirteenYao = (tiles: number[]): number[] => {
    const thirteenYao: number[] = [11, 19, 21, 29, 31, 39, 41, 42, 43, 44, 45, 46, 47]
    if (tiles.toString() == thirteenYao.toString()) {
        return thirteenYao
    } else {
        return []
    }
}
const checkSevenPair = (tiles: number[]): number[] => {
    let result: number[] = []
    if (tiles.length < 13) return result
    tiles.push(0)
    let pairCount = 0
    let startPoint = 0
    let endPoint = 1
    let huTiles: number[] = []
    while (endPoint < 14) {
        if (tiles[startPoint] == tiles[endPoint]) {
            pairCount++
            startPoint += 2
            endPoint += 2
        } else {
            huTiles.push(tiles[startPoint])
            startPoint += 1
            endPoint += 1
        }
    }
    if (pairCount == 6 && huTiles.length == 1) {
        result = huTiles
    }
    return result
}
const checkCommon = (tiles: number[]): number[] => {
    let possibleHuTiles: number[] = []
    for (let i = 0; i < tiles.length; i++) {
        let currentTile = tiles[i]
        let currentPossibleHuTiles = tilesFeature[allTiles.indexOf(currentTile)].contactTiles
        for (let j = 0; j < currentPossibleHuTiles.length; j++) {
            let currentPossibleHuTile = currentPossibleHuTiles[j]
            if (!possibleHuTiles.includes(currentPossibleHuTile)) {
                possibleHuTiles.push(currentPossibleHuTile)
            }
        }
    }

    let huTiles: number[] = []
    for (let i = 0; i < possibleHuTiles.length; i++) {
        let checkTiles = deepClone(tiles)
        checkTiles.push(possibleHuTiles[i])
        checkTiles.sort((a, b) => (a - b))
        if (checkIsHu(checkTiles)) {
            huTiles.push(possibleHuTiles[i])
        }
    }
    return huTiles
}
const checkIsHu = (tiles: number[]): boolean => {
    let possiblePairs: number[] = findPossiblePairs(tiles)
    for (let i = 0; i < possiblePairs.length; i++) {
        let checkTiles = deepClone(tiles)
        checkTiles.splice(checkTiles.indexOf(possiblePairs[i]), 2)
        if (checkTriples(checkTiles)) return true
    }
    return false
}
const findPossiblePairs = (tiles: number[]): number[] => {
    let possiblePairs: number[] = []
    let startPoint = 0
    let endPoint = 1
    while (endPoint < tiles.length) {
        if (tiles[startPoint] == tiles[endPoint]) {
            if (!possiblePairs.includes(tiles[startPoint])) possiblePairs.push(tiles[startPoint])
            startPoint += 2
            endPoint += 2
        } else {
            startPoint += 1
            endPoint += 1
        }
    }
    return possiblePairs
}
const checkTriples = (tilesWithoutPair: number[]): boolean => {
    if (tilesWithoutPair.length == 0) return true
    let allPossibleTriples: Array<number[]> = []
    let allPossibleTriplesString: string[] = []
    let unRepeatTiles = Array.from(new Set(tilesWithoutPair))
    for (let i = 0; i < unRepeatTiles.length; i++) {
        let possibleTriplesOfTile: Array<number[]> = findPossibleTriplesOfTile(tilesWithoutPair, unRepeatTiles[i])
        for (let j = 0; j < possibleTriplesOfTile.length; j++) {
            let possibleTriple = possibleTriplesOfTile[j]
            if (!allPossibleTriplesString.includes(possibleTriple.toString())) {
                allPossibleTriples.push(possibleTriple)
                allPossibleTriplesString.push(possibleTriple.toString())
            }
        }
    }
    for (let i = 0; i < allPossibleTriples.length; i++) {
        let possibleTriple = allPossibleTriples[i]
        let checkTilesWithoutPair = deepClone(tilesWithoutPair)
        for (let k = 0; k < 3; k++) {
            checkTilesWithoutPair.splice(checkTilesWithoutPair.indexOf(possibleTriple[k]), 1)
        }
        if (checkTriples(checkTilesWithoutPair)) return true
    }
    return false
}
const findPossibleTriplesOfTile = (tilesWithoutPair: number[], tile: number): Array<number[]> => {
    let possibleTriples: Array<number[]> = []
    if (tilesFeature[allTiles.indexOf(tile)].isContactNear) {
        if (tile % 10 == 1) {
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        } else if (tile % 10 == 2) {
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        } else if (tile % 10 == 8) {
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
        } else if (tile % 10 == 9) {
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
        } else {
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        }
    }
    if (countItemInArray(tilesWithoutPair, tile) >= 3) possibleTriples.push([tile, tile, tile])
    return possibleTriples
}

效果展示

十三幺 七小对 九莲宝灯
麻将胡牌算法(遍历+剪枝) 麻将胡牌算法(遍历+剪枝) 麻将胡牌算法(遍历+剪枝)
普通牌型1 普通牌型2 普通牌型3
麻将胡牌算法(遍历+剪枝) 麻将胡牌算法(遍历+剪枝) 麻将胡牌算法(遍历+剪枝)

总结

这个算法核心思想就是借助递归来搜索胡牌,借助一些剪枝的操作来优化,从而快速地计算出胡牌。

做这个算法的起因是过年回家的时候和亲友小聚一下打打麻将,但奈何有初学的亲友容易看不明白胡牌。如果有人听牌了的话还方便帮忙看一下,但如果最早听牌的话就尴尬了,可能人都等困了他还是看不出来到底胡个啥。鉴于这种尴尬场景,再加上最近正好在学Web开发并在搭建自己的网站,就想到写这么一个胡牌计算器,方便亲友使用。本来想网上找个现成的胡牌算法的,但有的是没有东南西北中发白这些的,也有的注解不太清楚看起来挺头皮发麻的。最终还是决定自己写一个得了,毕竟胡牌规则并不复杂还算好写。

有童鞋想体验的话在我的个人网站上有我做的服务,非会员登录就可以使用啦,谢谢支持。文章来源地址https://www.toymoban.com/news/detail-501633.html

到了这里,关于麻将胡牌算法(遍历+剪枝)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 面试算法47:二叉树剪枝

    一棵二叉树的所有节点的值要么是0要么是1,请剪除该二叉树中所有节点的值全都是0的子树。例如,在剪除图8.2(a)中二叉树中所有节点值都为0的子树之后的结果如图8.2(b)所示。 下面总结什么样的节点可以被删除。首先,这个节点的值应该是0。其次,如果它有子树,那

    2024年02月06日
    浏览(28)
  • 秒懂算法 | 围棋中的Alpha-Beta剪枝算法

      极小化极大算法会遍历所有的可能性,但是根据经验可以知道,并不是所有的选项都需要进行深入的考虑,存在着某些明显不利的选项,当出现这种选项时就可以换一种思路进行考虑了。Alpha-Beta剪枝算法的出现正是为了减少极小化极大算法搜索树的节点数。1997年5月11日,

    2024年02月12日
    浏览(27)
  • 【人工智能】—局部搜索算法、爬山法、模拟退火、局部剪枝、遗传算法

    在某些规模太大的问题状态空间内,A*往往不够用 问题空间太大了 无法访问 f 小于最优的所有状态 通常,甚至无法储存整个边缘队列 解决方案 设计选择更好的启发式函数 Greedy hill-climbing (fringe size = 1) Beam search (limited fringe size) 瓶颈:内存不足,无法存储整个边缘队列 爬山搜

    2023年04月22日
    浏览(38)
  • [100天算法】-二叉树剪枝(day 48)

    用【产品经理法】的思维来解决递归问题。 产品 假设我们已经有了一个  pruneTree  方法,可以把一棵树中不包含  1  的枝节删掉。 子问题 明显是  pruneTree(root.left)  和  pruneTree(root.right) 。 大小问题的关系 首先,对于  root ,我们用  pruneTree(root.left)  和  pruneTree(root.righ

    2024年02月07日
    浏览(18)
  • 【算法】递归、回溯、剪枝、dfs 算法题练习(组合、排列、总和问题;C++)

    后面的练习是接着下面链接中的文章所继续的,在对后面的题练习之前,可以先将下面的的文章进行了解👇: 【算法】{画决策树 + dfs + 递归 + 回溯 + 剪枝} 解决排列、子集问题(C++) 思路 题意分析 :要求根据给出的数字,算出合法的括号组成个数。根据题目,我们可以总

    2024年02月22日
    浏览(38)
  • 罗勇军 →《算法竞赛·快冲300题》每日一题:“游泳” ← DFS+剪枝

    【题目来源】 http://oj.ecustacm.cn/problem.php?id=1753 http://oj.ecustacm.cn/viewnews.php?id=1023 【题目描述】 游泳池可以等分为n行n列的小区域,每个区域的温度不同。 小明现在在要从游泳池的左上角(1, 1)游到右下角(n, n),小明只能向上下左右四个方向游,不能游出泳池。 而小明对温度十分

    2024年02月10日
    浏览(28)
  • 【算法心得】正确估计dfs时间复杂度;剪枝优化不怕重构

    https://leetcode.cn/problems/verbal-arithmetic-puzzle/ 这题看到题,“表达式中使用的不同字符数最大为 10”,就觉得dfs就完事了,最多不过10!,10!才1e6,1e7这样。如果字符再少点,6! 7! 8!的,那简直就是嗖的一下就跑完了 结果TLE了 比方说,有7个字符,不是想象中的 7!,而是 10*9*...*4 ,

    2024年02月12日
    浏览(34)
  • 算法沉淀——穷举、暴搜、深搜、回溯、剪枝综合练习一(leetcode真题剖析)

    题目链接:https://leetcode.cn/problems/permutations/ 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1: 示例 2: 示例 3: 提示: 1 = nums.length = 6 -10 = nums[i] = 10 nums 中的所有整数 互不相同 思路 这是一个典型的回溯问题,需要在每

    2024年02月21日
    浏览(37)
  • 基于深度学习设计AI麻将程序

    国标麻将是麻将的一种玩法,其规则为中国国家体育总局于 1998 年 7 月所制定,其后在众多国际及国内麻将竞赛中应用。国标麻将为了增加竞技性、减少随机成分,将番种增加至 81 种,并设置为 8 番起和。 由于麻将的随机成分大,且往往具有赌博性质,麻将竞技的普及程度

    2024年02月09日
    浏览(30)
  • 【YOLOv7/YOLOv5系列算法改进NO.49】模型剪枝、蒸馏、压缩

    作为当前先进的深度学习目标检测算法YOLOv7,已经集合了大量的trick,但是还是有提高和改进的空间,针对具体应用场景下的检测难点,可以不同的改进方法。此后的系列文章,将重点对YOLOv7的如何改进进行详细的介绍,目的是为了给那些搞科研的同学需要创新点或者搞工程

    2024年02月08日
    浏览(28)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包