zoukankan      html  css  js  c++  java
  • 洗牌算法

    洗牌算法或者说随机乱序算法在很多情形下可以应用到,比如棋牌游戏,歌曲乱序等等。

    对于棋牌游戏,我们希望在发牌时,每个玩家发到的牌的情况都是差不多的,不会一直特别好的牌,也不会一直特别差的牌。更准确的说,对于每张牌,我们希望这张牌出现在牌组中不同的位置上是等概率的。

    ​1​ Fisher-Yates

    Fisher-Yates 洗牌算法最初由 Ronald Fisher 和 Frank Yates 1938 年发表在他们的著书《Statistical tables for biological, agricultural and medical research》中。算法描述把数字 1 - N 随机打乱的步骤是:

    1. 准备写下两个序列:未决序列 {1,2, .. N}  和已决序列 (初始状态为空){}。;

    2. 从 1 到未决序列的长度间选择一个数字 k;

    3. 将未决序列的第 k 个数移至已决序列尾端;

    4. 重复步骤 2 直至未决序列为空;

    5. 已决序列即打乱后的序列。

    假设现在需要打乱数字 1 - 8,于是写下两个序列:

    未决序列

    已决序列

    {1,2,3,4,5,6,7,8}

    {}

    现在未决序列的长度为 8,所以从 1 - 8 中随机选一个 k 值 3,将未决序列的第 3 个数(现在是 3)移至已决序列:

    未决序列

    已决序列

    {1,2,3,4,5,6,7,8}

    {3}

    现在未决序列的长度为 7,所以从 1 - 7 中随机选一个 k 值 6,将未决序列的第 6 个数(现在是 7)移至已决序列:

    未决序列

    已决序列

    {1,2,3,4,5,6,7,8}

    {3,7}

    重复这个步骤,直到未决序列为空,我们得到了一个随机的排列——已决序列:


    未决序列

    已决序列

    {1,2,3,4,5,6,7,8}

    {3,7,2,5,8,1,6,4}

     

    如果用伪代码来描述这个算法可以稍微“聪明”点,因为数组可以就地交换,不需要维护两个数组:

    -- To shuffle an array a of n elements (indices 0..n-1):
    for i from n−1 downto 1 do
         j ← random integer such that 0 ≤ j ≤ i
         exchange a[j] and a[i]

    或者换个方向:

    -- To shuffle an array a of n elements (indices 0..n-1):
    for i from 0 to n−2 do
         j ← random integer such that i ≤ j < n
         exchange a[i] and a[j]

    在这个算法中,因为只使用了一个数组的空间,所以空间复杂度是 O(n)。只遍历一次就能打乱数组,时间复杂度是 O(n)。

    ​2​ 概率分析

    Fisher-Yates 算法其实可以等效为不放回抽样模型。不放回抽样即在逐个抽取个体时,每次被抽到的个体不放回总体中参加下一次抽取的方法。Fisher-Yates 算法随机选中的元素同样是不放回的,选中即确定在排列中的位置。

    不放回抽样有个特性:每个抽样的元素在被抽取的次序上是等概率的。所以经过 Fisher-Yates 算法后,每个元素在所有位置上的概率也是一样的。

    我们可以进行简单的概率计算来验证这一点:

    现有四种花色的 A、2、3、4、5、6、7、8、9、10、J、Q、K 共计 52 张牌(移除大小王),随机打乱这 52 张牌,所有可能出现的排列顺序共计有 52! 种。假设取红桃 A 来分析,计算红桃 A 在不放回抽牌的情况下出现在 52 个位置上的概率。

    出现在第 1 个位置:1 / 52;

    出现在第 2 个位置: (51 / 52) * (1 / 51) = 1 / 52;

    出现在第 3 个位置:(51 / 52) * (50 / 51) * (1 / 50) = 1 / 52;

    发现规律了,以此类推,红桃 A 出现在第 52 个位置的概率也是 1 / 52。

    ​3​ 程序验证(Lua)

    首先定义一副扑克牌。我们用一个字节来表示一张牌,取高 4 位为花色,低 4 位为牌值。四种花色的 A、2、3、4、5、6、7、8、9、10、J、Q、K 定义为:

    local poker = {
        0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
        0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D,
        0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D,
        0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D,
    }

    实现 Fisher-Yates 算法:

    local function shuffle(poker)
      for i = 1, #poker - 1 do
        local r = math.random(i, #poker)
        local t = poker[i]
        poker[i] = poker[r]
        poker[r] = t
      end
    end

    假设花色 1 为红桃,牌值 1 为 A,那么红桃 A 的数值为 0x11。循环 1000000 次洗牌,每次找出红桃 A 的位置,并将相应下标的 count 计数加 1。

    local loop = 1000000
    local count = {}
    for i = 1, loop do
            local t = deepcopy(poker)
            shuffle(t)
            for k,v in ipairs(t) do
                    if v == 0x11 then
                            count[k] = count[k] or 0
                            count[k] = count[k] + 1
                            break
                    end
            end
    end

    对每个位置出现的次数/总循环次数 1000000,即为红桃 A 出现在每个位置上的概率。

    local result = ''
    for i = 1, #count do
            result = result .. ' ' .. count[i] / loop
    end
    print(result)

    运行结果:

    0.019282 0.019079 0.019306 0.01923 0.019104 0.01905 0.018902 0.019208 0.019321 0.019094 0.019425 0.019356 0.019352 0.019463 0.019177 0.019432 0.019271 0.019066 0.019431 0.0194 0.019154 0.019006 0.018965 0.019471 0.019178 0.019193 0.019086 0.019273 0.019274 0.019146 0.019259 0.018649 0.019286 0.019283 0.019304 0.019242 0.019525 0.019049 0.019115 0.019392 0.019281 0.019285 0.019164 0.019376 0.019388 0.019465 0.019034 0.019298 0.019343 0.019197 0.019004 0.019366

    可以看到每个位置概率都为 0.019,即 1 / 52。

    ​4​ 参考资料

    1. Wiki. Fisher–Yates shuffle

    2. Wiki.Simple random sample

    3. The intuition behind Fisher-Yates shuffling

  • 相关阅读:
    eclipse测试链接sql server2008 数据库
    Java中的日期格式转化
    java.sql.SQLException: Field 'id' doesn't have a default value(用eclipse操作数据库时报了这种奇怪的错误)的原因与解决方法
    Java进程与线程的区别
    Java开发岗位面试题归类
    JavaScript 里,$ 代表什么?/JQuery是什么语言?/html中用link标签引入css时的中 rel="stylesheet"属性?/EL表达式是什么?
    (一)初识EasyTouch
    简单影子制作
    屏幕分辨率
    VuforiaAR 教程
  • 原文地址:https://www.cnblogs.com/codetravel/p/12528467.html
Copyright © 2011-2022 走看看