当随机不够随机:一个在线扑克游戏的教训

文章来源:http://kb.cnblogs.com/page/207086/
作者: LAURA D. HAMILTON  来源: 伯乐在线  发布时间: 2014-05-04 21:17  阅读: 3322 次  推荐: 10     [收藏]  

  今天我要讲一个发生于1999年,一个很流行的在线扑克平台的开发者开发的洗牌软件,带有很微小但很致命的漏洞的故事。虽然这个故事已经15年了,但它给算法开发者带来的教训仍有重要意义。

  在随机数产生器或算法中,很容易出现一些微小的漏洞,但这些漏洞可能会导致灾难性的结果。在线扑克和真正的扑克一样,是以洗牌开始的。保证洗牌的随机性尤为重要。

  一副正常的牌有52张,并且各不相同,这样就有52!,也就是8.0658×10^67种不同的洗牌方式。这是一个巨大的数字。

  1999年,ASF软件公司发布了这个软件,支持那个年代许多流行的在线扑克平台。他们发布了洗牌算法。

  算法如下, 看看能否找到不对的地方。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
procedure TDeck . Shuffle;
var
     ctr: Byte ;
     tmp: Byte ;
     random_number: Byte ;
begin
     { Fill the deck with unique cards }
     for ctr := 1 to 52 do
         Card[ctr] := ctr;
     { Generate a new seed based on the system clock }
     randomize;
     { Randomly rearrange each card }
     for ctr := 1 to 52 do begin
         random_number := random( 51 )+ 1 ;
         tmp := card[random_number];
         card[random_number] := card[ctr];
         card[ctr] := tmp;
     end ;
     CurrentCard := 1 ;
     JustShuffled := True ;
end ;

  错误1: 差一错误 

  上述算法试图遍历所有牌,将每一张牌跟另外一张随机选择的牌进行交换。但是犯了每个程序员都犯过的错误——差一错误。函数random(n)返回一个0到n-1之间的随机数,而不是程序员所想的1到n之间的。因此,这个算法中第52张牌永远不会和他自己进行交换,也就是说第52张牌永远不会停在第52个位置。这是随机洗牌不够随机的第一个原因。

  错误2:洗牌不均匀

  上述算法将第i张牌和另外一张从整副也就是52张牌中随机选择的牌进行交换。而合适的洗牌算法应该只和第i到第n张牌中的一张进行交换。这是因为考虑到每一张牌应该只进行一次随机交换。一副牌有n!种不同的排列,合适的洗牌算法应该只产生每种排列一次。原算法使一些排列出现的概率明显高于另一些排列,是个不好的实现。 

  错误3:32位种子

  如果你的业务或技术依赖于随机数的使用,最好的选择是采用一个硬件随机数产生器。ASF却不是,他用了一个带有伪随机数产生器的确定机。更糟糕的是,他使用的是32位的种子。由于种子100%的决定了伪随机数产生器的输出,只有N^32种可能的种子值就意味着只有N^32种可能的打乱顺序。所以在理论上有8.0658×10^67种打乱顺序的情况下,他只有4百万可能。

  错误4:系统时钟作为种子

  上述算法使用Pascal函数Randomize()生成随机数,而这个函数是根据从午夜开始的毫秒数来选择种子的。由于一天之中只有86,400,000毫秒,也就意味着上述算法只能产生86,400,000种可能的乱序。

  但更糟糕的是,由于随机数产生器的种子是基于服务器时钟的,黑客们只要将他们的程序与服务器时钟同步就能够将可能出现的乱序减少到只有200,000种。到那个时候一旦黑客知道5张牌,他就可以实时的对200,000种可能的乱序进行快速搜索,找到游戏中的那种。所以一旦黑客知道手中的两张牌和3张公用牌,就可以猜出转牌和河牌时会来什么牌,以及其他玩家的牌。(注:在德州扑克中,倒数第二张公共牌,叫“转牌”,最后一张牌,叫“河牌”。)

  以《算法》的作者Robert Sedgewick的一段话作为结束语:

“That’s a pretty tough thing to have happen if you’re implementing online poker. You might want to make sure that if you’re advertising that you’re doing a random shuffle that you go ahead and do so.”—Robert Sedgewick, Professor of Computer Science, Princeton

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 可以使用rand()函数生成随机数,然后将生成的随机数对52取余数,得到的余数就是一张牌的编号。可以使用一个数组来表示一副扑克牌,然后将数组中的元素随机交换位置,最后将数组分成两份即可。具体代码实现可以参考以下示例: ```c #include <stdio.h> #include <stdlib.h> #include <time.h> int main() { int poker[52] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52}; int i, j, temp; srand((unsigned)time(NULL)); // 初始化随机数种子 // 洗牌 for (i = ; i < 52; i++) { j = rand() % 52; temp = poker[i]; poker[i] = poker[j]; poker[j] = temp; } // 分牌 int player1[26], player2[26]; for (i = ; i < 26; i++) { player1[i] = poker[i]; player2[i] = poker[i+26]; } // 输出结果 printf("Player 1: "); for (i = ; i < 26; i++) { printf("%d ", player1[i]); } printf("\n"); printf("Player 2: "); for (i = ; i < 26; i++) { printf("%d ", player2[i]); } printf("\n"); return ; } ``` ### 回答2: 扑克随机分成两份的方法有很多种,下面我给出一种简单的算法。 首先,创建一副标准的扑克牌,包括52张牌,分别是梅花、红桃、方块和黑桃的2到Ace,即2、3、4、5、6、7、8、9、10、J、Q、K、A。 1. 首先,创建一个数组cards来表示扑克牌,即将所有的牌按顺序放入数组中。 2. 接下来,使用随机数生成器来生成一个0到51之间的随机整数,表示要随机选取的牌的下标。 3. 将选取的牌从数组中取出,放入第一份牌中。可以使用另一个数组hand1来表示第一份牌,将选中的牌加入到hand1数组中。 4. 再次使用随机数生成器生成一个0到51之间的随机整数,表示要随机选取的牌的下标。 5. 将选取的牌从数组中取出,放入第二份牌中。使用另一个数组hand2来表示第二份牌,将选中的牌加入到hand2数组中。 6. 重复步骤4和5,直到将所有的牌都分完为止。 最后,hand1数组中的牌即为第一份牌,hand2数组中的牌即为第二份牌。这样就完成了扑克牌的随机分成俩份的过程。 需要注意的是,上述方法是一种简单的随机分牌算法,并不是真正的完全随机。如果需要更加严谨的随机性,可以采用更复杂的算法或使用专门的随机数生成器库。 ### 回答3: 要随机地将一副扑克牌分成两份,可以按照以下步骤进行: 1. 首先,将一副有52张牌的扑克牌洗牌,确保牌的顺序是随机的。这可以通过使用C语言中的随机函数来实现。需要注意的是,在开始洗牌之前,需要调用srand函数并以当前时间作为种子,以确保每次生成的随机数序列都是不同的。 2. 然后,创建两个空的扑克牌组,分别用于存储洗牌后的前半部分和后半部分的牌。 3. 使用随机数函数生成一个数值范围在0到51之间的随机整数。这个随机数将作为洗牌后的牌堆中取牌的索引。 4. 将洗牌后的牌堆中索引为随机数的牌取出,并将其添加到第一组扑克牌中。 5. 重复步骤3和步骤4,直到第一组扑克牌的数量达到原始牌堆数量的一半。 6. 将剩余的牌全部添加到第二组扑克牌中。 最后,你将会得到两份牌,每份都是随机分配的一半扑克牌。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值