我想尽可能快地生成随机排列。 问题:作为O(n)的knuth shuffle涉及生成n个随机数。 由于生成随机数非常昂贵。 我想找到一个涉及固定O(1)量随机数的O(n)函数。
我意识到之前已经问过这个问题,但我没有看到任何相关的答案。
只是强调一点:我不是在寻找比O(n)更少的东西,只是一种涉及更少随机数生成的算法。
由于
答案 0 :(得分:8)
创建每个排列的1-1映射到从1到n的数字! (n阶乘)。生成1到n!中的随机数,使用映射,得到排列。
对于映射,这可能很有用:http://en.wikipedia.org/wiki/Permutation#Numbering_permutations
当然,这会很快失控,就像n!很快就会变得很大。
答案 1 :(得分:2)
生成随机数需要很长时间才能说出来? Javas Random.nextInt的实现大致是
oldseed = seed;
nextseed = (oldseed * multiplier + addend) & mask;
return (int)(nextseed >>> (48 - bits));
每个元素都需要做太多工作吗?
答案 2 :(得分:1)
不是你问的确切,但如果提供随机数生成器不满足你,可能你应该尝试不同的东西。通常,伪随机数生成可以非常简单。
可能是最着名的算法
http://en.wikipedia.org/wiki/Linear_congruential_generator
更多
http://en.wikipedia.org/wiki/List_of_pseudorandom_number_generators
答案 3 :(得分:1)
正如其他答案所示,您可以在0到N的范围内创建一个随机整数!并用它来产生一个洗牌。虽然理论上是正确的,但一般来说这不会更快,因为N!快速增长,你会把所有时间都花在做bigint算术上。
如果你想要速度并且不介意牺牲一些随机性,那么使用不太好的随机数生成器会更好。线性同余生成器(参见http://en.wikipedia.org/wiki/Linear_congruential_generator)将在几个周期内为您提供随机数。
答案 4 :(得分:1)
通常不需要全范围的下一个随机值,所以要使用完全相同数量的随机性,你可以使用下一个方法(这几乎就像随机(0,N!),我猜):
// ...
m = 1; // range of random buffer (single variant)
r = 0; // random buffer (number zero)
// ...
for(/* ... */) {
while (m < n) { // range of our buffer is too narrow for "n"
r = r*RAND_MAX + random(); // add another random to our random-buffer
m *= RAND_MAX; // update range of random-buffer
}
x = r % n; // pull-out next random with range "n"
r /= n; // remove it from random-buffer
m /= n; // fix range of random-buffer
// ...
}
P.S。当然会有一些与2 ^ n不同的除法相关的错误,但它们会在结果样本中分配。
答案 5 :(得分:1)
在进行计算之前生成N个数字(N <您需要的随机数的数量),或者将它们作为数据存储在数组中,使用慢速但良好的随机生成器;然后选择一个数字,简单地将索引递增到计算循环内的数组中;如果您需要不同的种子,请创建多个表格。
答案 6 :(得分:1)
您确定您的数学和算法方法是否正确?
我遇到了完全相同的问题,Fisher-Yates shuffle将成为角落案件的瓶颈。但对我来说,真正的问题是蛮力算法,它不能很好地适应所有问题。以下故事解释了迄今为止我提出的问题和优化。
4名玩家的交易卡
可能的交易数量是 96位数字。这给随机数生成器带来了很大的压力,以避免在从生成的交易样本集中选择游戏计划时出现静态异常。我选择使用来自/ dev / random的2xmt19937_64种子,因为网络上的长期和重型广告对科学模拟很有用。
简单的方法是使用Fisher-Yates shuffle生成交易并过滤掉与已收集的信息不匹配的交易。 Knuth shuffle每次交易需要大约1400个CPU周期,主要是因为我必须生成51个随机数并在表中交换51次条目。
对于我只需要在7分钟内生成10000-100000笔交易的正常情况无关紧要。但是在极端情况下,过滤器可能只选择需要大量交易的非常小的一部分手。
为多张卡片使用单个号码
使用callgrind(valgrind)进行分析时,我注意到主要的减速是C ++随机数生成器(在从第一个瓶颈的std :: uniform_int_distribution切换之后)。
然后我想出我可以使用单个随机数用于多张卡片。我们的想法是首先使用数字中最不重要的信息,然后删除该信息。
int number = uniform_rng(0, 52*51*50*49);
int card1 = number % 52;
number /= 52;
int cards2 = number % 51;
number /= 51;
......
当然这只是次要优化,因为生成仍然是O(N)。
使用位排列生成
接下来的想法是在这里完全解决问题,但我最终仍然使用O(N)但成本比原来的shuffle更大。但是让我们看看解决方案以及为什么它如此悲惨地失败。
我决定使用提示Dealing All the Deals by John Christman
void Deal::generate()
{
// 52:26 split, 52!/(26!)**2 = 495,918,532,948,1041
max = 495918532948104LU;
partner = uniform_rng(eng1, max);
// 2x 26:13 splits, (26!)**2/(13!)**2 = 10,400,600**2
max = 10400600LU*10400600LU;
hands = uniform_rng(eng2, max);
// Create 104 bit presentation of deal (2 bits per card)
select_deal(id, partner, hands);
}
到目前为止看起来还不错,但是select_deal实现是PITA。
void select_deal(Id &new_id, uint64_t partner, uint64_t hands)
{
unsigned idx;
unsigned e, n, ns = 26;
e = n = 13;
// Figure out partnership who owns which card
for (idx = CARDS_IN_SUIT*NUM_SUITS; idx > 0; ) {
uint64_t cut = ncr(idx - 1, ns);
if (partner >= cut) {
partner -= cut;
// Figure out if N or S holds the card
ns--;
cut = ncr(ns, n) * 10400600LU;
if (hands > cut) {
hands -= cut;
n--;
} else
new_id[idx%NUM_SUITS] |= 1 << (idx/NUM_SUITS);
} else
new_id[idx%NUM_SUITS + NUM_SUITS] |= 1 << (idx/NUM_SUITS);
idx--;
}
unsigned ew = 26;
// Figure out if E or W holds a card
for (idx = CARDS_IN_SUIT*NUM_SUITS; idx-- > 0; ) {
if (new_id[idx%NUM_SUITS + NUM_SUITS] & (1 << (idx/NUM_SUITS))) {
uint64_t cut = ncr(--ew, e);
if (hands >= cut) {
hands -= cut;
e--;
} else
new_id[idx%NUM_SUITS] |= 1 << (idx/NUM_SUITS);
}
}
}
现在我已经完成了O(N)置换解决方案以证明算法可行,我开始搜索从随机数到位置换的O(1)映射。太糟糕了,看起来只有解决方案才会使用会破坏CPU缓存的巨大查找表。对于AI而言,这对双模拟分析器使用非常大量的缓存并不是一个好主意。
数学解决方案
经过努力弄清楚如何生成随机位置换后,我决定回到数学。在交易卡之前完全可以应用过滤器。这需要将交易分成可管理的分层集合,并在过滤掉不可能的集合后根据其相对概率在集合之间进行选择。
我还没有准备好代码来测试在过滤器选择交易的主要部分的常见情况下我浪费了多少周期。但我相信这种方法可以提供最稳定的发电性能,使成本低于0.1%。
答案 7 :(得分:0)
生成32
位整数。对于每个索引i
(可能只占阵列中元素数量的一半),如果位i % 32
为1
,则将i
与n - i - 1
交换。
当然,对于您的目的,这可能不够随意。您可以通过不与n - i - 1
交换,而是通过应用于n
和i
的其他函数来提高分布,从而提高分布效果。你甚至可以使用两个函数:一个用于位0
,另一个用于1
。
答案 8 :(得分:0)
请参阅https://doi.org/10.1145/3009909,以仔细分析生成随机排列所需的随机位数。 (这是开放存取的,但是不容易阅读!最重要的是:如果仔细实施,所有用于生成随机排列的常规方法都可以有效地使用随机位。)
并且...如果您的目标是为大N快速生成随机排列,建议您尝试使用MergeShuffle算法。 article published in 2015声称在并行和顺序实现中比Fisher-Yates加快了两倍,并且在顺序计算方面比他们测试的其他标准算法(Rao-Sandelius)有了明显提高。
https://github.com/axel-bacher/mergeshuffle上提供了MergeShuffle(以及常用的Fisher-Yates和Rao-Sandelius算法)的实现。但是,请告诫自己!作者是理论家,而不是软件工程师。他们已将实验代码发布到github,但没有维护。有一天,我想有人(也许您!)会将MergeShuffle添加到GSL中。目前tag.text
是Fisher-Yates的实现,请参见https://www.gnu.org/software/gsl/doc/html/randist.html?highlight=gsl_ran_shuffle。