3

値のセットから連続してランダムに描画する場合、描画された値を再度描画することが許可されている場合、特定の値が (もちろん) すぐに連続して 2 回 (またはそれ以上) 描画される可能性はわずかですが、問題 (特定のアプリケーションの目的で) であり、この機会を排除したいと考えています。その方法に関するアルゴリズムのアイデアはありますか (シンプル/効率的)?

理想的には、データ セットのサイズのパーセンテージとしてしきい値を設定したいと考えています。

一連の値のサイズとN=100しきい値T=10%を指定すると、指定された値が現在の描画で描画された場合、次の描画で再び表示されないことが保証されN*T=10ます。

明らかに、この制限によりランダム選択に偏りが生じます。提案されたアルゴリズムが選択のランダム性にさらなるバイアスを導入することは気にしません。このアプリケーションにとって本当に重要なことは、選択が人間の観察者にとってそう見えるほどランダムであることです。

実装の詳細として、値はデータベース レコードとして格納されるため、データベース テーブルのフラグ/値、または外部メモリ構造を使用できます。抽象的なケースについての回答も大歓迎です。

編集:

私はこの他のSOの質問をここでヒットしました。これは私自身のものとよく重なっています。そこの良い点を通過します。

4

5 に答える 5

2

リストに n 個のアイテムがあり、最後の k 個のアイテムを選択したくないとします。

サイズ nk の配列からランダムに選択し、サイズ k のキューを使用して、描画したくないアイテムを貼り付けます (前面に追加し、背面から削除します)。

すべての操作は O(1) です。

---- 明確化 ----

n 個のアイテムを指定し、最後の k 回の描画を再描画しないという目標を設定し、次のように配列とキューを作成します。

  1. サイズ nk の配列 A を作成し、項目の nk をリストに入れます (ランダムに選択するか、好きなようにシードします)。

  2. キュー (リンクされたリスト) Q を作成し、残りの k 個のアイテムをランダムな順序または好きな順序で入力します。

ここで、アイテムをランダムに選択するたびに:

  1. 配列からランダムなインデックスを選択し、これを i と呼びます。

  2. A[i] を求めている人に渡して、Q の前に追加します。

  3. Q の後ろから要素を削除し、A[i] に格納します。

配列と連結リストが作成された後は、すべてが O(1) になります。これは、1 回限りの O(n) 操作です。

さて、n を変更したい場合 (つまり、要素を追加または削除したい場合) はどうすればよいのか疑問に思われるかもしれません。

要素を追加するたびに、k を決定するロジック (つまり、固定値、n の固定分数など) に応じて、A または Q のサイズを大きくしたいと考えています。

Q が増加した場合、結果は簡単です。新しい要素を Q に追加するだけです。この場合、Q の最後に追加して、できるだけ早く機能するようにします。それを A に入れ、A からいくつかの要素を追い出し、それを Q の最後に追加することもできます。

A が増加する場合、償却定数時間で配列を増加させる標準的な手法を使用できます。たとえば、A がいっぱいになるたびに、そのサイズを 2 倍にし、A の生きているセルの数を追跡します。(よくわからない場合は、ウィキペディアで「動的配列」を調べてください)。

于 2013-10-16T13:45:13.363 に答える
1

すべての「値」をサイズ N の「リスト」に入れ、リストをシャッフルして、リストの一番上から値を取得します。次に、任意のインデックス >= 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:

最後に (できれば)、解決策は非常に簡単だと思います。以下のコードは、要素をランダムな順序で含む、呼び出された N 個の要素の配列があることを前提としてTheArrayいます (並べ替えられた配列で動作するように書き直すことができます)。値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
于 2013-10-16T11:05:21.170 に答える
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
  • すべての set 操作を反転します (すべての set の add を remove に、またはその逆に置き換え、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

シャッフルベースのアプローチ: (上記よりやや複雑)

しきい値が高い場合、既に存在する値を生成し続ける可能性があるため、上記の処理に時間がかかる場合があります。

この場合、いくつかのシャッフル ベースのアプローチの方が適切な場合があります。

  • データをシャッフルします。
  • 最初の要素を繰り返し処理します。
  • その際、抜き差し範囲内のランダムな位置に差し込んでください[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].

次に、それを削除して、範囲内のインデックス(インデックス 5 など)4に挿入し直します。[5,10]

次に、 があり[3,8,9,2,4,6,7,1,10,5]ます。

そして、必要に応じて次の要素を削除し、挿入し直します。

実装:

効率性をあまり気にしないのであれば、配列は問題ありません。1 つの要素を取得するには時間がかかり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)、1 つの要素を取得することになります。

于 2013-10-16T11:08:24.757 に答える