【问题标题】:How can I fairly choose an item from a list?我怎样才能公平地从列表中选择一个项目?
【发布时间】:2009-12-17 03:15:59
【问题描述】:

假设我有一份奖品清单:

奖A B奖 奖品C

而且,对于他们每个人,我想从我的与会者名单中选出一名获胜者。

假设我的与会者名单如下:

用户1、用户2、用户3、用户4、用户5

从该列表中选择用户的公正方法是什么?

显然,我将使用加密安全的伪随机数生成器,但如何避免偏向列表的前面?我假设我不会使用模数?

编辑
所以,这就是我想出的:

class SecureRandom
{
    private RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

    private ulong NextUlong()
    {
        byte[] data = new byte[8];
        rng.GetBytes(data);
        return BitConverter.ToUInt64(data, 0);
    }

    public int Next()
    {
        return (int)(NextUlong() % (ulong)int.MaxValue);
    }

    public int Next(int maxValue)
    {
        if (maxValue < 0)
        {
            throw new ArgumentOutOfRangeException("maxValue");
        }

        if (maxValue == 0)
        {
            return 0;
        }

        ulong chop = ulong.MaxValue - (ulong.MaxValue % (ulong)maxValue);

        ulong rand;

        do
        {
            rand = NextUlong();
        } while (rand >= chop);

        return (int)(rand % (ulong)maxValue);
    }
}

注意:

Next() 在 [0, int.MaxValue] 范围内返回一个 int
Next(int.MaxValue) 在 [0, int.MaxValue) 范围内返回一个 int

【问题讨论】:

  • 对第 10 位进行取模,或者使用更好的随机数生成器。只要知道大小,模数有什么问题?您可以编写一个单元测试,以确保您生成的分布是体面的。但是,在一些罕见的平行宇宙中,您可以连续获得一百万个零。 ;)
  • 原谅我的无知,但为什么“RNG”会有偏见?
  • 不一定。使用模数会。

标签: algorithm language-agnostic random


【解决方案1】:

特殊随机数生成器的伪代码:

rng is random number generator produces uniform integers from [0, max)
compute m = max modulo length of attendee list
do {
    draw a random number r from rng
} while(r >= max - m)
return r modulo length of attendee list

这消除了对列表前部的偏见。那么

put the attendees in some data structure indexable by integers
for every prize in the prize list
draw a random number r using above
compute index = r modulo length of attendee list
return the attendee at index

在 C# 中:

public NextUnbiased(Random rg, int max) {
    do {
        int r = rg.Next();
    } while(r >= Int32.MaxValue - (Int32.MaxValue % max));
    return r % max;
}

public Attendee SelectWinner(IList<Attendee> attendees, Random rg) {    
    int winningAttendeeIndex = NextUnbiased(rg, attendees.Length)
    return attendees[winningAttendeeIndex];
}

然后:

// attendees is list of attendees
// rg is Random
foreach(Prize prize in prizes) {
    Attendee winner = SelectWinner(attendees, rg);
    Console.WriteLine("Prize {0} won by {1}", prize.ToString(), winner.ToString());
}

【讨论】:

  • PSRNG 类是否存在相同的函数?
  • 另外,这被标记为与语言无关。 ;)
  • PSRNG 类?解决了与语言无关的问题。
  • 对不起,CSPRNG。喜欢RNGCryptoServiceProvider
  • 哦,我知道你用模数做了什么,刮掉了最上面的一组数字......
【解决方案2】:

假设一个公平分布的随机数生成器...

do {
    i = rand();
} while (i >= RAND_MAX / 5 * 5);
i /= 5;

这给出了 5 个插槽中的每一个

[ 0 .. RAND_MAX / 5 )
[ RAND_MAX / 5 .. RAND_MAX / 5 * 2 )
[ RAND_MAX / 5 * 2 .. RAND_MAX / 5 * 3 )
[ RAND_MAX / 5 * 3 .. RAND_MAX / 5 * 4 )
[ RAND_MAX / 5 * 4 .. RAND_MAX / 5 * 5 )

并丢弃超出范围的掷骰。

【讨论】:

    【解决方案3】:

    您已经有了几个非常好的答案,这取决于提前知道列表的长度。

    要从列表中公平地选择单个项目而不首先需要知道列表的长度,请执行以下操作:

     if (list.empty()) error_out_somehow
     r=list.first()          // r is a reference or pointer
     s=list.first()          // so is s
     i = 2
     while (r.next() is not NULL)
        r=r.next()
        if (random(i)==0) s=r  // random() returns a uniformly
                               // drawn integer between 0 and i
        i++
     return s
    

    (如果您将列表存储为链表,则很有用)


    要在这种情况下分发奖品,只需在奖品列表中为每个奖品随机选择一名获胜者即可。 (如果您想防止双赢,请将获胜者从参与者列表中删除。)


    为什么会起作用?

    1. 您从1/1 的第一项开始
    2. 在下一次通过时,你选择了第二个项目一半的时间(1/2),这意味着第一个项目有概率1 * (2-1)/2 = 1/2
    3. 在进一步的迭代中,您以1/n 的概率选择第n 个项目,并且每个先前项目的机会减少(n-1)/n 的系数

    这意味着当你走到最后时,在列表中(n 项)中出现第 m 项的机会是

    1/m * m/(m+1) * (m+1)/(m+2) * ... * (n-2)/(n-1) * (n-1)/n = 1/n
    

    并且对于每个项目都是相同的。


    如果你注意了,你会注意到这意味着每次你想从列表中选择一个项目时都要遍历整个列表,所以这对于(比如说)重新排序整个列表并不是最有效的(尽管它确实公平)。

    【讨论】:

      【解决方案4】:

      我想一个答案是为每个项目分配一个随机值,并取最大或最小的,根据需要向下钻取。

      我不确定这是否是最有效的,虽然...

      【讨论】:

        【解决方案5】:

        如果您使用的是良好的数字生成器,即使使用模数,您的偏差也会很小。例如,如果您正在使用具有 64 位熵和五个用户的随机数生成器,那么您对数组前面的偏差应该是 3x10^-19 的数量级(我的数字可能是关闭的,我不知道)不要想太多)。与后来的用户相比,第一个用户获胜的可能性增加了 10 分之三。这应该足以在任何人的书中保持公平。

        【讨论】:

        • “非常微小”的偏见对我的用户来说不够好。
        【解决方案6】:

        您可以从提供商处购买真正随机的位,或使用机械设备。

        【讨论】:

          【解决方案7】:

          Here 你会发现 Oleg Kiselyov 对纯函数式随机改组的讨论。

          链接内容的描述(引自那篇文章的开头):

          本文将给出两个完美的纯函数式程序, 随机且均匀地打乱一系列任意元素。我们 证明算法是正确的。算法实现 在 Haskell 中,可以轻松地重写为其他(功能) 语言。我们还讨论了为什么常用的基于排序的 shuffle 算法没有完美的洗牌。

          您可以使用它来打乱您的列表,然后选择打乱结果的第一项(或者您可能不想给同一个人两个奖品 - 然后使用结果的 n 个初始位置,对于 n =奖品数量);或者您可以简化算法以仅生成第一项;或者你可以看看那个网站,因为我可以发誓有一篇关于从具有均匀分布的任意树状结构中选择一个随机元素的文章,以纯粹的功能方式,提供正确性证明,但是我的搜索结果让我失望了,我似乎找不到它。

          【讨论】:

            【解决方案8】:

            如果没有真正的随机位,您总会有一些偏差。即使是相当少的客人和奖品,为客人分配奖品的方式也比任何常见的 PRNG 时期都要多得多。按照 lpthnc 的建议,购买一些真正的随机位,或者购买一些随机位生成硬件。

            至于算法,只需对客人列表进行随机洗牌即可。小心,因为幼稚的洗牌算法确实有偏见:http://en.wikipedia.org/wiki/Shuffling#Shuffling_algorithms

            【讨论】:

            【解决方案9】:

            您可以 100% 可靠地从任意列表中选择一个随机项目,只需一次通过,而无需提前知道列表中有多少项目。

            伪代码:

            count = 0.0;
            item_selected = none;
            
            foreach item in list
             count = count + 1.0;
             chance = 1.0 / count;
             if ( random( 1.0 ) <= chance ) then item_selected = item;
            

            测试程序比较单个 rand() % N 与上述迭代的结果:

            #include "stdafx.h"
            #include <stdio.h>
            #include <stdlib.h>
            #include <memory.h>
            
            static inline float frand01()
            {
                return (float)rand() / (float)RAND_MAX;
            }
            
            int _tmain(int argc, _TCHAR* argv[])
            {
                static const int NUM_ITEMS = 50;
            
                int resultRand[NUM_ITEMS];
                int resultIterate[NUM_ITEMS];
            
                memset( resultRand, 0, NUM_ITEMS * sizeof(int) );
                memset( resultIterate, 0, NUM_ITEMS * sizeof(int) );
            
                for ( int i = 0; i < 100000; i++ )
                {
                    int choiceRand = rand() % NUM_ITEMS;
            
                    int choiceIterate = 0;
                    float count = 0.0;
                    for ( int item = 0; item < NUM_ITEMS; item++ )
                    {
                        count = count + 1.0f;
                        float chance = 1.0f / count;
                        if ( frand01() <= chance )
                        {
                            choiceIterate = item;
                        }
                    }
            
                    resultRand[choiceRand]++;
                    resultIterate[choiceIterate]++;
                }
            
                printf("Results:\n");
                for ( int i = 0; i < NUM_ITEMS; i++ )
                {
                    printf( "%02d - %5d %5d\n", i, resultRand[i], resultIterate[i] );
                }
            
                return 0;
            }
            

            输出:

            Results:
            00 -  2037  2050
            01 -  2038  2009
            02 -  2094  1986
            03 -  2007  1953
            04 -  1990  2142
            05 -  1867  1962
            06 -  1941  1997
            07 -  2023  1967
            08 -  1998  2070
            09 -  1930  1953
            10 -  1972  1900
            11 -  2013  1985
            12 -  1982  2001
            13 -  1955  2063
            14 -  1952  2022
            15 -  1955  1976
            16 -  2000  2044
            17 -  1976  1997
            18 -  2117  1887
            19 -  1978  2020
            20 -  1886  1934
            21 -  1982  2065
            22 -  1978  1948
            23 -  2039  1894
            24 -  1946  2010
            25 -  1983  1927
            26 -  1965  1927
            27 -  2052  1964
            28 -  2026  2021
            29 -  2090  1993
            30 -  2039  2016
            31 -  2030  2009
            32 -  1970  2094
            33 -  2036  2048
            34 -  2020  2046
            35 -  2010  1998
            36 -  2104  2041
            37 -  2115  2019
            38 -  1959  1986
            39 -  1998  2031
            40 -  2041  1977
            41 -  1937  2060
            42 -  1946  2048
            43 -  2014  1986
            44 -  1979  2072
            45 -  2060  2002
            46 -  2046  1913
            47 -  1995  1970
            48 -  1959  2020
            49 -  1970  1997
            

            【讨论】:

            • 这将非常重地放在列表的开头,并且也将无法选择一个项目。例如,想象一个只有一个项目的列表。
            • 其实不是。 (好的,所以它应该是
            • 这里有一个测试程序来证明:
            • 啊,好的。我明白这是如何工作的。这很酷,但不是我真正想要的。
            • 虽然dmckee的回答是一样的,而且效率更高。执行 rand() % N == 0 比处理浮点精度的东西要快。
            猜你喜欢
            • 2023-01-19
            • 2021-08-03
            • 2022-01-17
            • 2016-04-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-06-08
            相关资源
            最近更新 更多