【问题标题】:Efficient way to generate a seemingly random permutation from a very large set without repeating?从非常大的集合中生成看似随机排列而不重复的有效方法?
【发布时间】:2015-11-28 05:32:09
【问题描述】:

我有一个非常大的集合(十亿 或更多,预计会以指数方式增长到某个水平),我想从中生成看似随机的元素而不重复。我知道我可以选择一个随机数并重复并记录我生成的元素,但是随着数字的生成,这会占用越来越多的内存,并且在数百万个元素出来之后就不实用了。

我的意思是,我可以说 1、2、3 到数十亿,每个都是常数时间,而不记得之前的所有内容,或者我可以说 1、3、5、7、9 和然后是 2,4,6,8,10,但是有没有更复杂的方法可以做到这一点并最终获得该集合的看似随机排列?

更新

1、集合在生成过程中不改变大小。我的意思是当用户的输入线性增加时,集合的大小呈指数增长。

2、简而言之,集合就像从1到100亿或更多的每一个整数的集合。

3,总之,它会上升到 100 亿,因为每个元素都携带了许多独立选择的信息,例如。想象一个 RPG 角色有 10 个属性,每个属性可以从 1 到 100(对于我的问题,不同的选择可以有不同的范围),因此有 10^20 个可能的字符,数字“10873456879326587345”将对应于具有“11, 88, 35...",我想要一种算法来一个一个地生成它们而不重复,但是让它看起来随机。

【问题讨论】:

  • 集合的大小在整个过程中是固定的,还是会在选择两个数字之间增长?
  • 您拥有的项目是一组还是列表/数组? IE。它是可索引的吗?参考:en.wikipedia.org/wiki/Set_(abstract_data_type)en.wikipedia.org/wiki/Array_data_type
  • @biziclop 集合的大小在整个过程中是固定的。感谢您的提问,现在更新我的问题。
  • @Paddy3118 这是一个可枚举的集合。可以把它想象成一组从 1 到 100 亿或更多的整数。
  • 如果两个角色至少可以共享一些属性设置,为什么不随机生成每个属性呢?

标签: algorithm random permutation


【解决方案1】:

感谢您提出有趣的问题。您可以使用模幂创建具有几个字节的“伪随机”*(循环)排列。假设我们有 n 个元素。寻找一个大于 n+1 的素数 p。然后找到一个以 p 为模的原根 g。基本上根据原始根的定义,动作 x --> (g * x) % p 是 {1, ..., p-1} 的循环置换。所以 x --> ((g * (x+1))%p) - 1 是 {0, ..., p-2} 的循环置换。如果给出的值更大(或等于)n,我们可以通过重复先前的排列来得到 {0, ..., n-1} 的循环排列。

我将这个想法实现为一个 Go 包。 https://github.com/bwesterb/powercycle

package main

import (
    "fmt"
    "github.com/bwesterb/powercycle"
)

func main() {
    var x uint64
    cycle := powercycle.New(10)
    for i := 0; i < 10; i++ {
        fmt.Println(x)
        x = cycle.Apply(x)
    }
}

这会输出类似

0
6
4
1
2
9
3
5
8
7

但这可能会因所选的生成器而异。

它很快,但不是超快:在我使用了 5 年的 i7 上,计算 1000000000000000 个元素上的一个周期应用程序只需不到 210 纳秒。更多详情:

BenchmarkNew10-8                     1000000          1328 ns/op
BenchmarkNew1000-8                    500000          2566 ns/op
BenchmarkNew1000000-8                  50000         25893 ns/op
BenchmarkNew1000000000-8              200000          7589 ns/op
BenchmarkNew1000000000000-8             2000        648785 ns/op
BenchmarkApply10-8                  10000000           170 ns/op
BenchmarkApply1000-8                10000000           173 ns/op
BenchmarkApply1000000-8             10000000           172 ns/op
BenchmarkApply1000000000-8          10000000           169 ns/op
BenchmarkApply1000000000000-8       10000000           201 ns/op
BenchmarkApply1000000000000000-8    10000000           204 ns/op

为什么我说“伪随机”?好吧,我们总是在创建一种非常特殊的循环:即使用模幂运算的循环。不过它看起来很伪随机。

【讨论】:

  • 我当时做了其他事情......但很高兴知道有一个解决方案!谢谢!
【解决方案2】:

我会使用一个随机数并将其与集合开头的一个元素交换。

这是一些伪代码

set = [1, 2, 3, 4, 5, 6]
picked = 0
Function PickNext(set, picked)
  If picked > Len(set) - 1 Then
    Return Nothing
  End If
  // random number between picked (inclusive) and length (exclusive)
  r = RandomInt(picked, Len(set))
  // swap the picked element to the beginning of the set
  result = set[r]
  set[r] = set[picked]
  set[picked] = result
  // update picked
  picked++
  // return your next random element
  Return temp
End Function

每次选择一个元素时,都会有一个交换,唯一使用的额外内存是 picked 变量。如果元素在数据库或内存中,则可能发生交换。

编辑这是一个工作实现的 jsfiddle http://jsfiddle.net/sun8rw4d/

JavaScript

var set = [];
set.picked = 0;
function pickNext(set) {
    if(set.picked > set.length - 1) { return null; }
    var r = set.picked + Math.floor(Math.random() * (set.length - set.picked));
    var result = set[r];
    set[r] = set[set.picked];
    set[set.picked] = result;
    set.picked++;
    return result;
}

// testing
for(var i=0; i<100; i++) {
    set.push(i);
}
while(pickNext(set) !== null) { }
document.body.innerHTML += set.toString();

EDIT 2 最后,随机二元游走。这可以通过 O(Log2(N)) 堆栈空间(内存)来完成,对于 100 亿来说只有 33 个。不涉及洗牌或交换。使用三进制而不是二进制可能会产生更好的伪随机结果。

// on the fly set generator
var count = 0;
var maxValue = 64;
function nextElement() {
    // restart the generation
    if(count == maxValue) {
        count = 0;
    }
    return count++;
}

// code to pseudo randomly select elements
var current = 0;
var stack = [0, maxValue - 1];
function randomBinaryWalk() {
    if(stack.length == 0) { return null; }
    var high = stack.pop();
    var low = stack.pop();
    var mid = ((high + low) / 2) | 0;
    // pseudo randomly choose the next path
    if(Math.random() > 0.5) {
        if(low <= mid - 1) {
            stack.push(low);
            stack.push(mid - 1);
        }
        if(mid + 1 <= high) {
            stack.push(mid + 1);
            stack.push(high);
        }
    } else {
        if(mid + 1 <= high) {
            stack.push(mid + 1);
            stack.push(high);
        }
        if(low <= mid - 1) {
            stack.push(low);
            stack.push(mid - 1);
        }
    }
    // how many elements to skip
    var toMid = (current < mid ? mid - current : (maxValue - current) + mid);
    // skip elements
    for(var i = 0; i < toMid - 1; i++) {
        nextElement();
    }
    current = mid;
    // get result
    return nextElement();
}

// test
var result;
var list = [];
do {
    result = randomBinaryWalk();
    list.push(result);
} while(result !== null);
document.body.innerHTML += '<br/>' + list.toString();

以下是使用少量 64 个元素的几次运行的结果。 JSFiddlehttp://jsfiddle.net/yooLjtgu/

30,46,38,34,36,35,37,32,33,31,42,40,41,39,44,45,43,54,50,52,53,51,48,47 ,49,58,60,59,61,62,56,57,55,14,22,18,20,19,21,16,15,17,26,28,29,27,24,25,23 ,6,2,4,5,3,0,1,63,10,8,7,9,12,11,13

30,14,22,18,16,15,17,20,19,21,26,28,29,27,24,23,25,6,10,8,7,9,12,13 ,11,2,0,63,1,4,5,3,46,38,42,44,45,43,40,41,39,34,36,35,37,32,31,33,54 ,58,56,55,57,60,59,61,62,50,48,49,47,52,51,53

正如我在评论中提到的,除非您有一种有效的方式跳过到集合的“动态”生成中的特定点,否则效率不会很高。

【讨论】:

  • 这基本上是一步一步完成的Fisher-Yates shuffle。
  • @biziclop - 无论如何都是一个就地版本,更像是来自同一维基百科页面en.wikipedia.org/wiki/… 的 Sattolo 算法@ 我所做的一项改进是将选取的元素移到开头,这允许集合在采摘仍在进行中。
  • 集合没有开头或结尾,集合的第 i 个元素没有意义,因此上述下标不适用于集合。
  • 我无法进行任何“移动”,因为该集合有数十亿个元素并且不完全适合内存——您的第一行根本行不通。对不起!这些元素实际上是通过它们的索引动态生成的。同样,我正在寻找类似于我给出的示例的内容,不需要记住已生成的内容或剩下的内容。
  • @Paddy3118 - 我不知道你从哪里得到集合的定义,但对大多数人来说,在数学中,集合是不同对象的集合。集合有大小并且可以枚举。
【解决方案3】:

如果它是可枚举的,则使用调整为周期 0 .. 2^n - 1 的伪随机整数生成器,其中上限刚好大于您的集合的大小,并生成伪随机整数丢弃超过你的集合的大小。使用这些整数为您的集合中的项目建立索引。

【讨论】:

  • 是的,我认为这是正确的方向。谢谢。对我来说,下一步是尝试找到具有可调节周期的东西……但是这些生成器是否保证每个数字在重复之前只生成一次?
  • 一些算法生成周期为2^n - 1。研究线性反馈移位寄存器或PCGpcg-random.org
  • 是的,经过一番挖掘,我发现了这个:github.com/preshing/RandomSequence/blob/master/randomsequence.h
  • 如果我能弄清楚它是如何工作的并改变周期就足够了
  • 我查看了您的链接,它并不是说每个数字都出现一次,或者可以自定义时间段。
【解决方案4】:

为自己预先计算一系列索引(例如在文件中),这些索引具有您需要的属性,然后为您的枚举随机选择一个起始索引并以循环方式使用该系列。

预计算序列的长度应该 > 集合的最大大小。

如果你将它(取决于你的编程语言等)与文件映射结合起来,你最终的nextIndex(INOUT state) 函数(几乎)和return mappedIndices[state++ % PERIOD]; 一样简单,如果你有每个条目的固定大小(例如 8 个字节) -> uint64_t)。

当然,返回值可以是 > 您当前设置的大小。简单地绘制索引,直到你得到一个

更新(针对问题更新):

如果要在您的 RPG 中创建 100 亿个独特角色,还有另一种选择可以实现您的目标:生成一个 GUID 并为自己编写一个函数,该函数根据 GUID 计算您的数字。 man uuid 如果您使用的是 unix 系统。否则谷歌它。 uuid 的某些部分不是随机的,而是包含元信息,某些部分是系统的(例如您的网卡 MAC 地址)或随机的,具体取决于生成器算法。但它们很可能是独一无二的。因此,每当您需要一个新的唯一号码时,生成一个 uuid 并通过某种算法将其转换为您的号码,该算法基本上以一种非平凡的方式将 uuid 字节映射到您的号码(例如使用哈希函数)。

【讨论】:

  • 如果您的意思是“生成数十亿个元素的排列作为存储在文件中的缓存,然后使用它”,那将占用千兆字节的 dick 空间。正如问题中所建议的那样,所需的空间也将以相对于“属性数量”(参见隐喻)的指数速度增长。总的来说,这是一个有用的策略,但遗憾的是,它不能很好地解决这个问题。
  • @Zuoanqh 我假设如果您有磁盘空间(可能有数十亿个集合条目),那么您也有足够大的磁盘,但没有足够的内存来“更快”地做某事。现在阅读更新,我开始怀疑您对问题的看法是否最佳:如果您寻求的数字基本上由 [0..100] 中的 N 个随机数组成,那么 2 个玩家以完全相同的字符?恕我直言,您根本不需要簿记。
  • 是的,但这只是一个例子。实际上会有数以百万计的“玩家”,我想要一些始终完美运行的东西,而不是随着时间的推移变得越来越慢的东西。一切都与可扩展性有关!我今天也想要这百万个字符,明天再有几百万,一直这样,当我处理完它们时,我会删除它,但我不希望明天出现相同的任何东西。
猜你喜欢
  • 2015-12-28
  • 2012-10-31
  • 1970-01-01
  • 2011-07-23
  • 1970-01-01
  • 2011-11-17
  • 2015-12-19
  • 2020-02-07
  • 1970-01-01
相关资源
最近更新 更多