Haskell 中给定种子的随机数质量

2023-12-06

我正在玩一个小型 ascii 游戏中的程序生成,并且在 haskell 中遇到了随机数的问题。基本思想是提供一个随机数,该随机数以游戏世界某些部分的 (x,y) 为种子,例如确定那里是否有一棵树(这家伙解释得很好)

这是我为每一代尝试不同的种子时得到的结果:

randomFromSeed :: Int -> Int -> Int
randomFromSeed max seed = fst (randomR (0, max - 1) (mkStdGen seed))

Prelude> map (randomFromSeed 10) [1..20]
[5,9,3,7,1,5,9,3,7,1,5,9,3,7,1,5,9,3,7,1]

显然它的周期为 5,但另一方面mkStdGen 文档它说:

函数 mkStdGen 提供了另一种生成初始生成器的方法,即将 Int 映射到生成器。同样,不同的参数应该可能产生不同的生成器。

那么为什么似乎只有 5 个不同的生成器出现呢?

当给予不同的种子时,如何才能使它们真正随机?

Edit由于一些奇怪的原因,使用更大的数字会更好:

Prelude> let mult = 1000000 in map (randomFromSeed 10) [0,mult .. 20*mult]
[3,7,0,6,9,2,8,1,4,0,3,9,2,5,1,4,7,3,6,9,5]

So how come, there seems to be only 5 distinct generators coming ?

认为只有 5 台发电机是一种错觉。如果打印每个序列的第二个数字而不是第一个数字,您会得到以下结果:

random2ndFromSeed :: Int -> Int -> Int
random2ndFromSeed max seed =
    let   g0       = mkStdGen seed
          (v1, g1) = randomR (0, max - 1) g0
          (v2, g2) = randomR (0, max - 1) g1
    in   v2
 λ> 
 λ> map  (random2ndFromSeed 10)  [1..40]
[6,9,3,8,1,4,8,3,6,9,3,8,1,4,8,3,6,9,3,8,1,4,8,3,6,9,3,8,1,4,8,3,6,9,3,8,1,4,8,3]
 λ> 

所以周期看起来是 8 而不是 5!

解决这个明显问题的一种方法是将标准生成器替换为三鱼一是设计较新,具有更好的统计特性。或者你也可以使用PCG-随机正如戴夫·康普顿所提到的。

import  System.Random.TF

tfRandomFromSeed :: Int -> Int -> Int
tfRandomFromSeed max seed = let   g0 = mkTFGen seed
                            in    fst $ randomR (0, max - 1) g0
 λ> 
 λ> map  (tfRandomFromSeed 10)  [1..40]
[4,5,6,7,5,3,3,0,0,4,2,8,0,4,1,0,0,1,3,5,6,4,3,6,4,0,3,6,4,0,2,4,5,9,7,3,8,5,2,4]
 λ> 

更一般地说,随机性的出现应该来自于生成器的重复应用next功能。在这里,该函数仅对每个种子/序列应用一次,因此不要求随机性。

如何创建持久的二维随机场

从评论来看,实际需要的是二维空间中点的“随机”函数。如果玩家在一些随机游走之后返回到某个已经访问过的点,则期望找到与之前相同的随机值,并且这无需记住以前的随机值。

为了以某种方式实现这一点,让我们对随机值的统计特性有一些保证,我们需要使用单个种子和单个随机序列来实现;这就是我们的应用数学家testing.

我们需要两件事来产生这样一个持久的二维随机场:

  1. 一种将 2D 点映射到单个自然数的方法,用作随机序列的偏移量
  2. 一个可以有效访问的随机数生成器随意的每个序列的点

将 2D 点映射到自然数

例如,这可以通过利用康托配对函数来自初等集合论。

我们可以使用这段代码:

-- limited to first quadrant, x >= 0 and y >= 0:
cantor1 :: Int -> Int -> Int
cantor1 x y = y + (let s = x + y  in  div  (s * (s+1))  2)

-- for all 4 quadrants:
cantor :: (Int, Int) -> Int
cantor (x,y) =
    let quadrant
          | x >= 0  &&  y >= 0   =  0
          | x <  0  &&  y >= 0   =  1
          | x <  0  &&  y  < 0   =  2  
          | x >= 0  &&  y <  0   =  3
          | otherwise            =  error  "cantor: internal error #1"
        cant1
          | x >= 0  &&  y >= 0   =  cantor1     x      y
          | x <  0  &&  y >= 0   =  cantor1  (-1-x)    y
          | x <  0  &&  y  < 0   =  cantor1  (-1-x)  (-1-y)
          | x >= 0  &&  y <  0   =  cantor1     x    (-1-y)
          | otherwise            =  error  "cantor: internal error #2"
    in
         4*cant1 + quadrant

安排任意访问

完成这个初步步骤后,我们必须认识到常规 Haskell 随机数生成 API 不太适合手头的任务。

API 通过以下方式提供对随机序列的顺序访问:next功能。但没有随意的访问,例如 C++ 随机库中提供的discard功能。和经典的一元风格使用莫纳德随机数接口都是关于顺序访问的。它基本上就像一个状态单子。

此外,使用某些随机数生成器,根本不可能有效访问序列的任意点。在这种情况下,C++discard函数只是使用昂贵的单步执行来到达想要的点。

幸运的是,有一个Haskell 实现Pierre L'Ecuyer 等人MRG32k3a随机数生成器。

使用 MRG32k3a,对随机序列的任意访问可归结为 2 个伽罗瓦域中小矩阵的幂运算。感谢古老而受人尊敬的印度求幂算法,这可以在 O(log n) 时间内完成。

github中的MRG32k3a代码没有提供完整的Haskell风格的接口,例如RandomGen实例,所以我们必须在它周围添加一些包装器代码。

首先,我们需要一些导入条款:

import  System.Random
import  System.Random.TF
import qualified  Data.List           as  L
import qualified  Text.Printf         as  TP
import qualified  Data.Text           as  TL
import qualified  Data.ByteString     as  BS
import qualified  Data.Text.Encoding  as  TSE
import qualified  Crypto.Hash.SHA256  as  SHA
import qualified  System.Random.MRG32K3A.Simple as MRG

然后是包装器代码本身:

newtype MRGen = MRGen MRG.State  -- wrapper type for MRG32k3a generator
                deriving  Show

instance RandomGen  MRGen  where
    genRange = let  mrg32k3a_m1 = ((2::Integer)^32 - 209)
               in   const  (0::Int, fromIntegral (mrg32k3a_m1 - 1))

    next (MRGen g0) = let  (v, g1) = MRG.next g0
                      in   ((fromIntegral v)::Int, MRGen g1)

    split (MRGen g0) = let  g1 = MRG.advance ((2::Integer)^96) g0
                       in   (MRGen g0, MRGen g1) 

mkMRGen :: Int -> MRGen
mkMRGen userSeed = let  longSeed = hashSeed userSeed
                        g0       =  MRG.seed longSeed
                   in   MRGen g0

ranSeek :: MRGen -> Integer -> MRGen
ranSeek (MRGen g0) count =  let  g1 = (MRG.advance count g0)  in   MRGen g1

hashSeed :: Int -> Integer
hashSeed userSeed =
    let str   = "MRG32k3a:" ++ (TP.printf "0x%x" userSeed)
        bytes =  (TSE.encodeUtf8 . TL.pack) $ str
        ints  = (map (fromIntegral) $ BS.unpack (SHA.hash bytes)) :: [Integer]
    in
        L.foldl'  (\acc d -> acc*256 + d)  0  (take 20 ints)

功能mkMRGen类似于mkStdGen。对随机序列的任意访问由函数提供ranSeek :: MRGen -> Integer -> MRGen在 O(log n) 时间内。

边注:我正在重新散列用户提供的种子mkMRGen。这是因为 github 包使用其种子作为随机序列的偏移量。因此,为了避免小用户种子的序列重叠风险,我需要从用户种子生成大量数字。

感谢我们的RandomGen例如,我们可以访问常用功能,例如随机 :: RandomGen g => g -> (a, g)。例如,我们可以从一个简单的数据生成一个 Double 类型的二维随机字段Int像这样的种子:

randomDoubleField :: Int -> (Int, Int) -> Double
randomDoubleField userSeed (x,y) =
    let  k  = 1  -- number of needed random values per plane point
         g0 = mkMRGen userSeed
         g1 = ranSeek  g0  (fromIntegral (k * cantor (x,y)))
    in   fst (random g1)

现在我们有了这个小工具包,我们可以编写一个小测试程序,为零点的邻域绘制一些随机景观,每个 2D 点一个字符。

比如说,字符“t”代表一种类型的树,“T”代表另一种类型的树。树的缺失用减号表示。

主要程序:

randomCharField :: Int -> (Int, Int) -> Char
randomCharField  userSeed  (x,y) =
    let  n = floor (8.0 * randomDoubleField userSeed (x,y) )
    in   "------tT"  !!  n


rowString :: Int -> Int -> Int -> String
rowString userSeed size y =
               let  xRange = [(-size) .. size]
               in   map  (randomCharField userSeed)  [ (x,y) | x <- xRange ]


main = do
    let  userSeed = 42
         size     = 6
         yRange   = [(-size) .. size]
    mapM_  (putStrLn . (rowString userSeed size))  yRange

程序输出:

--t-T----TT-t
------t-----T
-T--T--T-----
--t-T--tTTT--
--T--t---T---
t-Tt------t--
-T-----t-T---
-T-t-t----T--
tT-tT---tT--t
---TTt---t---
-------T---t-
--t---------t
-tT-t---t----

优化注意事项:如果性能是一个问题,您可能想要移动(mkMRGen userSeed)计算脱离循环。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Haskell 中给定种子的随机数质量 的相关文章

  • 为什么 Haskell 的默认字符串实现是一个字符链接列表?

    Haskell 默认值的事实String众所周知 实现在速度和内存方面都效率不高 据我所知 lists一般来说 在 Haskell 中实现为单链表 并且适用于大多数小型 简单数据类型 例如Int 这似乎不是一个好主意 但是对于String这
  • 在 Haskell 中,为什么我必须在这段代码中使用美元符号?

    我仍在尝试破解这段代码 import Data Char groupsOf groupsOf n xs take n xs groupsOf n tail xs problem 8 x maximum map product groupsO
  • Haskell 中的 print 是纯函数吗?

    Is print在 Haskell 中是纯函数 为什么或者为什么不 我认为不是 因为它并不总是返回与纯函数应返回的值相同的值 类型的值IO Int并不是真正的Int 它更像是一张纸 上面写着 嘿 Haskell 运行时 请生成一个Int如此
  • 纯函数怎么能做IO呢?

    我最近了解到莫纳德随机数 http hackage haskell org package MonadRandom 0 1 13 docs Control Monad Random Class html t 3aMonadRandom图书馆
  • Haskell,堆栈:找到可执行文件

    我正在寻找类似的东西 stack whereis hasktags where whereis行为或多或少类似于 UNIXwhereis命令 hasktags是这样运行的 stack exec hasktags stack exec whe
  • “Eta减少”并不总是在Haskell中举行?

    我发现我可以说 LANGUAGE RankNTypes f1 forall b b gt b gt forall c c gt c f1 f id f HLint 告诉我我可以在这里做 Eta 减少 但是 f2 forall b b gt
  • 如何在 Haskell 中安装库?

    我尝试使用控制 Monad Extra andM https hackage haskell org package extra 1 7 10 docs Control Monad Extra html import Control Mon
  • Haskell 中的中缀运算符优先级

    对于以下 Haskell 表达式 返回 a gt gt f 应该读作 返回a gt gt f or 返回 a gt gt f 这里的相关规则是什么 规则始终是函数应用程序的优先级高于任何运算符 因此 return a gt gt f 被解析
  • 用于遇到 [...] 的 Haskell Parsec 解析器

    我正在尝试使用 Parsec 在 Haskell 中编写一个解析器 目前我有一个可以解析的程序 test x 1 2 3 end 执行此操作的代码如下 testParser do reserved test v lt identifier
  • 有没有更好的方法将 UTC 时间转换为大纪元时间?

    我想将文件的修改时间设置为从 exif 数据获取的时间 为了从 exif 获取时间 我发现 Graphics Exif getTag Exif gt String gt IO Maybe String 要设置文件修改时间 我发现 Syste
  • Haskell 输入返回元组

    我想知道 IO 函数是否可以返回元组 因为我想从这个函数中获取这些元组作为另一个函数的输入 investinput IO gt Char Int investinput do putStrLn Enter Username username
  • 为什么 ZipList 不是 List 的默认应用实例

    我目前正在学习 Haskell 中的应用程序 如果我没记错的话 列表有两个不同的应用实例 List and ZipList 第二个被定义为包装列表值的新类型 这ZipList应用实例对我来说似乎更直观 这可能是一个愚蠢的问题 但有具体原因吗
  • Haskell 对于 Web 应用程序来说足够成熟吗? [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 带有 RankNTypes 扩展的奇怪类型推断

    我正在尝试在 Haskell 中尝试 System F 类型 并通过以下方式实现了自然数的 Church 编码type 当加载这段代码时 OPTIONS GHC Wall LANGUAGE RankNTypes type CNat fora
  • 迭代打印列表中的每个整数

    假设我有一个整数列表l 1 2 我想打印到stdout Doing print l产生 1 2 假设我想打印不带大括号的列表 map print l产生 No instance for Show IO arising from a use
  • 关于“没有绑定的类型签名”的错误

    我在 Haskell 中遇到 ASCII 问题 fromEnum Char gt Int toEnum Int gt Char offset Int offset fromEnum A fromEnum a toUpper Char gt
  • 在另一个字符串中查找子字符串的索引 Haskell

    我要创建一个带有两个参数 字符串 的函数 该函数应查看第一个参数是否是第二个参数的子字符串 如果是这种情况 它将返回每个出现的元组 其中包含子字符串的起始索引和子字符串的结尾索引 例如 f String gt String gt Int I
  • 在 Haskell 中获取玫瑰树的根

    最近我开始学习 Haskell 并在以下练习中遇到困难 Write functions root Rose a gt a and children Rose a gt Rose a that return the value stored
  • 函数式语言中的部分求值和函数内联有什么区别?

    我知道 函数内联就是用函数定义代替函数调用 部分评估是在编译时评估程序的已知 静态 部分 在 C 等命令式语言中 两者之间存在区别 其中运算符与函数不同 但是 在像 Haskell 这样的函数式语言 其中运算符也是函数 中 两者之间有什么区
  • Haskell:对 Num 类型类的使用感到困惑

    我很困惑为什么这有效 f Num a gt a gt a f x x 42 但这并没有 g Num a gt a gt a g x x 4 2 我本来就明白Num包含实现运算符的所有类型 因此 如果42 is an Int and 4 2

随机推荐