随机抽取:如何确保不会过早重绘值

时间:2013-10-16 10:18:05

标签: algorithm random

从连续的一组值中随机绘制时,允许绘制的值 再次绘制,一个给定的值(当然)很快被连续两次(或更多)抽出,但这会引起一个问题(为了给定的应用程序的目的),我们想要消除这个机会。关于如何做的任何算法想法(简单/高效)?

理想情况下,我们希望将阈值设置为数据集大小的百分比:

说出值集合N=100的大小和阈值T=10%,然后如果在当前绘制中绘制给定值,则保证不会在下一个{ {1}}绘制。

显然,这种限制会在随机选择中引入偏差。我们不介意 提出的算法在选择的随机性中引入了进一步的偏差,究竟是什么 对于这个应用程序来说,重要的是选择是随机的,以便显示出来 对于人类观察者来说。

作为实现细节,值存储为数据库记录,因此可以使用数据库表标志/值,也可以使用外部存储器结构。关于抽象案例的答案也是受欢迎的。

修改

我刚刚触及另一个问题here,它与我自己的问题有很好的重叠。通过那里的好点。

5 个答案:

答案 0 :(得分:2)

这是一个实现,在O(1)(对于单个元素)执行整个过程而没有任何偏见:

我们的想法是将数组中的最后K个元素A(包含所有值)视为一个队列,我们​​从N-k中的第一个A值中提取一个值,这是随机值,当指针表示队列的头部时,将其与位置N-Pointer中的元素交换,当它跨越K个元素时,它将重置为1.

为了消除前K个绘制中的任何偏差,随机值将在1N-Pointer之间而不是N-k之间绘制,因此每个绘制时此虚拟队列的大小会增加直到达到K的大小(例如,在3次绘制之后,A1N-3之间N-2中出现可能值的数量,并且暂停的值显示在索引{{ 1}}到N

所有操作都是O(1)用于绘制单个元素,并且在整个过程中没有偏见。

void DrawNumbers(val[] A, int K)
{
    N = A.size;
    random Rnd = new random;
    int Drawn_Index;
    int Count_To_K = 1;
    int Pointer = K;

    while (stop_drawing_condition)
    {
        if (Count_To_K <= K)
        {
            Drawn_Index = Rnd.NextInteger(1, N-Pointer);
            Count_To_K++;
        }

        else
        {
            Drawn_Index = Rnd.NextInteger(1, N-K)
        }

        Print("drawn value is: " + A[Drawn_Index])

        Swap(A[Drawn_Index], A[N-Pointer])
        Pointer--;
        if (Pointer < 1) Pointer = K; 
    }
}

我以前的建议,通过使用列表和实际队列,取决于列表的remove方法,我认为通过使用数组来实现自我,我认为最好O(logN)平衡二叉树,因为列表必须能直接访问索引。

void DrawNumbers(list N, int K)
{
    queue Suspended_Values = new queue;
    random Rnd = new random;
    int Drawn_Index;

    while (stop_drawing_condition)
    {
          if (Suspended_Values.count == K)
                N.add(Suspended_Value.Dequeue());

          Drawn_Index = Rnd.NextInteger(1, N.size) // random integer between 1 and the number of values in N

          Print("drawn value is: " + N[Drawn_Index]);          

          Suspended_Values.Enqueue(N[Drawn_Index]);
          N.Remove(Drawn_Index);
    }
}

答案 1 :(得分:2)

假设您的列表中有n个项目,并且您不希望选择任何k个最后项目。

从大小为n-k的数组中随机选择,并使用大小为k的队列来粘贴您不想绘制的项目(添加到前面并从后面移除)。

所有操作都是O(1)。

----澄清----

给出n个项目,并且目标是不重绘任何最后的k个绘制,创建一个数组和队列,如下所示。

  1. 创建一个大小为n-k的数组A,并将项目的n-k放入列表中(随机选择,或者随意播种)。

  2. 创建一个队列(链接列表)Q并用剩余的k项填充它,再次按随机顺序或任何你喜欢的顺序填充。

  3. 现在,每次您想要随机选择一个项目:

    1. 从数组中选择一个随机索引,称之为i。

    2. 将A [i]交给任何要求的人,并将其添加到Q的前面。

    3. 从Q背面删除元素,并将其存储在A [i]中。

    4. 创建数组和链表后,一切都是O(1),这是一次性O(n)操作。

      现在,您可能想知道,如果我们想要更改n(即添加或删除元素),我们该怎么做。

      每次我们添加一个元素时,我们要么增加A或Q的大小,这取决于我们决定k是什么的逻辑(即固定值,n的固定分数,等等......)。 p>

      如果Q增加然后结果很简单,我们只需将新元素附加到Q.在这种情况下,我可能会将它附加到Q的末尾,以便尽快进入游戏状态。你也可以把它放在A中,从A中踢出一些元素并将它附加到Q的末尾。

      如果A增加,您可以使用标准技术在分摊的常量时间内增加数组。例如,每次A填满时,我们将它的大小加倍,并跟踪现场A的单元数。 (如果不熟悉,请在维基百科中查找“动态阵列”。)

答案 2 :(得分:2)

我假设您有一个数组A,其中包含您要绘制的项目。在每个时间段,您都会从A中随机选择一个项目。

您希望阻止在某些i次迭代中再次绘制任何给定项k

假设你的门槛是A的10%。

所以创建一个队列,称之为drawn,可以容纳threshold个项目。还要创建包含绘制项的哈希表。调用哈希表hash

然后:

do
{
    i = Get random item from A
    if (i in hash)
    {
        // we have drawn this item recently. Don't draw it.
        continue;
    }
    draw(i);
    if (drawn.count == k)
    {
        // remove oldest item from queue
        temp = drawn.dequeue();
        // and from the hash table
        hash.remove(temp);
    }
    // add new item to queue and hash table
    drawn.enqueue(i);
    hash.add(i);
} while (forever);

哈希表仅用于提高查找速度。如果您愿意对队列进行顺序搜索以确定最近是否绘制了一个项目,则可以不使用哈希表。

答案 3 :(得分:1)

我将所有“值”放入大小为N的“列表”中,然后随机播放列表并从列表顶部检索值。然后,您将检索到的值“插入”任意索引&gt; = N * T。

的随机位置

不幸的是,我不是一个真正的数学家:(所以我只是尝试了它(在VB中,所以请把它作为伪代码;))

Public Class BiasedRandom

Private prng As New Random
Private offset As Integer
Private l As New List(Of Integer)

Public Sub New(ByVal size As Integer, ByVal threshold As Double)

    If threshold <= 0 OrElse threshold >= 1 OrElse size < 1 Then Throw New System.ArgumentException("Check your params!")
    offset = size * threshold
    ' initial fill
    For i = 0 To size - 1
        l.Add(i)
    Next
    ' shuffle "Algorithm p"
    For i = size - 1 To 1 Step -1
        Dim j = prng.Next(0, i + 1)
        Dim tmp = l(i)
        l(i) = l(j)
        l(j) = tmp
    Next

End Sub

Public Function NextValue() As Integer

    Dim tmp = l(0)
    l.RemoveAt(0)
    l.Insert(prng.Next(offset, l.Count + 1), tmp)
    Return tmp

End Function

结束班

然后进行简单的检查:

Public Class Form1
Dim z As Integer = 10
Dim k As BiasedRandom

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    k = New BiasedRandom(z, 0.5)
End Sub

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim j(z - 1)
    For i = 1 To 10 * 1000 * 1000
        j(k.NextValue) += 1
    Next
    Stop
End Sub

结束班

当我查看分发时,看起来对于一个没有武装的眼睛来说看起来还不错;)

编辑: 在考虑了RonTeller的论证之后,我不得不承认他是对的。我不认为有一种性能友好的方式来实现所需的并且属于一个好的(不是比所需的更偏向)随机顺序。 我接下来的想法是:

给出一个列表(数组无论如何):

0123456789 '没有洗牌以明确我的意思

我们返回第一个元素为0.这个元素不得再次出现4(作为例子)更多的绘制,但我们也想避免强烈的偏见。为什么不简单地将它放在列表的末尾然后随机播放列表的“尾部”,即最后6个元素?

<强> 1234695807

我们现在返回1并重复上述步骤。

<强> 2340519786

依此类推。由于删除和插入是一种不必要的工作,可以使用简单的数组和实际元素的“指针”。我已经改变了上面的代码来提供样本。它比第一个慢,但应该避免上述偏见。

Public Function NextValue() As Integer

    Static current As Integer = 0
    ' only shuffling a part of the list
    For i = current + l.Count - 1 To current + 1 + offset Step -1
        Dim j = prng.Next(current + offset, i + 1)
        Dim tmp = l(i Mod l.Count)
        l(i Mod l.Count) = l(j Mod l.Count)
        l(j Mod l.Count) = tmp
    Next
    current += 1

    Return l((current - 1) Mod l.Count)

End Function

编辑2:

最后(希望如此),我认为解决方案非常简单。下面的代码假定有一个名为TheArray的N个元素的数组,它们包含随机顺序的元素(可以重写以使用有序数组)。值DelaySize确定值在绘制后应暂停多长时间。

Public Function NextValue() As Integer

    Static current As Integer = 0

    Dim SelectIndex As Integer = prng.Next(0, TheArray.Count - DelaySize)
    Dim ReturnValue = TheArray(SelectIndex)
    TheArray(SelectIndex) = TheArray(TheArray.Count - 1 - current Mod DelaySize)
    TheArray(TheArray.Count - 1 - current Mod DelaySize) = ReturnValue
    current += 1
    Return ReturnValue

End Function

答案 4 :(得分:1)

基于设置的方法:

如果阈值较低(比如低于40%),建议的方法是:

  • 拥有最后N*T个生成值的集合和队列。
  • 生成值时,请继续重新生成它,直到它不包含在集合中。
  • 当推送到队列时,弹出最旧的值并将其从集合中删除。

的伪代码:

generateNextValue:
  // once we're generated more than N*T elements,
  //   we need to start removing old elements
  if queue.size >= N*T
    element = queue.pop
    set.remove(element)

  // keep trying to generate random values until it's not contained in the set
  do
    value = getRandomValue()
  while set.contains(value)

  set.add(value)
  queue.push(value)

  return value

如果阈值很高,你可以直接转过头来:

  • 让该集代表最后N*T个生成值中的所有值
  • 反转所有设置操作(将所有设置添加替换为删除,反之亦然,并将contains替换为!contains)。

的伪代码:

generateNextValue:
  if queue.size >= N*T
    element = queue.pop
    set.add(element)

  // we can now just get a random value from the set, as it contains all candidates,
  //   rather than generating random values until we find one that works
  value = getRandomValueFromSet()
  //do
  //  value = getRandomValue()
  //while !set.contains(value)

  set.remove(value)
  queue.push(value)

  return value

基于混乱的方法:(稍微复杂一点)

如果阈值很高,则上述情况可能需要很长时间,因为它可能会继续生成已存在的值。

在这种情况下,一些基于shuffle的方法可能是一个更好的主意。

  • Shuffle数据。
  • 反复处理第一个元素。
  • 执行此操作时,将其移除并将其插回[N*T, N]范围内的随机位置。

示例:

假设N * T = 5,所有可能的值都是[1,2,3,4,5,6,7,8,9,10]

然后我们首先洗牌,给我们,让我们说,[4,3,8,9,2,6,7,1,10,5]

然后我们删除4并将其插回到[5,10]范围内的某个索引中(例如在索引5处)。

然后我们有[3,8,9,2,4,6,7,1,10,5]

继续删除下一个元素并根据需要将其插回。

<强>实施

如果我们不关心整体效率,那么阵列很好 - 获得一个元素将花费O(n)时间。

为了提高效率,我们需要使用支持高效随机位置插入和首次位置删除的有序数据结构。首先想到的是一个(自平衡)二叉搜索树,按索引排序。

我们不会存储实际索引,索引将由树的结构隐式定义。

在每个节点,我们将有一个子计数(自身为+ 1)(需要在插入/删除时更新)。

插入可以按如下方式完成:(暂时忽略自平衡部分)

// calling function
insert(node, value)
  insert(node, N*T, value)

insert(node, offset, value)
  // node.left / node.right can be defined as 0 if the child doesn't exist
  leftCount = node.left.count - offset
  rightCount = node.right.count

  // Since we're here, it means we're inserting in this subtree,
  //   thus update the count
  node.count++

  // Nodes to the left are within N*T, so simply go right
  // leftCount is the difference between N*T and the number of nodes on the left,
  //   so this needs to be the new offset (and +1 for the current node)
  if leftCount < 0
    insert(node.right, -leftCount+1, value)
  else
    // generate a random number,
    //   on [0, leftCount), insert to the left
    //   on [leftCount, leftCount], insert at the current node
    //   on (leftCount, leftCount + rightCount], insert to the right
    sum = leftCount + rightCount + 1
    random = getRandomNumberInRange(0, sum)
    if random < leftCount
      insert(node.left, offset, value)
    else if random == leftCount
      // we don't actually want to update the count here
      node.count--
      newNode = new Node(value)
      newNode.count = node.count + 1
      // TODO: swap node and newNode's data so that node's parent will now point to newNode
      newNode.right = node
      newNode.left = null
    else
      insert(node.right, -leftCount+1, value)

可视化在当前节点插入:

如果我们有类似的话:

    4
   /
  1
 / \
2   3

我们希望在5现在插入1,它会执行此操作:

  4
 /
5
 \
  1
 / \
2   3

请注意,例如,当red-black tree执行操作以保持自身平衡时,这些都不涉及比较,因此不需要知道任何已插入元素的顺序(即索引)。但它必须适当更新计数。

获得一个元素的整体效率为O(log n)