当一些卡片无法使用时,从牌组中挑选随机卡的最有效方法是什么?

时间:2009-07-15 20:47:58

标签: algorithm math random probability

我有一个数组,告诉你卡是否正在使用:

int used[52];

如果我有很多二手卡,这是一种挑选随机卡的可怕方法:

do {
  card = rand() % 52;
} while (used[card]);

因为如果我只有3-4张未使用的卡片,那么找到它们将会永远存在。

我想出了这个:

 int card;
 int k = 0;
 int numUsed = 0;
 for (k=0; k < 52; ++k) {
   if (used[k]) numUsed += 1;
 }
 if (numUsed == 52) return -1;
 card = rand() % (52 - numUsed);

 for (k=0; k < 52; ++k) {
   if (used[k]) continue;
   if (card == 0) return k;
   card -= 1;
 }

我认为如果牌组已满,效果会更好,但是当牌组为空时工作会更糟,因为我必须通过两个for循环。

最有效的方法是什么?

9 个答案:

答案 0 :(得分:10)

我认为你的两遍算法很可能是你能做的最好的,考虑到你在评论中添加的约束,你事先并不知道哪些牌有资格参加抽签。

你可以尝试狡猾的“一次性从未知大小的列表中随机选择”算法:

int sofar = 0;
int selected = -1;
for (i = 0; i < 52; ++i) {
    if (used[i]) continue;
    ++sofar;
    if ((rand() % sofar) == 0) selected = i;
}
if (selected == -1) panic; // there were no usable cards 
else used[selected] = 1;   // we have selected a card

然后,如果(正如您在评论中所说)不同的抽奖具有不同的标准,您可以用实际标准替换used[i]

它的工作方式是你选择第一张牌。然后用第二张卡替换它,概率为1/2。用第三张卡替换结果的概率为1/3等。通过感应证明,经过n步后,每张卡的选择概率为1 / n。

此方法使用大量随机数,因此它可能比您的两遍版本慢,除非每个项目都很慢,或者评估标准很慢。它通常用于例如用于从文件中选择随机行,您实际上不希望两次运行数据。它对随机数中的偏差也很敏感。

但这很好又简单。

[编辑:证明

设p(j,k)为卡号j是步骤k后当前所选卡的概率。

需要证明:对于所有n,p(j,n)= 1 / n对于所有1&lt; = j&lt; = n

对于n = 1,显然p(1,1)= 1,因为在第一步选择第一张卡的概率为1/1 = 1.

假设对于所有1&lt; = j&lt; = k,p(j,k)= 1 / k。

然后我们在步骤(k + 1)选择第(k + 1)张卡,概率为1 /(k + 1),即p(k + 1,k + 1)= 1 /(k + 1)

我们以概率k /(k + 1)保留现有选择,因此对于任何j&lt; K + 1:

p(j,k+1) = p(j,k) * k/(k+1)
         = 1/k    * k/(k+1)   // by the inductive hypothesis
         = 1/(k+1)

所有1&lt; = k&lt; = k + 1

的p(j,k + 1)= 1 /(k + 1)

因此,通过归纳,对于所有1&lt; = j&lt; = n]

的所有n:p(j,n)= 1 / n

答案 1 :(得分:9)

为什么不保留另一张未使用的卡片?

如果您想以随机顺序排列它们,可以先将它们随机播放(Fisher-Yates),然后根据需要弹出它们。

答案 2 :(得分:6)

执行此操作的最佳方法是将套牌随机排列,然后选择第一张未使用的卡片。 Here's the most common way to perform a shuffle like this.

答案 3 :(得分:2)

处理随机卡的标准算法是。

  • 初始化牌组以包含所有牌(顺序并不重要)
  • 循环:
  • 生成0到甲板大小的随机索引 - 1
  • 在该索引处显示卡片(或做任何你想做的事)
  • 使用[deck-size -1]
  • 的卡片在卡座中交换索引卡片
  • 减少甲板大小
  • 转到循环:根据需要经常

答案 4 :(得分:1)

您可以使用以下代码摆脱这两个循环:

int card;
int k = 0;
int i = 0;
int unUsed[52];
int numUsed = 0;
for (k = 0; k < 52; ++k) {
  if (used[k]) {
    numUsed += 1;
  } else {
    unUsed[i] = k;
    i++;
  }
}
if (numUsed == 52) return -1;
card = rand() % (52 - numUsed);
return unUsed[card];

虽然我认为效率的提高不会很大,但你会使用更多的内存。

答案 5 :(得分:1)

另一个选项是拥有两个列表,一个用于跟踪已使用的卡,另一个用于跟踪未使用的卡。因此,如果您使用卡,请将其从未使用的卡列表中减去并将其添加到使用过的卡列表的末尾。这样,您不必每次都运行两个for循环。

答案 6 :(得分:0)

将已使用的卡片放在阵列的末尾,并将未使用的卡片保留在开头。跟踪尚未使用的卡数量。使用新卡时,将其与最后一张未使用的卡交换,并减少剩余卡的数量。

if (numRemaining == 0) return -1;
int cardNum = rand() % numRemaining;
Card card = cards[cardNum]; // or int, if cards are represented by their numbers
cards[cardNum] = cards[numRemaining - 1];
cards[numRemaining - 1] = card;
numRemaining--;

答案 7 :(得分:0)

答案 8 :(得分:-1)

我不确定这是否会产生真正的随机抽取,但几乎在所有情况下都会避免整个套牌的循环。我甚至不太确定如何比较性能明智,但在这里它仍然是:

  • 从甲板上获取随机卡
  • 如果卡已经使用,请选择随机方向(向前或向后)
  • 从当前位置沿着随机确定的方向走到牌组,直到找到下一张未使用的牌(当然你必须确保你正确地在阵列的两端缠绕)

所以在最糟糕的情况下,你在最后一个未使用过的卡片旁边挑一张牌,然后沿着'错误'方向走过牌组,从而在牌组中完成一个完整的循环。