【问题标题】:Algorithm to select a single, random combination of values?选择单个随机值组合的算法?
【发布时间】:2011-01-24 13:17:42
【问题描述】:

假设我有 y 不同的值,我想随机选择其中的 x。这样做的有效算法是什么?我可以只调用rand()x 次,但如果xy 很大,性能会很差。

请注意,此处需要组合:每个值应该具有相同的被选择概率,但它们在结果中的顺序并不重要。当然,任何生成 的算法都符合条件,但我想知道是否有可能在没有随机顺序要求的情况下更有效地做到这一点。

How do you efficiently generate a list of K non-repeating integers between 0 and an upper bound N 涵盖了这种情况的排列。

【问题讨论】:

  • @Jerry Coffin 回答不保证随机性,它只保证每个元素在输出中出现的概率相同。所以他的回答并不能解决你的问题。请查看my answer in cr 以了解有关此问题的更多信息并获得可行的算法。
  • @BrunoCosta 这个问题的标签是combinations,意思是“没有特定的顺序”(正如标签的描述所说)。

标签: permutations algorithm combinations


【解决方案1】:

罗伯特·弗洛伊德(Robert Floyd)针对这种情况发明了一种采样算法。它通常优于洗牌然后抓取第一个 x 元素,因为它不需要 O(y) 存储。正如最初所写的那样,它假定值来自 1..N,但是通过简单地将其产生的值作为下标处理到向量/数组/其他内容中来产生 0..N 和/或使用非连续值是微不足道的。

在伪代码中,算法是这样运行的(从 Jon Bentley 的 Programming Pearls 专栏“A sample of Brilliance”中窃取)。

initialize set S to empty
for J := N-M + 1 to N do
    T := RandInt(1, J)
    if T is not in S then
        insert T in S
    else
        insert J in S

最后一点(如果 T 已经在 S 中,则插入 J)是棘手的部分。底线是it assures the correct mathematical probability of inserting J,以便它产生无偏见的结果。

对于yO(x)1O(1)O(x) 存储。

请注意,根据问题中的标签,该算法仅保证每个元素在结果中出现的概率相等,而不是它们在其中的相对顺序。


1O(x2) 在最坏的情况下涉及的哈希映射可以忽略,因为它实际上是不存在所有值具有相同哈希的病理情况

【讨论】:

  • 找到了... ACM 通讯,1987 年 9 月,第 30 卷,第 9 期。
  • @Federico:我想我应该提到它,但它也可以在更多编程珍珠:程序员的自白中找到。我强烈推荐它。
  • 为什么 M=100, N=2^32 很糟糕?除此之外,我的意思是,很难在 1 .. 2^32 - 100 范围内获得一个统一的随机整数。在我看来很好:在 M=1 的最极端情况下,它只是从 1 .. N 中随机选择一个数字并获取相应的元素,这是最优的。事实上,如果 M 和 N 靠得很近,我会考虑翻转算法:选择 N-M 个元素,然后将原始集合的集合差异减去那个作为结果。减少对 RandInt 的调用次数。
  • 我花了一点时间来证明练习的正确性。我发布了它math.stackexchange.com/questions/178690/…
  • @BrunoCosta:我想这取决于你所说的“作品”是什么意思。正如它为结果生成一个集合这一事实所暗示的那样,它更多的是关于选择什么数字而不是顺序。如果您向它询问从 1 到 N 的 N 个数字,它会这样做(但是是的,它们将按顺序生成)。结果的顺序将取决于您使用的 Set 如何对其内容进行排序。
【解决方案2】:

假设您也希望订单是随机的(或者不介意它是随机的),我只会使用截断的 Fisher-Yates 洗牌。启动随机播放算法,但在您选择了第一个 x 值后停止,而不是“随机选择”所有 y

Fisher-Yates 的工作方式如下:

  • 随机选择一个元素,并将其与数组末尾的元素交换。
  • 递归(或更可能迭代)数组的其余部分,不包括最后一个元素。

第一个之后的步骤不修改数组的最后一个元素。前两个之后的步骤不会影响最后两个元素。第一个 x 之后的步骤不会影响最后一个 x 元素。因此,此时您可以停止 - 数组的顶部包含均匀随机选择的数据。数组的底部包含一些随机化的元素,但你得到的排列并不是均匀分布的。

当然,这意味着您已经丢弃了输入数组 - 如果这意味着您需要在开始之前对其进行复制,并且 x 与 y 相比较小,那么复制整个数组的效率不是很高。请注意,如果您将来要使用它的只是进一步选择,那么它的顺序有点随机这一事实并不重要,您可以再次使用它。因此,如果您多次进行选择,则可能一开始只能进行一份副本,并摊销成本。

【讨论】:

  • 这假设你有一个输入列表,你可以通过交换来修改它。这通常是正确的,但通常是不可能的。
  • 好点子 - 我已经间接指出了这一点,说底部已经有点随机化了,但我已经明确了。
  • 查看我的answer 实施非破坏性部分fisher-yates-knuth suffle,严格遵守O(n) 时间和空间
  • @SPWorley,看我的answer,它是非破坏性的部分洗牌
【解决方案3】:

如果你真的只需要生成combinations——元素的顺序无关紧要——你可以使用combinadics,因为它们是implemented e.g. here by James McCaffrey

将此与 k-permutations 进行对比,其中元素的顺序确实很重要。

第一种情况(1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), (3,2,1) 被认为是相同的- 在后者中,它们被认为是不同的,尽管它们包含相同的元素。

如果您需要组合,您可能真的只需要生成一个随机数(尽管它可能有点大) - 可以直接用于查找第 m 个组合。 由于此随机数代表特定组合的索引,因此您的随机数应介于 0 和 C(n,k) 之间。 计算组合数也可能需要一些时间。

这可能只是不值得麻烦 - 除了Jerry's and Federico's answer 肯定比实现组合更简单。 但是,如果您真的只需要一个组合,并且您对生成所需的确切随机位数量感到厌烦...... ;-)

虽然不清楚您是想要组合还是 k 排列,但这里是后者的 C# 代码(是的,如果 x > y/2,我们只能生成一个补码,但是我们会留下一个必须洗牌才能得到真正的 k 排列的组合):

static class TakeHelper
{
    public static IEnumerable<T> TakeRandom<T>(
        this IEnumerable<T> source, Random rng, int count)
    {
        T[] items = source.ToArray();

        count = count < items.Length ? count : items.Length;

        for (int i = items.Length - 1 ; count-- > 0; i--)
        {
            int p = rng.Next(i + 1);
            yield return items[p];
            items[p] = items[i];
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Random rnd = new Random(Environment.TickCount);
        int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7 };
        foreach (int number in numbers.TakeRandom(rnd, 3))
        {
            Console.WriteLine(number);
        }
    }
}

另一个更精细的实现生成k-permutations,我一直在闲逛,我相信如果您只需要迭代结果,它在某种程度上是对现有算法的改进。虽然它还需要生成 x 个随机数,但它在过程中只使用 O(min(y/2, x)) 内存:

    /// <summary>
    /// Generates unique random numbers
    /// <remarks>
    /// Worst case memory usage is O(min((emax-imin)/2, num))
    /// </remarks>
    /// </summary>
    /// <param name="random">Random source</param>
    /// <param name="imin">Inclusive lower bound</param>
    /// <param name="emax">Exclusive upper bound</param>
    /// <param name="num">Number of integers to generate</param>
    /// <returns>Sequence of unique random numbers</returns>
    public static IEnumerable<int> UniqueRandoms(
        Random random, int imin, int emax, int num)
    {
        int dictsize = num;
        long half = (emax - (long)imin + 1) / 2;
        if (half < dictsize)
            dictsize = (int)half;
        Dictionary<int, int> trans = new Dictionary<int, int>(dictsize);
        for (int i = 0; i < num; i++)
        {
            int current = imin + i;
            int r = random.Next(current, emax);
            int right;
            if (!trans.TryGetValue(r, out right))
            {
                right = r;
            }
            int left;
            if (trans.TryGetValue(current, out left))
            {
                trans.Remove(current);
            }
            else
            {
                left = current;
            }
            if (r > current)
            {
                trans[r] = left;
            }
            yield return right;
        }
    }

一般的想法是做一个Fisher-Yates shufflememorize the transpositions in the permutation。 它没有在任何地方发表,也没有得到任何同行评议。我相信这是一种好奇心,而不是有一些实用价值。尽管如此,我对批评持开放态度,并且通常想知道您是否发现它有任何问题 - 请考虑这一点(并在投反对票之前添加评论)。

【讨论】:

    【解决方案4】:

    一个小建议:如果x >> y/2,最好随机选择y-x个元素,然后选择互补集。

    【讨论】:

      【解决方案5】:

      例如,如果您有 2^64 个不同的值,则可以使用对称密钥算法(使用 64 位块)快速重新排列所有组合。 (例如河豚)。

      for(i=0; i<x; i++)
         e[i] = encrypt(key, i)
      

      这不是纯粹意义上的随机,但可以用于您的目的。 如果您想按照加密技术处理任意 # 个不同的值,您可以,但它更复杂。

      【讨论】:

        【解决方案6】:

        诀窍是使用shuffle 的变体,或者换句话说,部分洗牌。

        function random_pick( a, n ) 
        {
          N = len(a);
          n = min(n, N);
          picked = array_fill(0, n, 0); backup = array_fill(0, n, 0);
          // partially shuffle the array, and generate unbiased selection simultaneously
          // this is a variation on fisher-yates-knuth shuffle
          for (i=0; i<n; i++) // O(n) times
          { 
            selected = rand( 0, --N ); // unbiased sampling N * N-1 * N-2 * .. * N-n+1
            value = a[ selected ];
            a[ selected ] = a[ N ];
            a[ N ] = value;
            backup[ i ] = selected;
            picked[ i ] = value;
          }
          // restore partially shuffled input array from backup
          // optional step, if needed it can be ignored
          for (i=n-1; i>=0; i--) // O(n) times
          { 
            selected = backup[ i ];
            value = a[ N ];
            a[ N ] = a[ selected ];
            a[ selected ] = value;
            N++;
          }
          return picked;
        }
        

        注意该算法在时间和空间上都是严格的O(n),产生无偏选择(它是一个部分无偏洗牌)和输入数组上的非破坏性(作为部分洗牌),但这是可选的

        改编自here

        更新

        另一种方法,在[0,1] IVAN STOJMENOVIC, "ON RANDOM AND ADAPTIVE PARALLEL GENERATION OF COMBINATORIAL OBJECTS"3 部分)中仅对PRNG(伪随机数生成器)进行一次调用(第 3 节) 987654330@(最坏情况)复杂度

        【讨论】:

          【解决方案7】:

          这是一种简单的方法,仅当Y 远大于X 时效率低。

          void randomly_select_subset(
              int X, int Y,
              const int * inputs, int X, int * outputs
          ) {
              int i, r;
              for( i = 0; i < X; ++i ) outputs[i] = inputs[i];
              for( i = X; i < Y; ++i ) {
                  r = rand_inclusive( 0, i+1 );
                  if( r < i ) outputs[r] = inputs[i];
              }
          }
          

          基本上,将不同值的第一个 X 复制到输出数组,然后对于每个剩余值,随机决定是否包含该值。

          随机数进一步用于选择我们的(可变)输出数组中的一个元素来替换。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-11-18
            • 2012-04-12
            • 1970-01-01
            • 1970-01-01
            • 2012-05-19
            • 2012-02-21
            • 1970-01-01
            相关资源
            最近更新 更多