性能最高的麻将算法 -- lua

关于这个算法的由来

今天是在公司写麻将算法的第四天,在第二天的时候已基本实现带N个癞子的麻将判胡算法,
但是,性能太垃圾了,运行1万次就需要大概3秒左右,也就是我上一篇文章提及的算法,想一下如果全国1000万人同时玩麻将,有100服务器,那么每秒需要判断1000万次是否胡牌,每台服务器每秒需要判断10万次,那么每个人需要在0-30秒后才能得到结果,虽然这个只是大概数据,但是显然不符合实际,所以算法要从根本思路去颠覆

新算法 -- 类二叉递归法

废话不多说(如果要让我用文字去描述这个算法,那不如让我去死)
为了让人更容易看懂,我使用了一个特例去画思维导图,结合代码运行,应该大家就可以慢慢理解到里面的神奇精妙之处

这个案例没有讲解带N个癞子的情况,但是我相信理解了这个图和代码之后,带N个癞子的情况也能慢慢理解~
好了,真的不废话了,上干货!


麻将算法核心思路xmind图解.png

再结合代码:


-- @曾帅 2018/7/6

---------------------思路(伪代码)-------------
--[=[
检查胡牌:
  local 对子 = 找对子
  if 对子 then
    移除对子(对子)
    result
    if count == 0 then return result end
    检查胡牌()
    加回对子(对子)
  end
  lcoal 顺子= 找顺子()
  if 顺子 then
    移除顺子(顺子)
    result
    if count == 0 then return result end
    检查胡牌()
    加回顺子(顺子)
  end
  lcoal 刻子= 找刻子()
  if 刻子 then
    移除刻子(刻子)
    检查胡牌()
    加回刻子(刻子)
  end
]=]

-- 定义胡牌成功的牌组数据结构
local result = {
    dui_zi = nil,
    ke_zi_array = {},
    shun_zi_array = {},
}

-- 辅助函数,递归打印表,以便观察结果和测试查错等
function PrintTable(tbl, level) -- level 为递归深度,默认为1
    if tbl == nil then return end
    local msg = ""
    level = level or 1
    local indent_str = "" -- 缩进
    for i = 1, level do
        indent_str = indent_str .. "  "
    end
    if level == 1 then
        print(indent_str .. "{")
    end
    for k, v in pairs(tbl) do
        local k_str = ""
        if type(k) == "string" then -- 键值为字符串,则["xxx"]
            k_str = string.format("[\"%s\"]", k)
        else
            k_str = string.format("[%s]", k)
        end
        if type(v) == "table" then
            local item_str = string.format("%s%s = {", indent_str .. "  ", k_str)
            print(item_str)
            PrintTable(v, level + 1)
        elseif type(v) == "number" then
            local item_str = string.format("%s%s = %s,", indent_str .. "  ", k_str, tostring(v))
            print(item_str)
        else
            local item_str = string.format("%s%s = \"%s\",", indent_str .. "  ", k_str, tostring(v))
            print(item_str)
        end
    end
    if level == 1 then
        print(indent_str .. "}")
    else
        print(indent_str .. "},")
    end
end

-- 在有序牌中找到第一个非个数为0的牌,最多需要遍历十次
function GetFirstNotZeroKey(t, index)
    for i = 1, 10 do
        if t[i] and t[i] > 0 then
            return i + index - 1
        end
        if i == 10 then
            if t[99] and t[99] > 0 then
                return 99
            end
        end
    end
end

-- 查找到ABC,记录ABC牌组
-- 查找到AB,癞子足够,记录牌组和一个癞子
-- 查找到AC,癞子足够,记录牌组和一个癞子
-- 没查找到 返回false
-- 优化的地方:这里的查找并不是真的查找
-- 先拿到牌组中第一个个数不为0的牌(first_k)
-- 再去找顺子,都是在一个table中找一个key对应的value
-- 在lua中,这种操作时间复杂度都是O(1)
-- 所以这个方法的时间复杂度为O(1),比之前的O(N^2)优化了许多
function CheckAbc(t)
    local first_k = GetFirstNotZeroKey(t, 1)
    
    local zu_he = {first_k}
    local lai_zi_count = t[99]
    for k, v in ipairs({first_k + 1, first_k + 2, 99, 99}) do
        if t[v] and t[v] > 0 then
            if v == 99 and lai_zi_count == 0 then
                break
            end
            table.insert(zu_he, v)
            if v == 99 then lai_zi_count = lai_zi_count - 1 end
            if #zu_he == 3 then break end
        end
    end
    
    return #zu_he == 3 and zu_he or false
end

-- 查找到AAA,记录AAA牌组
-- 查找到AA,癞子足够,记录牌组和一个癞子
-- 查找到A,癞子足够,记录牌组和两个癞子(特殊情况,癞子少的时候较少,癞子多的时候起优化作用)
-- 原理和上面一致
function Check3n(t)
    for k, v in pairs(t) do
        if v >= 3 then
            return {k, k, k}
        end
        if t[99] and t[99] >= 1 and v == 2 then
            return {k, k, 99}
        end
        if t[99] and t[99] >= 2 and v == 1 then
            return {k, 99, 99}
        end
        if t[99] and t[99] >= 3 and v == 0 then
            return {99, 99, 99}
        end
    end
end

-- 原理和上面一致
function Check2n(t)
    for k, v in pairs(t) do
        if v >= 2 then
            return {k, k}
        end
        if t[99] and t[99] >= 1 then
            return {GetFirstNotZeroKey(t, 1), 99}
        end
        if t[99] and t[99] >= 2 then
            return {99, 99}
        end
    end
end

-- 把t2的元素加到t中,只是单纯把对应key的value加1,时间复杂度为O(1)
function Insert(t, t1)
    for i, v in ipairs(t1) do
        t[v] = t[v] + 1
    end
    return t
end
-- 把t2的元素从t中移除,原理同上
function Remove(t, t1)
    for i, v in ipairs(t1) do
        t[v] = t[v] - 1
    end
    return t
end

-- 递归去除(看简书上面的图解~),这里的删除添加操作与上面原理一样,时间复杂度为O(1)
function Check(t)
    local res_2n = Check2n(t) -- 返回找到的对子
    if not result.dui_zi and res_2n then -- 如果找到对子,并且之前没有对子在记录中
        ---[[
        print("-------------find_2n-------------")
        PrintTable(res_2n)
        print("-------------删除前的t表-------------")
        PrintTable(t)
        --]]
        Remove(t, res_2n) -- 从牌组中移除对子
        ---[[
        print("-------------removed_2n-------------")
        PrintTable(t)
        --]]
        result.dui_zi = res_2n -- 记录好对子
        if not GetFirstNotZeroKey(t, 1) then return result end -- 如果移除完之后没有牌了,那么就返回结果
        if Check(t) then return result end -- 否则继续递归
        result.dui_zi = nil -- 清除对子
        Insert(t, res_2n) -- 还原对子到表中
        ---[[
        print("-------------insert_2n-------------")
        PrintTable(res_2n)
        print("-------------插入回去的t表-------------")
        PrintTable(t)
        --]]
    end
    
    -- 同上
    local res_abc = CheckAbc(t)
    if res_abc then
        ---[[
        print("-------------find_abc-------------")
        PrintTable(res_abc)
        print("-------------删除前的t表-------------")
        PrintTable(t)
        --]]
        Remove(t, res_abc)
        ---[[
        print("-------------removed_abc-------------")
        PrintTable(t)
        --]]
        table.insert(result.shun_zi_array, res_abc)
        if not GetFirstNotZeroKey(t, 1) then return result end
        if Check(t) then return result end
        table.remove(result.shun_zi_array)
        Insert(t, res_abc)
        ---[[
        print("-------------insert_abc-------------")
        PrintTable(res_abc)
        print("-------------插入回去t表-------------")
        PrintTable(t)
        --]]
    end
    
    -- 同上
    local res_3n = Check3n(t)
    if res_3n then
        ---[[
        print("-------------find_3n-------------")
        PrintTable(res_3n)
        print("-------------删除前的t表-------------")
        PrintTable(t)
        --]]
        Remove(t, res_3n)
        ---[[
        print("-------------removed_3n-------------")
        PrintTable(t)
        --]]
        table.insert(result.ke_zi_array, res_3n)
        if not GetFirstNotZeroKey(t, 1) then return result end
        if Check(t) then return result end
        table.remove(result.ke_zi_array)
        Insert(t, res_3n)
        ---[[
        print("-------------insert_3n-------------")
        PrintTable(res_3n)
        print("-------------插入回去的t表-------------")
        PrintTable(t)
        --]]
    end
    
    return false
end

-- 数出牌组中每个牌对应的个数,如牌是{1,1,1,3},则形成类似下面的表
--[[
{
    [1] = 3,
    [3] = 1,
}
--]]
function TTT(tab)
    local tabT = {}
    table.sort(tab, function(a, b)
        return a < b
    end)
    for key, v in pairs(tab) do
        if tabT[tab[key]] == nil then
            tabT[tab[key]] = 1
        else
            tabT[tab[key]] = tabT[tab[key]] + 1
        end
    end
    return tabT
end

-- 判断是否能胡牌
function CheckIsHu(t)
    if #t % 3 == 2 then -- 不符合基础规则(剔除大部分情况)
        -- 输出每张牌有多少个
        local count_t = {}
        count_t = TTT(t) -- 数表
        print("-------------------最开始的t表-----------------")
        PrintTable(count_t)
        
        local can_hu = Check(count_t) -- 递归验证
        if can_hu then
            print("-------------------最终胡牌组合-----------------")
            PrintTable(can_hu)
            return true
        end
        return false
    else
        return false
    end
end

print("--------------带N个赖子的胡牌算法-------------")

-- 提供测试的数据
-- local pai = {1, 99}
-- local pai = {1, 1, 99}
-- local pai = {1, 1, 2, 3, 99, 99, 99, 99}
-- local pai = {1, 3, 5, 8, 8, 99, 99, 99}
-- local pai = {1, 2, 3, 6, 8, 8, 8, 2, 3, 5, 5, 7, 7, 9, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99}
-- local pai = {1, 1, 1, 9, 4, 4, 5, 5, 6, 7, 7, 99, 99, 99} -- 复杂测试
-- local pai = {1, 3, 3, 3, 5, 7, 7, 99, 99, 99, 99}
-- local pai = {1, 2, 3, 5, 4, 99, 99, 99}
-- local pai = {1, 2, 3, 5, 4, 5, 5, 6}
-- local pai = {2, 3, 7, 9, 99, 99, 99, 99}
-- local pai = {1,1,2,3,99,99,99,99}
local pai = {1, 1, 2, 3, 4, 6, 6, 8}

-- 测试运行时间
local start_time = os.clock()
local res
res = CheckIsHu(pai)
local end_time = os.clock()

print(string.format("start_time   : %.6f", start_time))
print(string.format("end_time   : %.6f", end_time))
print(string.format("cost_time  : %.6f", end_time - start_time))

-- 结果
print("能否胡牌:" .. tostring(res))

好了,希望即将要去面试棋牌游戏公司的童鞋们用这个算法去征服你们的HR吧~
希望有更好的算法,愿意的话可以随时联系我 _
注 : 未经允许,请不要随意转载
有任何疑问或者是优化请联系我:
邮箱 511793781@qq.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 我们在操场上跑步,不过没跑完我们就停下来了。老师知道后不开心,让没跑完的继续跑。 我也不知道自己有没有跑完,所以索...
    爱梦的我阅读 144评论 0 0
  • 昨晚儿和爸爸说好,今早由爸爸送儿到学校,必须6点到校集中坐车去合肥。五点二十,儿仍在睡,只听爸爸喊儿起床。...
    大爱无疆杨青阅读 301评论 0 7
  • 没想到短短两年,好像彼此的心无法在靠近!
    迷雾m阅读 92评论 0 0
  • 我知道你们对秀恩爱没有兴趣,所以说一个秀抓老鼠技能的故事…… (真的是抓老鼠,看恶心了我不负责售后的) 起因……丁...
    是周日儿阅读 556评论 0 0
  • 这不知道是我多少次梦到自己在梦里飞了,这次还带了我妹妹。我们算是一个练武世家,我和妹妹又是这个家族中轻功内力最雄厚...
    爱梦的我阅读 201评论 0 0